Compare commits

...

32 Commits

Author SHA1 Message Date
Tara Zou
de89fd2011 use unitPriceinBillingCurrency instead 2024-06-05 15:33:46 -04:00
Tara Zou
5ee2af75dd fix format 2024-05-10 15:11:25 -04:00
Tara Zou
586ae376a3 update pricing UPAPI properties 2024-05-10 15:09:12 -04:00
Tara Zou
5de6c46a93 Merge branch 'master' into users/tarazou/UpdateFetchPriceAPI 2024-04-29 16:30:32 -04:00
Asier Isayas
92246144f7 Enable Legacy Mongo Shell in Fairfax (#1829)
* enable Mongo Proxy and LMS in sovereign clouds

* remove mooncake

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-29 16:25:58 -04:00
Tara Zou
d712327653 remove dupicate check 2024-04-29 15:53:26 -04:00
SATYA SB
a08415e7bc [3100018:[Programmatic Access - Azure Cosmos DB - Edit Property]: Text Area edit field does not have a label under 'Edit Property' pane. (#1819)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-04-29 22:26:27 +05:30
SATYA SB
b94ce28e96 [accessibility-2724013]:[Screen reader - Cosmos DB - Data Explorer -> Entities -> Add entity]: Screen reader announces incorrect role when focus lands on the "Edit" and "Delete" buttons. (#1822)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-04-29 22:23:49 +05:30
Tara Zou
e2d34d5131 Merge branch 'master' into users/tarazou/UpdateFetchPriceAPI 2024-04-26 16:24:48 -04:00
Tara Zou
d8bff694c6 fix lint 2024-04-26 16:19:35 -04:00
Tara Zou
7fbc33e82b fix compile 2024-04-26 16:12:56 -04:00
Tara Zou
c7c0a3a979 fix format 2024-04-26 15:12:11 -04:00
sunghyunkang1111
f8f7ea34bd Copilot rewording (#1824)
* Copilot rebranding to query advisor

* fix the subquery link
2024-04-26 14:09:55 -05:00
Asier Isayas
cbd5e6bf76 open Legacy Mongo SHell with correct base URL in sovereign clouds (#1823)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-26 14:55:47 -04:00
Tara Zou
6ba2ff6000 update UPAPI calls 2024-04-26 14:15:59 -04:00
Tara Zou
d7718c42da upapi update 2024-04-24 18:51:09 -04:00
Ashley Stanton-Nurse
618c5ec0fe Add button (and keyboard shortcut) to download query (#1817) 2024-04-24 15:11:51 -07:00
Asier Isayas
afc82845b5 activate Token Controller (#1820)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-24 15:04:01 -04:00
Ashley Stanton-Nurse
f4bcee5461 initialize new documents with their partition key (#1815)
* initialize new documents with their partition key

* refmt
2024-04-23 15:47:04 -07:00
Ashley Stanton-Nurse
17207624a9 add more intl-friendly tab nav shortcuts (#1814) 2024-04-23 15:46:41 -07:00
jawelton74
d36e511b18 Update d3, webpack-dev-server, typedoc dependencies. (#1812)
* Update d3, webpack-dev-server, typedoc dependencies.

* Fix unit test failures.

* Revert change to snapshot as it doesn't seem required when running in
github.
2024-04-23 10:15:48 -07:00
Ashley Stanton-Nurse
c1a28793ba bind F5 to execute query (#1813) 2024-04-23 09:08:29 -07:00
Asier Isayas
acf5acfdb4 Remove Legacy Mongo Shell feature flag (#1810)
* LMS Mongo Proxy support

* change stirng to url for get mongo shell url

* fix tests

* enable feature flag

* fixed unit test

* add mongoshell to path

* remove LMS feature flag

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-23 08:20:27 -04:00
jawelton74
7b81767ded Enable new backend for Settings API in Prod. (#1791) 2024-04-22 14:34:20 -07:00
jawelton74
c12eced120 Update node-fetch, react-dev-utils and azure/identity dependencies. (#1809) 2024-04-22 07:10:16 -07:00
jawelton74
2b15a4d43d Update package.json (#1807) 2024-04-19 15:03:11 -07:00
Ashley Stanton-Nurse
c220a8b070 [Task 3071878] Tab Navigation Keyboard Shortcuts (#1808)
* [Task 3071878] Tab Navigation Keyboard Shortcuts

* throw in development on duplicate handlers

* refmt
2024-04-19 13:44:30 -07:00
Ashley Stanton-Nurse
a5a5a95973 [Task 3061766] Additional Keyboard Shortcuts (#1805)
* [Task 3061766] Additional Keyboard Shortcuts

refmt and fix lints

shortcuts for: discard, new item/sproc/udf/trigger, delete
item/sproc/udf/trigger

copilot shortcut

* remove 'Ctrl+I' due to conflict with Monaco Autocomplete
2024-04-19 09:43:27 -07:00
Asier Isayas
e3fab9b5bf Add 'mongoshell' to Legacy Mongo Shell path (#1806)
* LMS Mongo Proxy support

* change stirng to url for get mongo shell url

* fix tests

* enable feature flag

* fixed unit test

* add mongoshell to path

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-18 15:39:13 -04:00
Asier Isayas
98000a27f0 Legacy Mongo Shell Mongo Proxy support (#1802)
* LMS Mongo Proxy support

* change stirng to url for get mongo shell url

* fix tests

* enable feature flag

* fixed unit test

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-17 19:01:12 -04:00
Ashley Stanton-Nurse
af664326ea Fix issues with the command bar when switching through React and Trigger tabs (#1804)
* fix bug in trigger tab that takes over the command bar while open

* clear context buttons when a react tab is active

* restore unintentionally removed code

* reformat
2024-04-17 15:57:29 -07:00
Ashley Stanton-Nurse
a44ed1f45c [Task 3061766] Global Keyboard Shortcuts, implemented through the Command Bar (#1789)
* keyboard shortcuts using tinykeys

* refmt and fix lints

* retarget keyboard shortcuts to the body instead of the root element of the React component tree

* refmt

* Update src/Explorer/Menus/CommandBar/CommandBarUtil.tsx

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* add Save binding to New Item command bar

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
2024-04-17 11:19:09 -07:00
42 changed files with 1866 additions and 1189 deletions

View File

@@ -76,6 +76,10 @@ module.exports = {
"^dnd-core$": "dnd-core/dist/cjs", "^dnd-core$": "dnd-core/dist/cjs",
"^react-dnd$": "react-dnd/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs",
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
"d3-force": "<rootDir>/node_modules/d3-force/dist/d3-force.min.js",
"d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js",
"d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js",
"d3-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js",
}, },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@@ -130,7 +134,6 @@ module.exports = {
// The test environment that will be used for testing // The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom", // testEnvironment: "jest-environment-jsdom",
modulePaths: ["node_modules", "<rootDir>/src"], modulePaths: ["node_modules", "<rootDir>/src"],
// Options that will be passed to the testEnvironment // Options that will be passed to the testEnvironment

1902
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos": "4.0.1-beta.2",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.1.1",
"@azure/msal-browser": "2.14.2", "@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
@@ -46,6 +46,7 @@
"@types/lodash": "4.14.171", "@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"@xmldom/xmldom": "0.7.13",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
@@ -54,7 +55,7 @@
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"d3": "6.1.1", "d3": "7.8.5",
"datatables.net-colreorder-dt": "1.7.0", "datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8", "datatables.net-dt": "1.13.8",
"date-fns": "1.29.0", "date-fns": "1.29.0",
@@ -69,12 +70,14 @@
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23", "i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0", "iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery-typeahead": "2.11.1", "jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.2", "jquery-ui-dist": "1.13.2",
"knockout": "3.5.1", "knockout": "3.5.1",
"loader-utils": "2.0.3",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.44.0", "monaco-editor": "0.44.0",
"ms": "2.1.3", "ms": "2.1.3",
@@ -98,10 +101,12 @@
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3", "sanitize-html": "2.3.3",
"shell-quote": "1.7.3",
"styled-components": "5.0.1", "styled-components": "5.0.1",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
"underscore": "1.9.1", "tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
@@ -170,25 +175,25 @@
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0", "mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "7.1.0", "monaco-editor-webpack-plugin": "7.1.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.7",
"playwright": "1.13.0", "playwright": "1.13.0",
"prettier": "3.0.3", "prettier": "3.0.3",
"process": "0.11.10", "process": "0.11.10",
"querystring-es3": "0.2.1", "querystring-es3": "0.2.1",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4", "react-dev-utils": "12.0.1",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "9.2.4", "ts-loader": "9.2.4",
"typedoc": "0.21.5", "typedoc": "0.22.15",
"typescript": "4.3.5", "typescript": "4.3.5",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "5.88.2", "webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1", "webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1" "webpack-dev-server": "4.15.2"
}, },
"scripts": { "scripts": {
"postinstall": "patch-package", "postinstall": "patch-package",

View File

@@ -138,7 +138,7 @@ export class PortalBackendEndpoints {
} }
export class MongoProxyEndpoints { export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238"; public static readonly Local: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
@@ -249,6 +249,7 @@ export class HttpHeaders {
public static partitionKey: string = "x-ms-documentdb-partitionkey"; public static partitionKey: string = "x-ms-documentdb-partitionkey";
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput"; public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
public static xAPIKey: string = "X-API-Key";
} }
export class ContentType { export class ContentType {

View File

@@ -672,6 +672,28 @@ export function getEndpoint(endpoint: string): string {
return url; return url;
} }
export function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
];
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 (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
);
}
// 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> {
@@ -688,24 +710,3 @@ async function errorHandling(response: Response, action: string, params: unknown
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
} }
function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
);
}

View File

@@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image <Image
{...imageProps} {...imageProps}
src={EditIcon} src={EditIcon}
alt="editEntity" alt={`Edit ${entityProperty} entity`}
onClick={onEditEntity} onClick={onEditEntity}
tabIndex={0} tabIndex={0}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
@@ -156,7 +156,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image <Image
{...imageProps} {...imageProps}
src={DeleteIcon} src={DeleteIcon}
alt="delete entity" alt={`Delete ${entityProperty} entity`}
id="deleteEntity" id="deleteEntity"
onClick={onDeleteEntity} onClick={onDeleteEntity}
tabIndex={0} tabIndex={0}

View File

@@ -42,6 +42,9 @@ export interface ConfigContext {
ARM_API_VERSION: string; ARM_API_VERSION: string;
GRAPH_ENDPOINT: string; GRAPH_ENDPOINT: string;
GRAPH_API_VERSION: string; GRAPH_API_VERSION: string;
CATALOG_ENDPOINT: string;
CATALOG_API_VERSION: string;
CATALOG_API_KEY: string;
ARCADIA_ENDPOINT: string; ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string; BACKEND_ENDPOINT?: string;
@@ -83,6 +86,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`,
], // 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/",
@@ -92,6 +96,9 @@ let configContext: Readonly<ConfigContext> = {
ARM_API_VERSION: "2016-06-01", ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.microsoft.com", GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6", GRAPH_API_VERSION: "1.6",
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
CATALOG_API_VERSION: "2023-05-01-preview",
CATALOG_API_KEY: "",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net", ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
@@ -108,6 +115,7 @@ let configContext: Readonly<ConfigContext> = {
"updateDocument", "updateDocument",
"deleteDocument", "deleteDocument",
"createCollectionWithProxy", "createCollectionWithProxy",
"legacyMongoShell",
], ],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,

View File

@@ -420,6 +420,7 @@ export interface SelfServeFrameInputs {
authorizationToken: string; authorizationToken: string;
csmEndpoint: string; csmEndpoint: string;
flights?: readonly string[]; flights?: readonly string[];
catalogAPIKey: string;
} }
export class MonacoEditorSettings { export class MonacoEditorSettings {

View File

@@ -1,6 +1,7 @@
/** /**
* React component for Command button component. * React component for Command button component.
*/ */
import { KeyboardAction } from "KeyboardShortcuts";
import * as React from "react"; import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
@@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
/** /**
* Click handler for command button click * Click handler for command button click
*/ */
onCommandClick: (e: React.SyntheticEvent) => void; onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
/** /**
* Label for the button * Label for the button
@@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
* Vertical bar to divide buttons * Vertical bar to divide buttons
*/ */
isDivider?: boolean; isDivider?: boolean;
/** /**
* Aria-label for the button * Aria-label for the button
*/ */
ariaLabel: string; ariaLabel: string;
/**
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
* If not specified, the button will not be triggerable by keyboard shortcuts.
*/
keyboardAction?: KeyboardAction;
} }
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> { export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {

View File

@@ -349,7 +349,7 @@ export class NodePropertiesComponent extends React.Component<
onActivated={this.setIsDeleteConfirm.bind(this, true)} onActivated={this.setIsDeleteConfirm.bind(this, true)}
aria-label="Delete this vertex" aria-label="Delete this vertex"
> >
<img src={DeleteIcon} alt="Delete" /> <img src={DeleteIcon} alt="Delete" role="button" />
</AccessibleElement> </AccessibleElement>
); );
} else { } else {
@@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component<
aria-label="Edit properties" aria-label="Edit properties"
onActivated={expandClickHandler} onActivated={expandClickHandler}
> >
<img src={EditIcon} alt="Edit" /> <img src={EditIcon} alt="Edit" role="button" />
</AccessibleElement> </AccessibleElement>
)} )}

View File

@@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
className="rightPaneTrashIcon rightPaneBtns" className="rightPaneTrashIcon rightPaneBtns"
tabIndex={0} tabIndex={0}
role="button" role="button"
aria-label={`Delete ${data.key}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)} onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
removeNewVertexPropertyKeyPress(event, index) removeNewVertexPropertyKeyPress(event, index)
} }
> >
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" /> <img
aria-label="hidden"
className="refreshcol rightPaneTrashIconImg"
src={DeleteIcon}
alt="Remove property"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,6 +5,7 @@
*/ */
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
@@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const buttons = 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);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons = const buttons =
@@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
}, },
}; };
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
setKeyboardHandlers(keyboardHandlers);
return ( return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}> <div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar <FluentCommandBar

View File

@@ -1,3 +1,4 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
@@ -57,6 +58,7 @@ export function createStaticCommandBarButtons(
buttons.push(homeBtn); buttons.push(homeBtn);
const newCollectionBtn = createNewCollectionGroup(container); const newCollectionBtn = createNewCollectionGroup(container);
newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below.
buttons.push(newCollectionBtn); buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") { if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container); const addSynapseLink = createOpenSynapseLinkDialogButton(container);
@@ -94,6 +96,7 @@ export function createStaticCommandBarButtons(
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@@ -277,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
return { return {
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_DATABASE,
onCommandClick: async () => { onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) { if (throughputCap && throughputCap !== -1) {
@@ -297,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@@ -312,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
@@ -337,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@@ -356,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newUserDefinedFunctionBtn: CommandButtonComponentProps = { const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon, iconSrc: AddUdfIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_UDF,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
@@ -375,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newTriggerBtn: CommandButtonComponentProps = { const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon, iconSrc: AddTriggerIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_TRIGGER,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
@@ -397,6 +406,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
return { return {
iconSrc: BrowseQueriesIcon, iconSrc: BrowseQueriesIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY,
onCommandClick: () => onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />), useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label, commandButtonLabel: label,
@@ -411,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
return { return {
iconSrc: OpenQueryFromDiskIcon, iconSrc: OpenQueryFromDiskIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />), onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -7,6 +7,7 @@ import {
IDropdownStyles, IDropdownStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
import * as React from "react"; import * as React from "react";
import _ from "underscore"; import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg"; import ChevronDownIcon from "../../../../images/Chevron_down.svg";
@@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
onRender: () => <ConnectionStatus container={container} poolId={poolId} />, onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
}; };
}; };
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {};
function createHandlers(buttons: CommandButtonComponentProps[]) {
buttons.forEach((button) => {
if (!button.disabled && button.keyboardAction) {
handlers[button.keyboardAction] = (e) => {
button.onCommandClick(e);
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
return true;
};
}
if (button.children && button.children.length > 0) {
createHandlers(button.children);
}
});
}
createHandlers(allButtons);
return handlers;
}

View File

@@ -630,7 +630,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
Enable sample database Enable sample database
<InfoTooltip> <InfoTooltip>
This is a sample database and collection with synthetic product data you can use to explore using This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is 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. created by, and maintained by Microsoft at no cost to you.
</InfoTooltip> </InfoTooltip>
</div> </div>
@@ -640,7 +640,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 }, label: { padding: 0 },
}} }}
className="padding" className="padding"
ariaLabel="Enable sample db for Copilot" ariaLabel="Enable sample db for Query Advisor"
checked={copilotSampleDBEnabled} checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange} onChange={handleSampleDatabaseChange}
/> />

View File

@@ -261,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
<TextField <TextField
multiline multiline
rows={5} rows={5}
ariaLabel={entityAttributeProperty}
value={entityAttributeValue} value={entityAttributeValue}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value"); entityChange(newInput, selectedRow, "value");

View File

@@ -385,7 +385,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
hasSmallHeadline={true} hasSmallHeadline={true}
headline="Write a prompt" headline="Write a prompt"
> >
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "}
<Link <Link
onClick={() => { onClick={() => {
setShowSamplePrompts(true); setShowSamplePrompts(true);

View File

@@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const toggleCopilotButton = { const toggleCopilotButton = {
iconSrc: QueryCommandIcon, iconSrc: QueryCommandIcon,
iconAlt: "Copilot", iconAlt: "Query Advisor",
onCommandClick: () => { onCommandClick: () => {
toggleCopilot(true); toggleCopilot(true);
}, },
commandButtonLabel: "Copilot", commandButtonLabel: "Query Advisor",
ariaLabel: "Copilot", ariaLabel: "Query Advisor",
hasPopup: false, hasPopup: false,
disabled: copilotActive, disabled: copilotActive,
}; };

View File

@@ -151,9 +151,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
{useQueryCopilot.getState().copilotEnabled && ( {useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton <SplashScreenButton
imgSrc={CopilotIcon} imgSrc={CopilotIcon}
title={"Query faster with Copilot"} title={"Query faster with Query Advisor"}
description={ description={
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!" "Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
} }
onClick={() => { onClick={() => {
const copilotVersion = userContext.features.copilotVersion; const copilotVersion = userContext.features.copilotVersion;

View File

@@ -1,6 +1,7 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as ko from "knockout"; import * as ko from "knockout";
@@ -462,7 +463,22 @@ export default class DocumentsTab extends TabsBase {
private initializeNewDocument = (): void => { private initializeNewDocument = (): void => {
this.selectedDocumentId(null); this.selectedDocumentId(null);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); const newDocument: any = {
id: "replace_with_new_document_id",
};
this.partitionKeyProperties.forEach((partitionKeyProperty) => {
let target = newDocument;
const keySegments = partitionKeyProperty.split(".");
const finalSegment = keySegments.pop();
// Initialize nested objects as needed
keySegments.forEach((segment) => {
target = target[segment] = target[segment] || {};
});
target[finalSegment] = "replace_with_new_partition_key_value";
});
const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4);
this.initialDocumentContent(defaultDocument); this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
@@ -893,6 +909,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: NewDocumentIcon, iconSrc: NewDocumentIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_ITEM,
onCommandClick: this.onNewDocumentClick, onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -907,6 +924,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveNewDocumentClick, onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -921,6 +939,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onRevertNewDocumentClick, onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -936,6 +955,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveExistingDocumentClick, onCommandClick: this.onSaveExistingDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -950,6 +970,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onRevertExisitingDocumentClick, onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -965,6 +986,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: DeleteDocumentIcon, iconSrc: DeleteDocumentIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.DELETE_ITEM,
onCommandClick: this.onDeleteExisitingDocumentClick, onCommandClick: this.onDeleteExisitingDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -1,3 +1,4 @@
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
import React, { Component } from "react"; import React, { Component } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext"; import { configContext } from "../../../ConfigContext";
@@ -9,7 +10,6 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
import { getMongoShellOrigin } from "./getMongoShellOrigin";
import { getMongoShellUrl } from "./getMongoShellUrl"; import { getMongoShellUrl } from "./getMongoShellUrl";
//eslint-disable-next-line //eslint-disable-next-line
@@ -50,13 +50,15 @@ export default class MongoShellTabComponent extends Component<
IMongoShellTabComponentStates IMongoShellTabComponentStates
> { > {
private _logTraces: Map<string, number>; private _logTraces: Map<string, number>;
private _useMongoProxyEndpoint: boolean;
constructor(props: IMongoShellTabComponentProps) { constructor(props: IMongoShellTabComponentProps) {
super(props); super(props);
this._logTraces = new Map(); this._logTraces = new Map();
this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
this.state = { this.state = {
url: getMongoShellUrl(), url: getMongoShellUrl(this._useMongoProxyEndpoint),
}; };
props.onMongoShellTabAccessor({ props.onMongoShellTabAccessor({
@@ -119,9 +121,10 @@ export default class MongoShellTabComponent extends Component<
) + Constants.MongoDBAccounts.defaultPort.toString(); ) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.props.collection.databaseId; const databaseId = this.props.collection.databaseId;
const collectionId = this.props.collection.id(); const collectionId = this.props.collection.id();
const apiEndpoint = configContext.BACKEND_ENDPOINT; const apiEndpoint = this._useMongoProxyEndpoint
? configContext.MONGO_PROXY_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const encryptedAuthToken: string = userContext.accessToken; const encryptedAuthToken: string = userContext.accessToken;
const targetOrigin = getMongoShellOrigin();
shellIframe.contentWindow.postMessage( shellIframe.contentWindow.postMessage(
{ {
@@ -137,7 +140,7 @@ export default class MongoShellTabComponent extends Component<
apiEndpoint: apiEndpoint, apiEndpoint: apiEndpoint,
}, },
}, },
targetOrigin, window.origin,
); );
} }

View File

@@ -1,86 +0,0 @@
import { extractFeatures } from "Platform/Hosted/extractFeatures";
import { configContext } from "../../../ConfigContext";
import { updateUserContext } from "../../../UserContext";
import { getMongoShellOrigin } from "./getMongoShellOrigin";
describe("getMongoShellOrigin", () => {
(window as { origin: string }).origin = "window_origin";
beforeEach(() => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV1": "false",
"feature.enableLegacyMongoShellV2": "false",
"feature.enableLegacyMongoShellV1Debug": "false",
"feature.enableLegacyMongoShellV2Debug": "false",
"feature.loadLegacyMongoShellFromBE": "false",
}),
),
});
});
it("should return by default", () => {
expect(getMongoShellOrigin()).toBe(window.origin);
});
it("should return window.origin when enableLegacyMongoShellV1", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV1": "true",
}),
),
});
expect(getMongoShellOrigin()).toBe(window.origin);
});
it("should return window.origin when enableLegacyMongoShellV2===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV2": "true",
}),
),
});
expect(getMongoShellOrigin()).toBe(window.origin);
});
it("should return window.origin when enableLegacyMongoShellV1Debug===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV1Debug": "true",
}),
),
});
expect(getMongoShellOrigin()).toBe(window.origin);
});
it("should return window.origin when enableLegacyMongoShellV2Debug===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV2Debug": "true",
}),
),
});
expect(getMongoShellOrigin()).toBe(window.origin);
});
it("should return BACKEND_ENDPOINT when loadLegacyMongoShellFromBE===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.loadLegacyMongoShellFromBE": "true",
}),
),
});
expect(getMongoShellOrigin()).toBe(configContext.BACKEND_ENDPOINT);
});
});

View File

@@ -1,10 +0,0 @@
import { configContext } from "../../../ConfigContext";
import { userContext } from "../../../UserContext";
export function getMongoShellOrigin(): string {
if (userContext.features.loadLegacyMongoShellFromBE === true) {
return configContext.BACKEND_ENDPOINT;
}
return window.origin;
}

View File

@@ -1,7 +1,6 @@
import { extractFeatures } from "Platform/Hosted/extractFeatures"; import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
import { updateUserContext, userContext } from "../../../UserContext"; import { updateUserContext, userContext } from "../../../UserContext";
import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl"; import { getMongoShellUrl } from "./getMongoShellUrl";
const mongoBackendEndpoint = "https://localhost:1234"; const mongoBackendEndpoint = "https://localhost:1234";
@@ -32,175 +31,18 @@ describe("getMongoShellUrl", () => {
cassandraEndpoint: "fakeCassandraEndpoint", cassandraEndpoint: "fakeCassandraEndpoint",
}, },
}, },
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV1": "false",
"feature.enableLegacyMongoShellV2": "false",
"feature.enableLegacyMongoShellV1Debug": "false",
"feature.enableLegacyMongoShellV2Debug": "false",
"feature.loadLegacyMongoShellFromBE": "false",
}),
),
portalEnv: "prod", portalEnv: "prod",
}); });
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`; queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
}); });
it("should return /mongoshell/indexv2.html by default", () => { it("should return /indexv2.html by default", () => {
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
}); });
it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => { it("should return /index.html when useMongoProxyEndpoint is true", () => {
updateUserContext({ const useMongoProxyEndpoint: boolean = true;
portalEnv: "localhost", expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
});
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
});
it("should return /mongoshell/index.html when enableLegacyMongoShellV1===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV1": "true",
}),
),
});
expect(getMongoShellUrl()).toBe(`/mongoshell/index.html?${queryString}`);
});
it("should return /mongoshell/index.html when enableLegacyMongoShellV2===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV2": "true",
}),
),
});
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
});
it("should return /mongoshell/index.html when enableLegacyMongoShellV1Debug===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV1Debug": "true",
}),
),
});
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/index.html?${queryString}`);
});
it("should return /mongoshell/index.html when enableLegacyMongoShellV2Debug===true", () => {
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV2Debug": "true",
}),
),
});
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/indexv2.html?${queryString}`);
});
describe("loadLegacyMongoShellFromBE===true", () => {
beforeEach(() => {
resetConfigContext();
updateConfigContext({
BACKEND_ENDPOINT: mongoBackendEndpoint,
platform: Platform.Hosted,
});
updateUserContext({
features: extractFeatures(
new URLSearchParams({
"feature.loadLegacyMongoShellFromBE": "true",
}),
),
});
});
it("should return /mongoshell/index.html", () => {
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
});
it("configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
updateConfigContext({
platform: Platform.Portal,
});
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
});
it("configContext.BACKEND_ENDPOINT !== '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
resetConfigContext();
updateConfigContext({
platform: Platform.Portal,
BACKEND_ENDPOINT: mongoBackendEndpoint,
});
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
});
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform === Platform.Hosted, should return /mongoshell/indexv2.html", () => {
resetConfigContext();
updateConfigContext({
platform: Platform.Hosted,
});
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
});
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
resetConfigContext();
updateConfigContext({
platform: Platform.Portal,
});
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
});
});
});
describe("getExtensionEndpoint", () => {
it("when platform === Platform.Hosted, backendEndpoint is undefined", () => {
expect(getExtensionEndpoint(Platform.Hosted, undefined)).toBe("");
});
it("when platform === Platform.Hosted, backendEndpoint === ''", () => {
expect(getExtensionEndpoint(Platform.Hosted, "")).toBe("");
});
it("when platform === Platform.Hosted, backendEndpoint === null", () => {
expect(getExtensionEndpoint(Platform.Hosted, null)).toBe("");
});
it("when platform === Platform.Hosted, backendEndpoint != ''", () => {
expect(getExtensionEndpoint(Platform.Hosted, "foo")).toBe("foo");
});
it("when platform === Platform.Portal, backendEndpoint is udefined", () => {
expect(getExtensionEndpoint(Platform.Portal, undefined)).toBe("");
});
it("when platform === Platform.Portal, backendEndpoint === ''", () => {
expect(getExtensionEndpoint(Platform.Portal, "")).toBe("");
});
it("when platform === Platform.Portal, backendEndpoint === null", () => {
expect(getExtensionEndpoint(Platform.Portal, null)).toBe("");
});
it("when platform !== Platform.Portal, backendEndpoint != ''", () => {
expect(getExtensionEndpoint(Platform.Portal, "foo")).toBe("foo");
}); });
}); });

View File

@@ -1,45 +1,11 @@
import { configContext, Platform } from "../../../ConfigContext";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
export function getMongoShellUrl(): string { export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
const { databaseAccount: account } = userContext; const { databaseAccount: account } = userContext;
const resourceId = account?.id; const resourceId = account?.id;
const accountName = account?.name; const accountName = account?.name;
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
if (userContext.features.enableLegacyMongoShellV1 === true) { return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
return `/mongoshell/index.html?${queryString}`;
}
if (userContext.features.enableLegacyMongoShellV1Debug === true) {
return `/mongoshell/debug/index.html?${queryString}`;
}
if (userContext.features.enableLegacyMongoShellV2 === true) {
return `/mongoshell/indexv2.html?${queryString}`;
}
if (userContext.features.enableLegacyMongoShellV2Debug === true) {
return `/mongoshell/debug/indexv2.html?${queryString}`;
}
if (userContext.portalEnv === "localhost") {
return `/mongoshell/indexv2.html?${queryString}`;
}
if (userContext.features.loadLegacyMongoShellFromBE === true) {
const extensionEndpoint: string = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
return `${extensionEndpoint}/content/mongoshell/debug/index.html?${queryString}`;
}
return `/mongoshell/indexv2.html?${queryString}`;
}
export function getExtensionEndpoint(platform: string, backendEndpoint: string): string {
const runtimeEndpoint = platform === Platform.Hosted ? backendEndpoint : "";
const extensionEndpoint: string = backendEndpoint || runtimeEndpoint || "";
return extensionEndpoint;
} }

View File

@@ -381,9 +381,13 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
<img className="paneErrorIcon" src={InfoColor} alt="Error" /> <img className="paneErrorIcon" src={InfoColor} alt="Error" />
</span> </span>
<span className="warningErrorDetailsLinkContainer"> <span className="warningErrorDetailsLinkContainer">
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported. We detected you may be using a subquery. To learn more about subqueries effectively,{" "}
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery"> <a
Please see Cosmos sub query documentation for further information href="https://learn.microsoft.com/azure/cosmos-db/nosql/query/subquery"
target="_blank"
rel="noreferrer"
>
visit the documentation
</a> </a>
</span> </span>
</div> </div>

View File

@@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
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";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -21,6 +22,7 @@ import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format"; import { format } from "react-string-format";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg"; import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.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";
@@ -224,6 +226,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
}; };
public onDownloadQueryClick = (): void => {
const text = this.getCurrentEditorQuery();
const queryFile = new File([text], `SavedQuery.txt`, { type: "text/plain" });
// It appears the most consistent to download a file from a blob is to create an anchor element and simulate clicking it
const blobUrl = URL.createObjectURL(queryFile);
const anchor = document.createElement("a");
anchor.href = blobUrl;
anchor.download = queryFile.name;
document.body.appendChild(anchor); // Must put the anchor in the document.
anchor.click();
document.body.removeChild(anchor); // Clean up the anchor.
};
public onSaveQueryClick = (): void => { public onSaveQueryClick = (): void => {
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />); useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
}; };
@@ -393,6 +409,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({ buttons.push({
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.EXECUTE_ITEM,
onCommandClick: this.props.isSampleCopilotActive onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore) ? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick, : this.onExecuteQueryClick,
@@ -403,14 +420,28 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
if (this.saveQueryButton.visible && configContext.platform !== Platform.Fabric) { if (this.saveQueryButton.visible) {
const label = "Save Query"; if (configContext.platform !== Platform.Fabric) {
const label = "Save Query";
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveQueryButton.enabled,
});
}
buttons.push({ buttons.push({
iconSrc: SaveQueryIcon, iconSrc: DownloadQueryIcon,
iconAlt: label, iconAlt: "Download Query",
onCommandClick: this.onSaveQueryClick, keyboardAction: KeyboardAction.DOWNLOAD_ITEM,
commandButtonLabel: label, onCommandClick: this.onDownloadQueryClick,
ariaLabel: label, commandButtonLabel: "Download Query",
ariaLabel: "Download Query",
hasPopup: false, hasPopup: false,
disabled: !this.saveQueryButton.enabled, disabled: !this.saveQueryButton.enabled,
}); });
@@ -437,7 +468,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
hasPopup: false, hasPopup: false,
}; };
const launchCopilotButton = { const launchCopilotButton: CommandButtonComponentProps = {
iconSrc: LaunchCopilot, iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel, iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat, onCommandClick: this.launchQueryCopilotChat,
@@ -450,14 +481,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
if (this.props.copilotEnabled) { if (this.props.copilotEnabled) {
const toggleCopilotButton = { const toggleCopilotButton: CommandButtonComponentProps = {
iconSrc: QueryCommandIcon, iconSrc: QueryCommandIcon,
iconAlt: "Copilot", iconAlt: "Query Advisor",
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
onCommandClick: () => { onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive); this._toggleCopilot(!this.state.copilotActive);
}, },
commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
hasPopup: false, hasPopup: false,
}; };
buttons.push(toggleCopilotButton); buttons.push(toggleCopilotButton);
@@ -468,6 +500,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({ buttons.push({
iconSrc: CancelQueryIcon, iconSrc: CancelQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: () => this.queryAbortController.abort(), onCommandClick: () => this.queryAbortController.abort(),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -520,6 +553,8 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
} }
this.saveQueryButton.enabled = newContent.length > 0;
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }

View File

@@ -1,5 +1,6 @@
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 { KeyboardAction } from "KeyboardShortcuts";
import React from "react"; import React from "react";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import DiscardIcon from "../../../../images/discard.svg"; import DiscardIcon from "../../../../images/discard.svg";
@@ -321,6 +322,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({ buttons.push({
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick, onCommandClick: this.onSaveClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -334,6 +336,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({ buttons.push({
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick, onCommandClick: this.onUpdateClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -347,6 +350,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onDiscard, onCommandClick: this.onDiscard,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -360,6 +364,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({ buttons.push({
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.EXECUTE_ITEM,
onCommandClick: () => { onCommandClick: () => {
this.collection.container.openExecuteSprocParamsPanel(this.node); this.collection.container.openExecuteSprocParamsPanel(this.node);
}, },

View File

@@ -6,6 +6,7 @@ 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/CommandBarComponentAdapter";
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";
@@ -13,6 +14,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility"; 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";
@@ -41,6 +43,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
showMongoAndCassandraProxiesNetworkSettingsWarningState, showMongoAndCassandraProxiesNetworkSettingsWarningState,
setShowMongoAndCassandraProxiesNetworkSettingsWarningState, setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning()); ] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
useEffect(() => {
setKeyboardHandlers({
[KeyboardAction.SELECT_LEFT_TAB]: () => useTabs.getState().selectLeftTab(),
[KeyboardAction.SELECT_RIGHT_TAB]: () => useTabs.getState().selectRightTab(),
[KeyboardAction.CLOSE_TAB]: () => useTabs.getState().closeActiveTab(),
});
}, [setKeyboardHandlers]);
return ( return (
<div className="tabsManagerContainer"> <div className="tabsManagerContainer">
{networkSettingsWarning && ( {networkSettingsWarning && (
@@ -297,6 +309,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
}; };
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
// React tabs have no context buttons.
useCommandBar.getState().setContextButtons([]);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
switch (activeReactTab) { switch (activeReactTab) {
case ReactTabKind.Connect: case ReactTabKind.Connect:
@@ -325,7 +340,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if ( if (
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) || ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
(userContext.apiType === "Cassandra" && (userContext.apiType === "Cassandra" &&
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) && configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
ipRules?.length ipRules?.length

View File

@@ -1,5 +1,6 @@
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 { 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";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
@@ -218,6 +219,18 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
return !!value; return !!value;
} }
componentDidUpdate(_prevProps: TriggerTab, prevState: ITriggerTabContentState): void {
const { triggerBody, triggerId, triggerType, triggerOperation } = this.state;
if (
triggerId !== prevState.triggerId ||
triggerBody !== prevState.triggerBody ||
triggerType !== prevState.triggerType ||
triggerOperation !== prevState.triggerOperation
) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
}
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
const label = "Save"; const label = "Save";
@@ -227,6 +240,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this, ...this,
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick, onCommandClick: this.onSaveClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -241,6 +255,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this, ...this,
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick, onCommandClick: this.onUpdateClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -256,6 +271,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this, ...this,
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onDiscard, onCommandClick: this.onDiscard,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -287,7 +303,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
}; };
render(): JSX.Element { render(): JSX.Element {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state; const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state;
return ( return (
<div className="tab-pane flexContainer trigger-form" role="tabpanel"> <div className="tab-pane flexContainer trigger-form" role="tabpanel">

View File

@@ -1,12 +1,13 @@
import { UserDefinedFunctionDefinition } from "@azure/cosmos"; import { UserDefinedFunctionDefinition } from "@azure/cosmos";
import { Label, TextField } from "@fluentui/react"; import { Label, TextField } from "@fluentui/react";
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";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction"; import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction"; import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -80,6 +81,7 @@ export default class UserDefinedFunctionTabContent extends Component<
setState: this.setState, setState: this.setState,
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick, onCommandClick: this.onSaveClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -94,6 +96,7 @@ export default class UserDefinedFunctionTabContent extends Component<
...this, ...this,
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick, onCommandClick: this.onUpdateClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -109,6 +112,7 @@ export default class UserDefinedFunctionTabContent extends Component<
...this, ...this,
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onDiscard, onCommandClick: this.onDiscard,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

159
src/KeyboardShortcuts.tsx Normal file
View File

@@ -0,0 +1,159 @@
import * as React from "react";
import { PropsWithChildren, useEffect } from "react";
import { KeyBindingMap, tinykeys } from "tinykeys";
import create, { UseStore } from "zustand";
/**
* Represents a keyboard shortcut handler.
* Return `true` to prevent the default action of the keyboard shortcut.
* Any other return value will allow the default action to proceed.
*/
export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void;
export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHandler>>;
/**
* The groups of keyboard actions that can be managed by the application.
* Each group can be updated separately, but, when updated, must be completely replaced.
*/
export enum KeyboardActionGroup {
TABS = "TABS",
COMMAND_BAR = "COMMAND_BAR",
}
/**
* The possible actions that can be triggered by keyboard shortcuts.
*/
export enum KeyboardAction {
NEW_QUERY = "NEW_QUERY",
EXECUTE_ITEM = "EXECUTE_ITEM",
CANCEL_OR_DISCARD = "CANCEL_OR_DISCARD",
SAVE_ITEM = "SAVE_ITEM",
DOWNLOAD_ITEM = "DOWNLOAD_ITEM",
OPEN_QUERY = "OPEN_QUERY",
OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK",
NEW_SPROC = "NEW_SPROC",
NEW_UDF = "NEW_UDF",
NEW_TRIGGER = "NEW_TRIGGER",
NEW_DATABASE = "NEW_DATABASE",
NEW_COLLECTION = "NEW_CONTAINER",
NEW_ITEM = "NEW_ITEM",
DELETE_ITEM = "DELETE_ITEM",
TOGGLE_COPILOT = "TOGGLE_COPILOT",
SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
CLOSE_TAB = "CLOSE_TAB",
}
/**
* The keyboard shortcuts for the application.
* This record maps each action to the keyboard shortcuts that trigger the action.
* Even if an action is specified here, it will not be triggered unless a handler is set for it.
*/
const bindings: Record<KeyboardAction, string[]> = {
// NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS.
// See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts.
[KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"],
[KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"],
[KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"],
[KeyboardAction.SAVE_ITEM]: ["$mod+S"],
[KeyboardAction.DOWNLOAD_ITEM]: ["$mod+Shift+S"],
[KeyboardAction.OPEN_QUERY]: ["$mod+O"],
[KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"],
[KeyboardAction.NEW_SPROC]: ["Alt+N P"],
[KeyboardAction.NEW_UDF]: ["Alt+N F"],
[KeyboardAction.NEW_TRIGGER]: ["Alt+N T"],
[KeyboardAction.NEW_DATABASE]: ["Alt+N D"],
[KeyboardAction.NEW_COLLECTION]: ["Alt+N C"],
[KeyboardAction.NEW_ITEM]: ["Alt+N I"],
[KeyboardAction.DELETE_ITEM]: ["Alt+D"],
[KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"],
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
};
interface KeyboardShortcutState {
/**
* A set of all the keyboard shortcuts handlers.
*/
allHandlers: KeyboardHandlerMap;
/**
* A set of all the groups of keyboard shortcuts handlers.
*/
groups: Partial<Record<KeyboardActionGroup, KeyboardHandlerMap>>;
/**
* Sets the keyboard shortcut handlers for the given group.
*/
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
}
/**
* Defines the calling component as the manager of the keyboard actions for the given group.
* @param group The group of keyboard actions to manage.
* @returns A function that can be used to set the keyboard action handlers for the given group.
*/
export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
allHandlers: {},
groups: {},
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {
const state = get();
const groups = { ...state.groups, [group]: handlers };
// Combine all the handlers from all the groups in the correct order.
const allHandlers: KeyboardHandlerMap = {};
eachKey(groups).forEach((group) => {
const groupHandlers = groups[group];
if (groupHandlers) {
eachKey(groupHandlers).forEach((action) => {
// Check for duplicate handlers in development mode.
// We don't want to raise an error here in production, but having duplicate handlers is a mistake.
if (process.env.NODE_ENV === "development" && allHandlers[action]) {
throw new Error(`Duplicate handler for Keyboard Action "${action}".`);
}
allHandlers[action] = groupHandlers[action];
});
}
});
set({ groups, allHandlers });
},
}));
function createHandler(action: KeyboardAction): KeyboardActionHandler {
return (e) => {
const state = useKeyboardActionHandlers.getState();
const handler = state.allHandlers[action];
if (handler && handler(e)) {
e.preventDefault();
e.stopPropagation();
}
};
}
const allHandlers: KeyBindingMap = {};
eachKey(bindings).forEach((action) => {
const shortcuts = bindings[action];
shortcuts.forEach((shortcut) => {
allHandlers[shortcut] = createHandler(action);
});
});
export function KeyboardShortcutRoot({ children }: PropsWithChildren<unknown>) {
useEffect(() => {
// We bind to the body because Fluent UI components sometimes shift focus to the body, which is above the root React component.
tinykeys(document.body, allHandlers);
}, []);
return <>{children}</>;
}
/** A _typed_ version of `Object.keys` that preserves the original key type */
function eachKey<K extends string | number | symbol, V>(record: Partial<Record<K, V>>): K[] {
return Object.keys(record) as K[];
}

View File

@@ -21,6 +21,7 @@ import "../externals/jquery.typeahead.min.js";
// Image Dependencies // Image Dependencies
import { Platform } from "ConfigContext"; import { Platform } from "ConfigContext";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
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";
import "../images/favicon.ico"; import "../images/favicon.ico";
@@ -91,52 +92,54 @@ const App: React.FunctionComponent = () => {
} }
return ( return (
<div className="flexContainer" aria-hidden="false"> <KeyboardShortcutRoot>
<div id="divExplorer" className="flexContainer hideOverflows"> <div className="flexContainer" aria-hidden="false">
<div id="freeTierTeachingBubble"> </div> <div id="divExplorer" className="flexContainer hideOverflows">
{/* Main Command Bar - Start */} <div id="freeTierTeachingBubble"> </div>
<CommandBar container={explorer} /> {/* Main Command Bar - Start */}
{/* Collections Tree and Tabs - Begin */} <CommandBar container={explorer} />
<div className="resourceTreeAndTabs"> {/* Collections Tree and Tabs - Begin */}
{/* Collections Tree - Start */} <div className="resourceTreeAndTabs">
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( {/* Collections Tree - Start */}
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree"> {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
<div className="collectionsTreeWithSplitter"> <div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
{/* Collections Tree Expanded - Start */} <div className="collectionsTreeWithSplitter">
<ResourceTreeContainer {/* Collections Tree Expanded - Start */}
container={explorer} <ResourceTreeContainer
toggleLeftPaneExpanded={toggleLeftPaneExpanded} container={explorer}
isLeftPaneExpanded={isLeftPaneExpanded} toggleLeftPaneExpanded={toggleLeftPaneExpanded}
/> isLeftPaneExpanded={isLeftPaneExpanded}
{/* Collections Tree Expanded - End */} />
{/* Collections Tree Collapsed - Start */} {/* Collections Tree Expanded - End */}
<CollapsedResourceTree {/* Collections Tree Collapsed - Start */}
toggleLeftPaneExpanded={toggleLeftPaneExpanded} <CollapsedResourceTree
isLeftPaneExpanded={isLeftPaneExpanded} toggleLeftPaneExpanded={toggleLeftPaneExpanded}
/> isLeftPaneExpanded={isLeftPaneExpanded}
{/* Collections Tree Collapsed - End */} />
{/* Collections Tree Collapsed - End */}
</div>
</div> </div>
</div> )}
)} <Tabs explorer={explorer} />
<Tabs explorer={explorer} /> </div>
</div> {/* Collections Tree and Tabs - End */}
{/* Collections Tree and Tabs - End */} <div
<div className="dataExplorerErrorConsoleContainer"
className="dataExplorerErrorConsoleContainer" role="contentinfo"
role="contentinfo" aria-label="Notification console"
aria-label="Notification console" id="explorerNotificationConsole"
id="explorerNotificationConsole" >
> <NotificationConsole />
<NotificationConsole /> </div>
</div> </div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div> </div>
<SidePanel /> </KeyboardShortcutRoot>
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
); );
}; };

View File

@@ -31,11 +31,6 @@ export type Features = {
readonly mongoProxyAPIs?: string; readonly mongoProxyAPIs?: string;
readonly enableThroughputCap: boolean; readonly enableThroughputCap: boolean;
readonly enableHierarchicalKeys: boolean; readonly enableHierarchicalKeys: boolean;
readonly enableLegacyMongoShellV1: boolean;
readonly enableLegacyMongoShellV1Debug: boolean;
readonly enableLegacyMongoShellV2: boolean;
readonly enableLegacyMongoShellV2Debug: boolean;
readonly loadLegacyMongoShellFromBE: boolean;
readonly enableCopilot: boolean; readonly enableCopilot: boolean;
readonly copilotVersion?: string; readonly copilotVersion?: string;
readonly disableCopilotPhoenixGateaway: boolean; readonly disableCopilotPhoenixGateaway: boolean;
@@ -106,11 +101,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
notebooksDownBanner: "true" === get("notebooksDownBanner"), notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"), enableThroughputCap: "true" === get("enablethroughputcap"),
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"), enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
enableLegacyMongoShellV1: "true" === get("enablelegacymongoshellv1"),
enableLegacyMongoShellV1Debug: "true" === get("enablelegacymongoshellv1debug"),
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
enableCopilot: "true" === get("enablecopilot", "true"), enableCopilot: "true" === get("enablecopilot", "true"),
copilotVersion: get("copilotversion") ?? "v2.0", copilotVersion: get("copilotversion") ?? "v2.0",
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"), disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),

View File

@@ -117,6 +117,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
updateConfigContext({ updateConfigContext({
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
CATALOG_API_KEY: inputs.catalogAPIKey,
}); });
updateUserContext({ updateUserContext({

View File

@@ -1,11 +1,15 @@
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { armRequestWithoutPolling } from "../../Utils/arm/request"; import { get } from "../../Utils/arm/generatedClients/cosmos/locations";
import { armRequestWithoutPolling, getOfferingIdsRequest } from "../../Utils/arm/request";
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import { RefreshResult } from "../SelfServeTypes"; import { RefreshResult } from "../SelfServeTypes";
import SqlX from "./SqlX"; import SqlX from "./SqlX";
import { import {
FetchPricesResponse, FetchPricesResponse,
GetOfferingIdsResponse,
OfferingIdMap,
OfferingIdRequest,
PriceMapAndCurrencyCode, PriceMapAndCurrencyCode,
RegionItem, RegionItem,
RegionsResponse, RegionsResponse,
@@ -166,11 +170,21 @@ export const getRegions = async (): Promise<Array<RegionItem>> => {
} }
}; };
export const getRegionShortName = async (regionDisplayName: string): Promise<string> => {
const locationsList = await get(userContext.subscriptionId, regionDisplayName);
if ("id" in locationsList) {
const locationId = locationsList.id;
return locationId.substring(locationId.lastIndexOf("/") + 1);
}
return undefined;
};
const getFetchPricesPathForRegion = (subscriptionId: string): string => { const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
}; };
export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => { export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<PriceMapAndCurrencyCode> => {
const telemetryData = { const telemetryData = {
feature: "Calculate approximate cost", feature: "Calculate approximate cost",
function: "getPriceMapAndCurrencyCode", function: "getPriceMapAndCurrencyCode",
@@ -181,39 +195,94 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Pr
try { try {
const priceMap = new Map<string, Map<string, number>>(); const priceMap = new Map<string, Map<string, number>>();
let currencyCode; let billingCurrency;
for (const regionItem of regions) { for (const region of map.keys()) {
const regionPriceMap = new Map<string, number>(); const regionPriceMap = new Map<string, number>();
const regionShortName = await getRegionShortName(region);
const requestBody: OfferingIdRequest = {
location: regionShortName,
ids: Array.from(map.get(region).keys()),
};
const response = await armRequestWithoutPolling<FetchPricesResponse>({ const response = await armRequestWithoutPolling<FetchPricesResponse>({
host: configContext.ARM_ENDPOINT, host: configContext.ARM_ENDPOINT,
path: getFetchPricesPathForRegion(userContext.subscriptionId), path: getFetchPricesPathForRegion(userContext.subscriptionId),
method: "POST", method: "POST",
apiVersion: "2020-01-01-preview", apiVersion: "2023-04-01-preview",
queryParams: { body: requestBody,
filter:
"armRegionNameeq '" +
regionItem.locationName.split(" ").join("").toLowerCase() +
"'andserviceFamilyeq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
},
}); });
for (const item of response.result.Items) { for (const item of response.result) {
if (currencyCode === undefined) { if (item.error) {
currencyCode = item.currencyCode; continue;
} else if (item.currencyCode !== currencyCode) { }
if (billingCurrency === undefined) {
billingCurrency = item.billingCurrency;
} else if (item.billingCurrency !== billingCurrency) {
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus."); throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
} }
regionPriceMap.set(item.skuName, item.retailPrice);
const offeringId = item.id;
const skuName = map.get(region).get(offeringId);
const unitPriceinBillingCurrency = item.prices.find((x) => x.type === "Consumption")
?.unitPriceinBillingCurrency;
regionPriceMap.set(skuName, unitPriceinBillingCurrency);
} }
priceMap.set(regionItem.locationName, regionPriceMap); priceMap.set(region, regionPriceMap);
} }
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp); selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: priceMap, currencyCode: currencyCode }; return { priceMap: priceMap, billingCurrency: billingCurrency };
} catch (err) { } catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name }; const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp); selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: undefined, currencyCode: undefined }; return { priceMap: undefined, billingCurrency: undefined };
}
};
const getOfferingIdPathForRegion = (): string => {
return `/skus?serviceFamily=Databases&service=Azure Cosmos DB`;
};
export const getOfferingIds = async (regions: Array<RegionItem>): Promise<OfferingIdMap> => {
const telemetryData = {
feature: "Get Offering Ids to calculate approximate cost",
function: "getOfferingIds",
description: "fetch offering ids API call",
selfServeClassName: SqlX.name,
};
const getOfferingIdsCodeTimestamp = selfServeTraceStart(telemetryData);
try {
const offeringIdMap = new Map<string, Map<string, string>>();
for (const regionItem of regions) {
const regionOfferingIdMap = new Map<string, string>();
const regionShortName = await getRegionShortName(regionItem.locationName);
const response = await getOfferingIdsRequest<GetOfferingIdsResponse>({
host: configContext.CATALOG_ENDPOINT,
path: getOfferingIdPathForRegion(),
method: "GET",
apiVersion: "2023-05-01-preview",
queryParams: {
filter: "armRegionNameeq '" + regionShortName + "'",
},
});
for (const item of response.result.items) {
if (item.offeringProperties?.length > 0) {
regionOfferingIdMap.set(item.offeringProperties[0].offeringId, item.skuName);
}
}
offeringIdMap.set(regionItem.locationName, regionOfferingIdMap);
}
selfServeTraceSuccess(telemetryData, getOfferingIdsCodeTimestamp);
return offeringIdMap;
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
return undefined;
} }
}; };

View File

@@ -24,6 +24,7 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
import { import {
deleteDedicatedGatewayResource, deleteDedicatedGatewayResource,
getCurrentProvisioningState, getCurrentProvisioningState,
getOfferingIds,
getPriceMapAndCurrencyCode, getPriceMapAndCurrencyCode,
getRegions, getRegions,
refreshDedicatedGatewayProvisioning, refreshDedicatedGatewayProvisioning,
@@ -370,9 +371,10 @@ export default class SqlX extends SelfServeBaseClass {
}); });
regions = await getRegions(); regions = await getRegions();
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions); const offeringIdMap = await getOfferingIds(regions);
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(offeringIdMap);
priceMap = priceMapAndCurrencyCode.priceMap; priceMap = priceMapAndCurrencyCode.priceMap;
currencyCode = priceMapAndCurrencyCode.currencyCode; currencyCode = priceMapAndCurrencyCode.billingCurrency;
const response = await getCurrentProvisioningState(); const response = await getCurrentProvisioningState();
if (response.status && response.status !== "Deleting") { if (response.status && response.status !== "Deleting") {

View File

@@ -30,23 +30,51 @@ export type UpdateDedicatedGatewayRequestProperties = {
serviceType: string; serviceType: string;
}; };
export type FetchPricesResponse = { export type FetchPricesResponse = Array<PriceItem>;
Items: Array<PriceItem>;
NextPageLink: string | undefined; export type PriceItem = {
Count: number; prices: Array<PriceType>;
id: string;
billingCurrency: string;
error: PriceError;
};
export type PriceType = {
type: string;
unitPriceinBillingCurrency: number;
};
export type PriceError = {
type: string;
description: string;
}; };
export type PriceMapAndCurrencyCode = { export type PriceMapAndCurrencyCode = {
priceMap: Map<string, Map<string, number>>; priceMap: Map<string, Map<string, number>>;
currencyCode: string; billingCurrency: string;
}; };
export type PriceItem = { export type GetOfferingIdsResponse = {
retailPrice: number; items: Array<OfferingIdItem>;
skuName: string; nextPageLink: string | undefined;
currencyCode: string;
}; };
export type OfferingIdItem = {
skuName: string;
offeringProperties: Array<OfferingProperties>;
};
export type OfferingProperties = {
offeringId: string;
};
export type OfferingIdRequest = {
ids: Array<string>;
location: string;
};
export type OfferingIdMap = Map<string, Map<string, string>>;
export type RegionsResponse = { export type RegionsResponse = {
properties: RegionsProperties; properties: RegionsProperties;
}; };

View File

@@ -82,7 +82,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
}; };
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [ export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
@@ -154,8 +154,16 @@ export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
export function useNewPortalBackendEndpoint(backendApi: string): boolean { export function useNewPortalBackendEndpoint(backendApi: string): boolean {
// This maps backend APIs to the environments supported by the new backend. // This maps backend APIs to the environments supported by the new backend.
const newBackendApiEnvironmentMap: { [key: string]: string[] } = { const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
[BackendApi.GenerateToken]: [PortalBackendEndpoints.Development], [BackendApi.GenerateToken]: [
[BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac], PortalBackendEndpoints.Development,
PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod,
],
[BackendApi.PortalSettings]: [
PortalBackendEndpoints.Development,
PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod,
],
}; };
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) { if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {

View File

@@ -160,3 +160,52 @@ async function getOperationStatus(operationStatusUrl: string) {
} }
throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`); throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`);
} }
export async function getOfferingIdsRequest<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
if (queryParams) {
queryParams.filter && url.searchParams.append("$filter", queryParams.filter);
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
}
if (!configContext.CATALOG_API_KEY) {
throw new Error("No catalog API key provided");
}
const response = await window.fetch(url.href, {
method,
headers: {
[HttpHeaders.xAPIKey]: configContext.CATALOG_API_KEY,
},
body: requestBody ? JSON.stringify(requestBody) : undefined,
});
if (!response.ok) {
let error: ARMError;
try {
const errorResponse = (await response.json()) as ParsedErrorResponse;
if ("error" in errorResponse) {
error = new ARMError(errorResponse.error.message);
error.code = errorResponse.error.code;
} else {
error = new ARMError(errorResponse.message);
error.code = errorResponse.code;
}
} catch (error) {
throw new Error(await response.text());
}
throw error;
}
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
const responseBody = (await response.json()) as T;
return { result: responseBody, operationStatusUrl: operationStatusUrl };
}

View File

@@ -1,3 +1,4 @@
import { clamp } from "@fluentui/react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels";
@@ -29,6 +30,11 @@ export interface TabsState {
setQueryCopilotTabInitialInput: (input: string) => void; setQueryCopilotTabInitialInput: (input: string) => void;
setIsTabExecuting: (state: boolean) => void; setIsTabExecuting: (state: boolean) => void;
setIsQueryErrorThrown: (state: boolean) => void; setIsQueryErrorThrown: (state: boolean) => void;
getCurrentTabIndex: () => number;
selectTabByIndex: (index: number) => void;
selectLeftTab: () => void;
selectRightTab: () => void;
closeActiveTab: () => void;
} }
export enum ReactTabKind { export enum ReactTabKind {
@@ -175,4 +181,44 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
setIsQueryErrorThrown: (state: boolean) => { setIsQueryErrorThrown: (state: boolean) => {
set({ isQueryErrorThrown: state }); set({ isQueryErrorThrown: state });
}, },
getCurrentTabIndex: () => {
const state = get();
if (state.activeReactTab !== undefined) {
return state.openedReactTabs.indexOf(state.activeReactTab);
} else if (state.activeTab !== undefined) {
const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab);
if (nonReactTabIndex !== -1) {
return state.openedReactTabs.length + nonReactTabIndex;
}
}
return -1;
},
selectTabByIndex: (index: number) => {
const state = get();
const totalTabCount = state.openedReactTabs.length + state.openedTabs.length;
const clampedIndex = clamp(index, totalTabCount - 1, 0);
if (clampedIndex < state.openedReactTabs.length) {
set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] });
} else {
set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined });
}
},
selectLeftTab: () => {
const state = get();
state.selectTabByIndex(state.getCurrentTabIndex() - 1);
},
selectRightTab: () => {
const state = get();
state.selectTabByIndex(state.getCurrentTabIndex() + 1);
},
closeActiveTab: () => {
const state = get();
if (state.activeReactTab !== undefined) {
state.closeReactTab(state.activeReactTab);
} else if (state.activeTab !== undefined) {
state.closeTab(state.activeTab);
}
},
})); }));

View File

@@ -30,7 +30,7 @@
<clear /> <clear />
<add name="X-Xss-Protection" value="1; mode=block" /> <add name="X-Xss-Protection" value="1; mode=block" />
<add name="X-Content-Type-Options" value="nosniff" /> <add name="X-Content-Type-Options" value="nosniff" />
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net" /> <add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net cosmos-explorer-preview.azurewebsites.net" />
</customHeaders> </customHeaders>
<redirectHeaders> <redirectHeaders>
<clear /> <clear />