Compare commits

..

2 Commits

Author SHA1 Message Date
Ashley Stanton-Nurse
2089d8ca9e a system of contributors? 2024-04-15 15:45:22 -07:00
Ashley Stanton-Nurse
e441b75325 initial mousetrap keyboard shortcuts 2024-04-15 15:02:10 -07:00
43 changed files with 1337 additions and 1824 deletions

View File

@@ -76,10 +76,6 @@ 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
@@ -134,6 +130,7 @@ 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

1901
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.5.2", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.1.1", "@azure/ms-rest-nodeauth": "3.0.7",
"@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,7 +46,6 @@
"@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",
@@ -55,7 +54,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": "7.8.5", "d3": "6.1.1",
"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",
@@ -70,16 +69,15 @@
"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",
"mousetrap": "1.6.5",
"ms": "2.1.3", "ms": "2.1.3",
"p-retry": "4.6.2", "p-retry": "4.6.2",
"patch-package": "8.0.0", "patch-package": "8.0.0",
@@ -101,12 +99,10 @@
"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",
"tinykeys": "2.1.0", "underscore": "1.9.1",
"underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
@@ -128,6 +124,7 @@
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/jquery": "3.5.29", "@types/jquery": "3.5.29",
"@types/mousetrap": "1.6.15",
"@types/node": "12.11.1", "@types/node": "12.11.1",
"@types/post-robot": "10.0.1", "@types/post-robot": "10.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
@@ -175,25 +172,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.7", "node-fetch": "2.6.1",
"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": "12.0.1", "react-dev-utils": "11.0.4",
"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.22.15", "typedoc": "0.21.5",
"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.2" "webpack-dev-server": "4.15.1"
}, },
"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 Local: string = "https://localhost:7238"; public static readonly Development: 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,7 +249,6 @@ 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,28 +672,6 @@ 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> {
@@ -710,3 +688,24 @@ 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={`Edit ${entityProperty} entity`} alt="editEntity"
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 ${entityProperty} entity`} alt="delete entity"
id="deleteEntity" id="deleteEntity"
onClick={onDeleteEntity} onClick={onDeleteEntity}
tabIndex={0} tabIndex={0}

View File

@@ -42,9 +42,6 @@ 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;
@@ -86,7 +83,6 @@ 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/",
@@ -96,9 +92,6 @@ 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
@@ -115,7 +108,6 @@ 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,7 +420,6 @@ 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,7 +1,8 @@
/** /**
* React component for Command button component. * React component for Command button component.
*/ */
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardShortcutAction } from "KeyboardShortcuts";
import { ExtendedKeyboardEvent } from "mousetrap";
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";
@@ -31,7 +32,7 @@ export interface CommandButtonComponentProps {
/** /**
* Click handler for command button click * Click handler for command button click
*/ */
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void; onCommandClick: (e: React.SyntheticEvent | ExtendedKeyboardEvent) => void;
/** /**
* Label for the button * Label for the button
@@ -114,11 +115,7 @@ export interface CommandButtonComponentProps {
*/ */
ariaLabel: string; ariaLabel: string;
/** keyboardShortcut?: KeyboardShortcutAction;
* 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" role="button" /> <img src={DeleteIcon} alt="Delete" />
</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" role="button" /> <img src={EditIcon} alt="Edit" />
</AccessibleElement> </AccessibleElement>
)} )}

View File

@@ -184,18 +184,12 @@ 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 <img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" />
aria-label="hidden"
className="refreshcol rightPaneTrashIconImg"
src={DeleteIcon}
alt="Remove property"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,7 +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 { KeyboardShortcutAction, KeyboardShortcutContributor, KeyboardShortcutHandler, useKeyboardShortcutContributor } 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";
@@ -41,7 +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); const setKeyboardShortcutHandlers = useKeyboardShortcutContributor(KeyboardShortcutContributor.COMMAND_BAR);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons = const buttons =
@@ -108,8 +108,16 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
}; };
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); const handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>> = {};
setKeyboardHandlers(keyboardHandlers); allButtons.forEach((button) => {
if(button.keyboardShortcut) {
handlers[button.keyboardShortcut] = (e) => {
button.onCommandClick(e);
return false;
}
}
});
setKeyboardShortcutHandlers(handlers);
return ( return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}> <div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>

View File

@@ -1,4 +1,4 @@
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardShortcutAction } 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";
@@ -58,7 +58,6 @@ 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);
@@ -96,7 +95,6 @@ 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);
@@ -280,7 +278,6 @@ 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) {
@@ -301,7 +298,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY, keyboardShortcut: KeyboardShortcutAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@@ -317,7 +314,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY, keyboardShortcut: KeyboardShortcutAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
@@ -343,7 +340,6 @@ 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);
@@ -363,7 +359,6 @@ 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);
@@ -383,7 +378,6 @@ 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);
@@ -406,7 +400,6 @@ 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,
@@ -421,7 +414,6 @@ 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,7 +7,6 @@ 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";
@@ -234,28 +233,3 @@ 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

@@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
autoComplete="off" autoComplete="off"
styles={getTextFieldStyles()} styles={getTextFieldStyles()}
pattern="[^/?#\\-]*[^/?#- \\]" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'" title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new keyspace id" placeholder="Type a new keyspace id"
size={40} size={40}
value={newKeyspaceId} value={newKeyspaceId}
@@ -292,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
ariaLabel="addCollection-table Id Create table" ariaLabel="addCollection-table Id Create table"
autoComplete="off" autoComplete="off"
pattern="[^/?#\\-]*[^/?#- \\]" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'" title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Enter table Id" placeholder="Enter table Id"
size={20} size={20}
value={tableId} value={tableId}

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 Query Advisor. This will appear as another database in the Data Explorer UI, and is NoSQL queries and Copilot. 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 Query Advisor" ariaLabel="Enable sample db for Copilot"
checked={copilotSampleDBEnabled} checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange} onChange={handleSampleDatabaseChange}
/> />

View File

@@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsExecuting(true); setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities); const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try { try {
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
await tableEntityListViewModel.addEntityToCache(newEntity); await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled(); tableEntityListViewModel.redrawTableThrottled();
@@ -261,7 +261,6 @@ 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 Query Advisor will generate the query for you. You can also choose from our{" "} Write a prompt here and Copilot 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: "Query Advisor", iconAlt: "Copilot",
onCommandClick: () => { onCommandClick: () => {
toggleCopilot(true); toggleCopilot(true);
}, },
commandButtonLabel: "Query Advisor", commandButtonLabel: "Copilot",
ariaLabel: "Query Advisor", ariaLabel: "Copilot",
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 Query Advisor"} title={"Query faster with Copilot"}
description={ description={
"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!" "Copilot 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,7 +1,6 @@
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";
@@ -463,22 +462,7 @@ export default class DocumentsTab extends TabsBase {
private initializeNewDocument = (): void => { private initializeNewDocument = (): void => {
this.selectedDocumentId(null); this.selectedDocumentId(null);
const newDocument: any = { const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
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);
@@ -909,7 +893,6 @@ 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,
@@ -924,7 +907,6 @@ 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,
@@ -939,7 +921,6 @@ 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,
@@ -955,7 +936,6 @@ 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,
@@ -970,7 +950,6 @@ 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,
@@ -986,7 +965,6 @@ 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,4 +1,3 @@
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";
@@ -10,6 +9,7 @@ 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,15 +50,13 @@ 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(this._useMongoProxyEndpoint), url: getMongoShellUrl(),
}; };
props.onMongoShellTabAccessor({ props.onMongoShellTabAccessor({
@@ -121,10 +119,9 @@ 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 = this._useMongoProxyEndpoint const apiEndpoint = configContext.BACKEND_ENDPOINT;
? 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(
{ {
@@ -140,7 +137,7 @@ export default class MongoShellTabComponent extends Component<
apiEndpoint: apiEndpoint, apiEndpoint: apiEndpoint,
}, },
}, },
window.origin, targetOrigin,
); );
} }

View File

@@ -0,0 +1,86 @@
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

@@ -0,0 +1,10 @@
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,6 +1,7 @@
import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext"; import { extractFeatures } from "Platform/Hosted/extractFeatures";
import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
import { updateUserContext, userContext } from "../../../UserContext"; import { updateUserContext, userContext } from "../../../UserContext";
import { getMongoShellUrl } from "./getMongoShellUrl"; import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl";
const mongoBackendEndpoint = "https://localhost:1234"; const mongoBackendEndpoint = "https://localhost:1234";
@@ -31,18 +32,175 @@ 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 /indexv2.html by default", () => { it("should return /mongoshell/indexv2.html by default", () => {
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`); expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
}); });
it("should return /index.html when useMongoProxyEndpoint is true", () => { it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => {
const useMongoProxyEndpoint: boolean = true; updateUserContext({
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`); portalEnv: "localhost",
});
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,11 +1,45 @@
import { configContext, Platform } from "../../../ConfigContext";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string { export function getMongoShellUrl(): 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}`;
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`; if (userContext.features.enableLegacyMongoShellV1 === true) {
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,13 +381,9 @@ 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 detected you may be using a subquery. To learn more about subqueries effectively,{" "} We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
<a <a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery">
href="https://learn.microsoft.com/azure/cosmos-db/nosql/query/subquery" Please see Cosmos sub query documentation for further information
target="_blank"
rel="noreferrer"
>
visit the documentation
</a> </a>
</span> </span>
</div> </div>

View File

@@ -10,7 +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 { KeyboardShortcutAction } 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";
@@ -22,7 +22,6 @@ 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";
@@ -226,20 +225,6 @@ 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} />);
}; };
@@ -409,7 +394,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, keyboardShortcut: KeyboardShortcutAction.EXECUTE_ITEM,
onCommandClick: this.props.isSampleCopilotActive onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore) ? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick, : this.onExecuteQueryClick,
@@ -420,28 +405,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
if (this.saveQueryButton.visible) { if (this.saveQueryButton.visible && configContext.platform !== Platform.Fabric) {
if (configContext.platform !== Platform.Fabric) { const label = "Save Query";
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: DownloadQueryIcon, iconSrc: SaveQueryIcon,
iconAlt: "Download Query", iconAlt: label,
keyboardAction: KeyboardAction.DOWNLOAD_ITEM, onCommandClick: this.onSaveQueryClick,
onCommandClick: this.onDownloadQueryClick, commandButtonLabel: label,
commandButtonLabel: "Download Query", ariaLabel: label,
ariaLabel: "Download Query",
hasPopup: false, hasPopup: false,
disabled: !this.saveQueryButton.enabled, disabled: !this.saveQueryButton.enabled,
}); });
@@ -468,7 +439,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
hasPopup: false, hasPopup: false,
}; };
const launchCopilotButton: CommandButtonComponentProps = { const launchCopilotButton = {
iconSrc: LaunchCopilot, iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel, iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat, onCommandClick: this.launchQueryCopilotChat,
@@ -481,15 +452,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
if (this.props.copilotEnabled) { if (this.props.copilotEnabled) {
const toggleCopilotButton: CommandButtonComponentProps = { const toggleCopilotButton = {
iconSrc: QueryCommandIcon, iconSrc: QueryCommandIcon,
iconAlt: "Query Advisor", iconAlt: "Copilot",
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
onCommandClick: () => { onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive); this._toggleCopilot(!this.state.copilotActive);
}, },
commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
hasPopup: false, hasPopup: false,
}; };
buttons.push(toggleCopilotButton); buttons.push(toggleCopilotButton);
@@ -500,7 +470,6 @@ 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,
@@ -553,8 +522,6 @@ 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,6 +1,5 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { Pivot, PivotItem } from "@fluentui/react"; import { Pivot, PivotItem } from "@fluentui/react";
import { 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";
@@ -322,7 +321,6 @@ 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,
@@ -336,7 +334,6 @@ 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,
@@ -350,7 +347,6 @@ 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,
@@ -364,7 +360,6 @@ 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,7 +6,6 @@ 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";
@@ -14,7 +13,6 @@ 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";
@@ -43,16 +41,6 @@ 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 && (
@@ -309,9 +297,6 @@ 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:
@@ -340,7 +325,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.Local) || ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
(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,6 +1,5 @@
import { TriggerDefinition } from "@azure/cosmos"; import { TriggerDefinition } from "@azure/cosmos";
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react"; import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
import { 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";
@@ -219,18 +218,6 @@ 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";
@@ -240,7 +227,6 @@ 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,
@@ -255,7 +241,6 @@ 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,
@@ -271,7 +256,6 @@ 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,
@@ -303,6 +287,7 @@ 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,13 +1,12 @@
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";
@@ -81,7 +80,6 @@ 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,
@@ -96,7 +94,6 @@ 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,
@@ -112,7 +109,6 @@ 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,

View File

@@ -1,159 +1,150 @@
import Mousetrap, { ExtendedKeyboardEvent } from "mousetrap";
import * as React from "react"; import * as React from "react";
import { PropsWithChildren, useEffect } from "react";
import { KeyBindingMap, tinykeys } from "tinykeys";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
/** // Provides a system of Keyboard Shortcuts that can be contributed to by different parts of the application.
* Represents a keyboard shortcut handler. //
* Return `true` to prevent the default action of the keyboard shortcut. // The goals of this system are:
* Any other return value will allow the default action to proceed. // * Shortcuts can be contributed from different parts of the application (e.g. the command bar, specified editor tabs, etc.)
*/ // * Contributors may only provide some of their shortcuts, others may be out-of-scope for the current context.
export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void; // * Contributors don't have to add/remove handlers individually, they can use a declarative pattern to set all their handlers at once.
//
// So, in order to do that, we store handlers in a two-level hierarchy:
// 1. We store a mapping of contributors to their Contributions.
// 2. Each Contribution is a mapping of actions to their handlers.
//
// Thus, a Contributor sets all its handlers at once, replacing all handlers previously contributed by that Contributor.
// The system then merges all Contributions into a single set of handlers, with duplicate handlers being handled in the order that the Contributors are processed.
export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHandler>>;
export type KeyboardShortcutHandler = (e: ExtendedKeyboardEvent, combo: string) => boolean | void;
/** /**
* The groups of keyboard actions that can be managed by the application. * Lists all the possible contributors to keyboard shortcut handlers.
* Each group can be updated separately, but, when updated, must be completely replaced. *
* A "Contributor" is a part of the application that can contribute keyboard shortcut handlers.
* The contributor must set all it's keyboard shortcut handlers at once.
* This allows the contributor to easily replace all it's keyboard shortcuts at once.
*
* For example, the command bar adds/removes keyboard shortcut handlers based on the current context, using the existing logic that determines which buttons are shown.
* Isolating contributors like this allow the command bar to easily replace all it's keyboard shortcuts when the context changes without breaking keyboard shortcuts contributed by other parts of the application.
*/ */
export enum KeyboardActionGroup { export enum KeyboardShortcutContributor {
TABS = "TABS", COMMAND_BAR = "COMMAND_BAR",
COMMAND_BAR = "COMMAND_BAR",
} }
/**
* The order in which contributors are processed.
* This is important because the last contributor to set a handler for a given action will be the one that is used.
*/
const contributorOrder: KeyboardShortcutContributor[] = [
KeyboardShortcutContributor.COMMAND_BAR,
];
/** /**
* The possible actions that can be triggered by keyboard shortcuts. * The possible actions that can be triggered by keyboard shortcuts.
*/ */
export enum KeyboardAction { export enum KeyboardShortcutAction {
NEW_QUERY = "NEW_QUERY", NEW_QUERY = "NEW_QUERY",
EXECUTE_ITEM = "EXECUTE_ITEM", 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. * The default keyboard shortcuts for the application.
* This record maps each action to the keyboard shortcuts that trigger the action. * 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. * Even if an action is specified here though, it will not be triggered unless a handler is set for it.
*/ */
const bindings: Record<KeyboardAction, string[]> = { const bindings: Record<KeyboardShortcutAction, string[]> = {
// NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS. [KeyboardShortcutAction.NEW_QUERY]: ["ctrl+j"],
// See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. [KeyboardShortcutAction.EXECUTE_ITEM]: ["shift+enter"],
[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"],
}; };
/**
* Represents all the handlers provided by a contributor.
*/
export type KeyboardShortcutContribution = Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>;
interface KeyboardShortcutState { interface KeyboardShortcutState {
/** /**
* A set of all the keyboard shortcuts handlers. * Collects all the contributions from different contributors.
*/ */
allHandlers: KeyboardHandlerMap; contributions: Partial<Record<KeyboardShortcutContributor, KeyboardShortcutContribution>>;
/** /**
* A set of all the groups of keyboard shortcuts handlers. * A merged set of all the handlers from all contributors.
*/ */
groups: Partial<Record<KeyboardActionGroup, KeyboardHandlerMap>>; allHandlers: KeyboardShortcutContribution;
/** /**
* Sets the keyboard shortcut handlers for the given group. * Sets the keyboard shortcut handlers for a given contributor.
*/ */
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void; setHandlers: (contributor: KeyboardShortcutContributor, handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => void;
} }
/** /**
* Defines the calling component as the manager of the keyboard actions for the given group. * Gets the setHandlers function for a given contributor.
* @param group The group of keyboard actions to manage. * @param contributor The contributor to get the setHandlers function for.
* @returns A function that can be used to set the keyboard action handlers for the given group. * @returns A function that sets the keyboard shortcut handlers for the given contributor.
*/ */
export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => export const useKeyboardShortcutContributor = (contributor: KeyboardShortcutContributor) => {
useKeyboardActionHandlers.getState().setHandlers(group, handlers); const setHandlers = useKeyboardShortcutHandlers.getState().setHandlers;
return (handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => {
setHandlers(contributor, handlers);
};
}
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({ const useKeyboardShortcutHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
allHandlers: {}, contributions: {},
groups: {}, allHandlers: {},
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => { setHandlers: (contributor: KeyboardShortcutContributor, handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => {
const state = get(); const current = get();
const groups = { ...state.groups, [group]: handlers };
// Combine all the handlers from all the groups in the correct order. // Update the list of contributions.
const allHandlers: KeyboardHandlerMap = {}; const newContributions = { ...current.contributions, [contributor]: handlers };
eachKey(groups).forEach((group) => {
const groupHandlers = groups[group]; // Merge all the contributions into a single set of handlers.
if (groupHandlers) { const allHandlers: KeyboardShortcutContribution = {};
eachKey(groupHandlers).forEach((action) => { contributorOrder.forEach((contributor) => {
// Check for duplicate handlers in development mode. const contribution = newContributions[contributor];
// We don't want to raise an error here in production, but having duplicate handlers is a mistake. if (contribution) {
if (process.env.NODE_ENV === "development" && allHandlers[action]) { (Object.keys(contribution) as KeyboardShortcutAction[]).forEach((action) => {
throw new Error(`Duplicate handler for Keyboard Action "${action}".`); allHandlers[action] = contribution[action];
} });
allHandlers[action] = groupHandlers[action]; }
}); });
} set({ contributions: newContributions, allHandlers })
}); }
set({ groups, allHandlers });
},
})); }));
function createHandler(action: KeyboardAction): KeyboardActionHandler { function createHandler(action: KeyboardShortcutAction): KeyboardShortcutHandler {
return (e) => { return (e, combo) => {
const state = useKeyboardActionHandlers.getState(); const handlers = useKeyboardShortcutHandlers.getState().allHandlers;
const handler = state.allHandlers[action]; const handler = handlers[action];
if (handler && handler(e)) { if (handler) {
e.preventDefault(); return handler(e, combo);
e.stopPropagation(); }
} };
};
} }
const allHandlers: KeyBindingMap = {}; export function KeyboardShortcutRoot(props: React.HTMLProps<HTMLDivElement>) {
eachKey(bindings).forEach((action) => { const ref = React.useRef<HTMLDivElement>(null);
const shortcuts = bindings[action]; React.useEffect(() => {
shortcuts.forEach((shortcut) => { const m = new Mousetrap(ref.current);
allHandlers[shortcut] = createHandler(action); const existingStopCallback = m.stopCallback;
}); m.stopCallback = (e, element, combo) => {
}); // Don't block mousetrap callback in the Monaco editor.
if (element.matches(".monaco-editor textarea")) {
return false;
}
return existingStopCallback(e, element, combo);
};
export function KeyboardShortcutRoot({ children }: PropsWithChildren<unknown>) { (Object.keys(bindings) as KeyboardShortcutAction[]).forEach((action) => {
useEffect(() => { m.bind(bindings[action], createHandler(action));
// 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); }, [ref]); // We only need to re-render the component when the ref changes.
}, []);
return <>{children}</>; return <div ref={ref} {...props}>
} </div>;
}
/** 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

@@ -92,53 +92,51 @@ const App: React.FunctionComponent = () => {
} }
return ( return (
<KeyboardShortcutRoot> <KeyboardShortcutRoot className="flexContainer" aria-hidden="false">
<div className="flexContainer" aria-hidden="false"> <div id="divExplorer" className="flexContainer hideOverflows">
<div id="divExplorer" className="flexContainer hideOverflows"> <div id="freeTierTeachingBubble"> </div>
<div id="freeTierTeachingBubble"> </div> {/* Main Command Bar - Start */}
{/* Main Command Bar - Start */} <CommandBar container={explorer} />
<CommandBar container={explorer} /> {/* Collections Tree and Tabs - Begin */}
{/* Collections Tree and Tabs - Begin */} <div className="resourceTreeAndTabs">
<div className="resourceTreeAndTabs"> {/* Collections Tree - Start */}
{/* Collections Tree - Start */} {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( <div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree"> <div className="collectionsTreeWithSplitter">
<div className="collectionsTreeWithSplitter"> {/* Collections Tree Expanded - Start */}
{/* Collections Tree Expanded - Start */} <ResourceTreeContainer
<ResourceTreeContainer container={explorer}
container={explorer} toggleLeftPaneExpanded={toggleLeftPaneExpanded}
toggleLeftPaneExpanded={toggleLeftPaneExpanded} isLeftPaneExpanded={isLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded} />
/> {/* Collections Tree Expanded - End */}
{/* Collections Tree Expanded - End */} {/* Collections Tree Collapsed - Start */}
{/* Collections Tree Collapsed - Start */} <CollapsedResourceTree
<CollapsedResourceTree toggleLeftPaneExpanded={toggleLeftPaneExpanded}
toggleLeftPaneExpanded={toggleLeftPaneExpanded} isLeftPaneExpanded={isLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded} />
/> {/* Collections Tree Collapsed - End */}
{/* Collections Tree Collapsed - End */}
</div>
</div> </div>
)} </div>
<Tabs explorer={explorer} /> )}
</div> <Tabs explorer={explorer} />
{/* Collections Tree and Tabs - End */} </div>
<div {/* Collections Tree and Tabs - End */}
className="dataExplorerErrorConsoleContainer" <div
role="contentinfo" className="dataExplorerErrorConsoleContainer"
aria-label="Notification console" role="contentinfo"
id="explorerNotificationConsole" aria-label="Notification console"
> id="explorerNotificationConsole"
<NotificationConsole /> >
</div> <NotificationConsole />
</div> </div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div> </div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</KeyboardShortcutRoot> </KeyboardShortcutRoot>
); );
}; };

View File

@@ -31,6 +31,11 @@ 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;
@@ -101,6 +106,11 @@ 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,7 +117,6 @@ 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,15 +1,11 @@
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { get } from "../../Utils/arm/generatedClients/cosmos/locations"; import { armRequestWithoutPolling } from "../../Utils/arm/request";
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,
@@ -170,21 +166,11 @@ 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 (map: OfferingIdMap): Promise<PriceMapAndCurrencyCode> => { export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => {
const telemetryData = { const telemetryData = {
feature: "Calculate approximate cost", feature: "Calculate approximate cost",
function: "getPriceMapAndCurrencyCode", function: "getPriceMapAndCurrencyCode",
@@ -195,94 +181,39 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
try { try {
const priceMap = new Map<string, Map<string, number>>(); const priceMap = new Map<string, Map<string, number>>();
let billingCurrency; let currencyCode;
for (const region of map.keys()) { for (const regionItem of regions) {
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: "2023-04-01-preview", apiVersion: "2020-01-01-preview",
body: requestBody,
});
for (const item of response.result) {
if (item.error) {
continue;
}
if (billingCurrency === undefined) {
billingCurrency = item.billingCurrency;
} else if (item.billingCurrency !== billingCurrency) {
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
}
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(region, regionPriceMap);
}
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: priceMap, billingCurrency: billingCurrency };
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
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: { queryParams: {
filter: "armRegionNameeq '" + regionShortName + "'", 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.Items) {
if (item.offeringProperties?.length > 0) { if (currencyCode === undefined) {
regionOfferingIdMap.set(item.offeringProperties[0].offeringId, item.skuName); currencyCode = item.currencyCode;
} else if (item.currencyCode !== currencyCode) {
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
} }
regionPriceMap.set(item.skuName, item.retailPrice);
} }
offeringIdMap.set(regionItem.locationName, regionOfferingIdMap); priceMap.set(regionItem.locationName, regionPriceMap);
} }
selfServeTraceSuccess(telemetryData, getOfferingIdsCodeTimestamp); selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
return offeringIdMap; return { priceMap: priceMap, currencyCode: currencyCode };
} catch (err) { } catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name }; const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp); selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
return undefined; return { priceMap: undefined, currencyCode: undefined };
} }
}; };

View File

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

View File

@@ -30,51 +30,23 @@ export type UpdateDedicatedGatewayRequestProperties = {
serviceType: string; serviceType: string;
}; };
export type FetchPricesResponse = Array<PriceItem>; export type FetchPricesResponse = {
Items: Array<PriceItem>;
export type PriceItem = { NextPageLink: string | undefined;
prices: Array<PriceType>; Count: number;
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>>;
billingCurrency: string; currencyCode: string;
}; };
export type GetOfferingIdsResponse = { export type PriceItem = {
items: Array<OfferingIdItem>; retailPrice: number;
nextPageLink: string | undefined;
};
export type OfferingIdItem = {
skuName: string; skuName: string;
offeringProperties: Array<OfferingProperties>; currencyCode: string;
}; };
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.Local, MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
@@ -154,16 +154,8 @@ 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]: [ [BackendApi.GenerateToken]: [PortalBackendEndpoints.Development],
PortalBackendEndpoints.Development, [BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
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,52 +160,3 @@ 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,4 +1,3 @@
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";
@@ -30,11 +29,6 @@ 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 {
@@ -181,44 +175,4 @@ 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 cosmos-explorer-preview.azurewebsites.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" />
</customHeaders> </customHeaders>
<redirectHeaders> <redirectHeaders>
<clear /> <clear />