mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-29 17:06:58 +00:00
Merge branch 'master' into users/languye/improve-filter-view
This commit is contained in:
commit
fd040fa29c
1320
package-lock.json
generated
1320
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -7,8 +7,8 @@
|
|||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.0.1-beta.2",
|
"@azure/cosmos": "4.0.1-beta.2",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.2.1",
|
"@azure/identity": "1.5.2",
|
||||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "2.14.2",
|
||||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
@ -47,6 +47,7 @@
|
|||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
"@uiw/react-split": "5.9.3",
|
"@uiw/react-split": "5.9.3",
|
||||||
|
"@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",
|
||||||
@ -70,12 +71,14 @@
|
|||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "6.0.1",
|
||||||
"i18next-http-backend": "1.0.23",
|
"i18next-http-backend": "1.0.23",
|
||||||
"iframe-resizer-react": "1.1.0",
|
"iframe-resizer-react": "1.1.0",
|
||||||
|
"immer": "9.0.6",
|
||||||
"immutable": "4.0.0-rc.12",
|
"immutable": "4.0.0-rc.12",
|
||||||
"is-ci": "2.0.0",
|
"is-ci": "2.0.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-typeahead": "2.11.1",
|
"jquery-typeahead": "2.11.1",
|
||||||
"jquery-ui-dist": "1.13.2",
|
"jquery-ui-dist": "1.13.2",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
|
"loader-utils": "2.0.3",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
@ -100,10 +103,12 @@
|
|||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rx-jupyter": "5.5.12",
|
"rx-jupyter": "5.5.12",
|
||||||
"sanitize-html": "2.3.3",
|
"sanitize-html": "2.3.3",
|
||||||
|
"shell-quote": "1.7.3",
|
||||||
"styled-components": "5.0.1",
|
"styled-components": "5.0.1",
|
||||||
"swr": "0.4.0",
|
"swr": "0.4.0",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
"underscore": "1.9.1",
|
"tinykeys": "2.1.0",
|
||||||
|
"underscore": "1.12.1",
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
},
|
},
|
||||||
@ -173,13 +178,13 @@
|
|||||||
"less-vars-loader": "1.1.0",
|
"less-vars-loader": "1.1.0",
|
||||||
"mini-css-extract-plugin": "2.1.0",
|
"mini-css-extract-plugin": "2.1.0",
|
||||||
"monaco-editor-webpack-plugin": "7.1.0",
|
"monaco-editor-webpack-plugin": "7.1.0",
|
||||||
"node-fetch": "2.6.1",
|
"node-fetch": "2.6.7",
|
||||||
"playwright": "1.13.0",
|
"playwright": "1.13.0",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
"querystring-es3": "0.2.1",
|
"querystring-es3": "0.2.1",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"react-dev-utils": "11.0.4",
|
"react-dev-utils": "12.0.1",
|
||||||
"rimraf": "3.0.0",
|
"rimraf": "3.0.0",
|
||||||
"sinon": "3.2.1",
|
"sinon": "3.2.1",
|
||||||
"style-loader": "0.23.0",
|
"style-loader": "0.23.0",
|
||||||
|
@ -138,7 +138,7 @@ export class PortalBackendEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MongoProxyEndpoints {
|
export class MongoProxyEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7238";
|
public static readonly Local: string = "https://localhost:7238";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||||
|
@ -672,6 +672,27 @@ 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,
|
||||||
|
];
|
||||||
|
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||||
|
if (
|
||||||
|
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
|
||||||
|
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||||
|
) {
|
||||||
|
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
canAccessMongoProxy &&
|
||||||
|
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||||
|
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
||||||
// It causes problems for TypeScript understanding the types
|
// It causes problems for TypeScript understanding the types
|
||||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||||
@ -688,24 +709,3 @@ async function errorHandling(response: Response, action: string, params: unknown
|
|||||||
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
|
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
|
||||||
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
|
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useMongoProxyEndpoint(api: string): boolean {
|
|
||||||
const activeMongoProxyEndpoints: string[] = [
|
|
||||||
MongoProxyEndpoints.Development,
|
|
||||||
MongoProxyEndpoints.Mpac,
|
|
||||||
MongoProxyEndpoints.Prod,
|
|
||||||
];
|
|
||||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
|
||||||
if (
|
|
||||||
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development &&
|
|
||||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
|
||||||
) {
|
|
||||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
canAccessMongoProxy &&
|
|
||||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
|
||||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -83,6 +83,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||||
|
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
@ -108,6 +109,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
"updateDocument",
|
"updateDocument",
|
||||||
"deleteDocument",
|
"deleteDocument",
|
||||||
"createCollectionWithProxy",
|
"createCollectionWithProxy",
|
||||||
|
"legacyMongoShell",
|
||||||
],
|
],
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||||
import { KeyCodes } from "../../../Common/Constants";
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick: (e: React.SyntheticEvent) => void;
|
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
|
|||||||
* Vertical bar to divide buttons
|
* Vertical bar to divide buttons
|
||||||
*/
|
*/
|
||||||
isDivider?: boolean;
|
isDivider?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aria-label for the button
|
* Aria-label for the button
|
||||||
*/
|
*/
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
|
||||||
|
* If not specified, the button will not be triggerable by keyboard shortcuts.
|
||||||
|
*/
|
||||||
|
keyboardAction?: KeyboardAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const buttons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
const buttons =
|
const buttons =
|
||||||
@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||||
@ -57,6 +58,7 @@ export function createStaticCommandBarButtons(
|
|||||||
buttons.push(homeBtn);
|
buttons.push(homeBtn);
|
||||||
|
|
||||||
const newCollectionBtn = createNewCollectionGroup(container);
|
const newCollectionBtn = createNewCollectionGroup(container);
|
||||||
|
newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below.
|
||||||
buttons.push(newCollectionBtn);
|
buttons.push(newCollectionBtn);
|
||||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
@ -94,6 +96,7 @@ export function createStaticCommandBarButtons(
|
|||||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddStoredProcedureIcon,
|
iconSrc: AddStoredProcedureIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_SPROC,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
@ -277,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_DATABASE,
|
||||||
onCommandClick: async () => {
|
onCommandClick: async () => {
|
||||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
if (throughputCap && throughputCap !== -1) {
|
if (throughputCap && throughputCap !== -1) {
|
||||||
@ -297,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
|||||||
id: "newQueryBtn",
|
id: "newQueryBtn",
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||||
@ -312,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
|||||||
id: "newQueryBtn",
|
id: "newQueryBtn",
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||||
@ -337,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddStoredProcedureIcon,
|
iconSrc: AddStoredProcedureIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_SPROC,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
@ -356,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
|
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddUdfIcon,
|
iconSrc: AddUdfIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_UDF,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||||
@ -375,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
const newTriggerBtn: CommandButtonComponentProps = {
|
const newTriggerBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddTriggerIcon,
|
iconSrc: AddTriggerIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_TRIGGER,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
|
||||||
@ -397,6 +406,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
|||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||||
onCommandClick: () =>
|
onCommandClick: () =>
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
@ -411,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: OpenQueryFromDiskIcon,
|
iconSrc: OpenQueryFromDiskIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
IDropdownStyles,
|
IDropdownStyles,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { KeyboardHandlerMap } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||||
@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
|||||||
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
||||||
|
const handlers: KeyboardHandlerMap = {};
|
||||||
|
|
||||||
|
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
if (!button.disabled && button.keyboardAction) {
|
||||||
|
handlers[button.keyboardAction] = (e) => {
|
||||||
|
button.onCommandClick(e);
|
||||||
|
|
||||||
|
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button.children && button.children.length > 0) {
|
||||||
|
createHandlers(button.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createHandlers(allButtons);
|
||||||
|
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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();
|
||||||
|
@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(entity);
|
deferred.resolve(entity);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
errorText,
|
||||||
"CreateKeyspaceCassandra",
|
"CreateKeyspaceCassandra",
|
||||||
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
|
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
|
||||||
);
|
);
|
||||||
deferred.reject(error);
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(
|
||||||
|
errorText,
|
||||||
|
"CreateTableCassandra",
|
||||||
|
`Error while creating a table with query ${createTableQuery}`,
|
||||||
|
);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data.columns);
|
deferred.resolve(data.columns);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data.columns);
|
deferred.resolve(data.columns);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
@ -893,6 +894,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: NewDocumentIcon,
|
iconSrc: NewDocumentIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_ITEM,
|
||||||
onCommandClick: this.onNewDocumentClick,
|
onCommandClick: this.onNewDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -907,6 +909,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveNewDocumentClick,
|
onCommandClick: this.onSaveNewDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -921,6 +924,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: DiscardIcon,
|
iconSrc: DiscardIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: this.onRevertNewDocumentClick,
|
onCommandClick: this.onRevertNewDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -936,6 +940,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveExistingDocumentClick,
|
onCommandClick: this.onSaveExistingDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -950,6 +955,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: DiscardIcon,
|
iconSrc: DiscardIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: this.onRevertExisitingDocumentClick,
|
onCommandClick: this.onRevertExisitingDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -965,6 +971,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: DeleteDocumentIcon,
|
iconSrc: DeleteDocumentIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.DELETE_ITEM,
|
||||||
onCommandClick: this.onDeleteExisitingDocumentClick,
|
onCommandClick: this.onDeleteExisitingDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -9,7 +9,6 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa
|
|||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import { getMongoShellOrigin } from "./getMongoShellOrigin";
|
|
||||||
import { getMongoShellUrl } from "./getMongoShellUrl";
|
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||||
|
|
||||||
//eslint-disable-next-line
|
//eslint-disable-next-line
|
||||||
@ -35,7 +34,7 @@ export interface IMongoShellTabAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMongoShellTabComponentStates {
|
export interface IMongoShellTabComponentStates {
|
||||||
url: string;
|
url: URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMongoShellTabComponentProps {
|
export interface IMongoShellTabComponentProps {
|
||||||
@ -50,13 +49,16 @@ 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 = userContext.features.enableLegacyMongoShell;
|
||||||
|
// this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
url: getMongoShellUrl(),
|
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||||
};
|
};
|
||||||
|
|
||||||
props.onMongoShellTabAccessor({
|
props.onMongoShellTabAccessor({
|
||||||
@ -119,9 +121,10 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
) + Constants.MongoDBAccounts.defaultPort.toString();
|
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||||
const databaseId = this.props.collection.databaseId;
|
const databaseId = this.props.collection.databaseId;
|
||||||
const collectionId = this.props.collection.id();
|
const collectionId = this.props.collection.id();
|
||||||
const apiEndpoint = configContext.BACKEND_ENDPOINT;
|
const apiEndpoint = this._useMongoProxyEndpoint
|
||||||
|
? configContext.MONGO_PROXY_ENDPOINT
|
||||||
|
: configContext.BACKEND_ENDPOINT;
|
||||||
const encryptedAuthToken: string = userContext.accessToken;
|
const encryptedAuthToken: string = userContext.accessToken;
|
||||||
const targetOrigin = getMongoShellOrigin();
|
|
||||||
|
|
||||||
shellIframe.contentWindow.postMessage(
|
shellIframe.contentWindow.postMessage(
|
||||||
{
|
{
|
||||||
@ -137,7 +140,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
apiEndpoint: apiEndpoint,
|
apiEndpoint: apiEndpoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targetOrigin,
|
window.origin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +221,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
name="explorer"
|
name="explorer"
|
||||||
className="iframe"
|
className="iframe"
|
||||||
style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }}
|
style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }}
|
||||||
src={this.state.url}
|
src={this.state.url.toString()}
|
||||||
id={this.props.tabsBaseInstance.tabId}
|
id={this.props.tabsBaseInstance.tabId}
|
||||||
onLoad={(event) => this.setContentFocus(event)}
|
onLoad={(event) => this.setContentFocus(event)}
|
||||||
title="Mongo Shell"
|
title="Mongo Shell"
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
|
||||||
import { configContext } from "../../../ConfigContext";
|
|
||||||
import { updateUserContext } from "../../../UserContext";
|
|
||||||
import { getMongoShellOrigin } from "./getMongoShellOrigin";
|
|
||||||
|
|
||||||
describe("getMongoShellOrigin", () => {
|
|
||||||
(window as { origin: string }).origin = "window_origin";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV1": "false",
|
|
||||||
"feature.enableLegacyMongoShellV2": "false",
|
|
||||||
"feature.enableLegacyMongoShellV1Debug": "false",
|
|
||||||
"feature.enableLegacyMongoShellV2Debug": "false",
|
|
||||||
"feature.loadLegacyMongoShellFromBE": "false",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return by default", () => {
|
|
||||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return window.origin when enableLegacyMongoShellV1", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV1": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return window.origin when enableLegacyMongoShellV2===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV2": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return window.origin when enableLegacyMongoShellV1Debug===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV1Debug": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return window.origin when enableLegacyMongoShellV2Debug===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV2Debug": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellOrigin()).toBe(window.origin);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return BACKEND_ENDPOINT when loadLegacyMongoShellFromBE===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.loadLegacyMongoShellFromBE": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellOrigin()).toBe(configContext.BACKEND_ENDPOINT);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,10 +0,0 @@
|
|||||||
import { configContext } from "../../../ConfigContext";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
|
|
||||||
export function getMongoShellOrigin(): string {
|
|
||||||
if (userContext.features.loadLegacyMongoShellFromBE === true) {
|
|
||||||
return configContext.BACKEND_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.origin;
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
|
||||||
import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
|
|
||||||
import { updateUserContext, userContext } from "../../../UserContext";
|
import { updateUserContext, userContext } from "../../../UserContext";
|
||||||
import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl";
|
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||||
|
|
||||||
const mongoBackendEndpoint = "https://localhost:1234";
|
const mongoBackendEndpoint = "https://localhost:1234";
|
||||||
|
const hostedExplorerURL = "https://cosmos.azure.com/";
|
||||||
|
|
||||||
describe("getMongoShellUrl", () => {
|
describe("getMongoShellUrl", () => {
|
||||||
let queryString = "";
|
let queryString = "";
|
||||||
@ -13,6 +13,7 @@ describe("getMongoShellUrl", () => {
|
|||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||||
|
hostedExplorerURL: hostedExplorerURL,
|
||||||
platform: Platform.Hosted,
|
platform: Platform.Hosted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -32,175 +33,18 @@ describe("getMongoShellUrl", () => {
|
|||||||
cassandraEndpoint: "fakeCassandraEndpoint",
|
cassandraEndpoint: "fakeCassandraEndpoint",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV1": "false",
|
|
||||||
"feature.enableLegacyMongoShellV2": "false",
|
|
||||||
"feature.enableLegacyMongoShellV1Debug": "false",
|
|
||||||
"feature.enableLegacyMongoShellV2Debug": "false",
|
|
||||||
"feature.loadLegacyMongoShellFromBE": "false",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
portalEnv: "prod",
|
portalEnv: "prod",
|
||||||
});
|
});
|
||||||
|
|
||||||
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return /mongoshell/indexv2.html by default", () => {
|
it("should return /indexv2.html by default", () => {
|
||||||
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => {
|
it("should return /index.html when useMongoProxyEndpoint is true", () => {
|
||||||
updateUserContext({
|
const useMongoProxyEndpoint: boolean = true;
|
||||||
portalEnv: "localhost",
|
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV1===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV1": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellUrl()).toBe(`/mongoshell/index.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV2===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV2": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV1Debug===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV1Debug": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/index.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return /mongoshell/index.html when enableLegacyMongoShellV2Debug===true", () => {
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.enableLegacyMongoShellV2Debug": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getMongoShellUrl()).toBe(`/mongoshell/debug/indexv2.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("loadLegacyMongoShellFromBE===true", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
resetConfigContext();
|
|
||||||
updateConfigContext({
|
|
||||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
|
||||||
platform: Platform.Hosted,
|
|
||||||
});
|
|
||||||
|
|
||||||
updateUserContext({
|
|
||||||
features: extractFeatures(
|
|
||||||
new URLSearchParams({
|
|
||||||
"feature.loadLegacyMongoShellFromBE": "true",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return /mongoshell/index.html", () => {
|
|
||||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
|
||||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
|
||||||
updateConfigContext({
|
|
||||||
platform: Platform.Portal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
|
||||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("configContext.BACKEND_ENDPOINT !== '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
|
||||||
resetConfigContext();
|
|
||||||
updateConfigContext({
|
|
||||||
platform: Platform.Portal,
|
|
||||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
|
||||||
});
|
|
||||||
|
|
||||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
|
||||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform === Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
|
||||||
resetConfigContext();
|
|
||||||
updateConfigContext({
|
|
||||||
platform: Platform.Hosted,
|
|
||||||
});
|
|
||||||
|
|
||||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
|
||||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("configContext.BACKEND_ENDPOINT === '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => {
|
|
||||||
resetConfigContext();
|
|
||||||
updateConfigContext({
|
|
||||||
platform: Platform.Portal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
|
|
||||||
expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getExtensionEndpoint", () => {
|
|
||||||
it("when platform === Platform.Hosted, backendEndpoint is undefined", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Hosted, undefined)).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when platform === Platform.Hosted, backendEndpoint === ''", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Hosted, "")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when platform === Platform.Hosted, backendEndpoint === null", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Hosted, null)).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when platform === Platform.Hosted, backendEndpoint != ''", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Hosted, "foo")).toBe("foo");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when platform === Platform.Portal, backendEndpoint is udefined", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Portal, undefined)).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when platform === Platform.Portal, backendEndpoint === ''", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Portal, "")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when platform === Platform.Portal, backendEndpoint === null", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Portal, null)).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when platform !== Platform.Portal, backendEndpoint != ''", () => {
|
|
||||||
expect(getExtensionEndpoint(Platform.Portal, "foo")).toBe("foo");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,45 +1,15 @@
|
|||||||
import { configContext, Platform } from "../../../ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
|
|
||||||
export function getMongoShellUrl(): string {
|
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL {
|
||||||
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}`;
|
||||||
|
const path: string = useMongoProxyEndpoint
|
||||||
|
? `/mongoshell/index.html?${queryString}`
|
||||||
|
: `/mongoshell/indexv2.html?${queryString}`;
|
||||||
|
|
||||||
if (userContext.features.enableLegacyMongoShellV1 === true) {
|
return new URL(path, configContext.hostedExplorerURL);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
|
|||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
@ -393,6 +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,
|
||||||
onCommandClick: this.props.isSampleCopilotActive
|
onCommandClick: this.props.isSampleCopilotActive
|
||||||
? () => OnExecuteQueryClick(this.props.copilotStore)
|
? () => OnExecuteQueryClick(this.props.copilotStore)
|
||||||
: this.onExecuteQueryClick,
|
: this.onExecuteQueryClick,
|
||||||
@ -408,6 +410,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveQueryIcon,
|
iconSrc: SaveQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveQueryClick,
|
onCommandClick: this.onSaveQueryClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -437,7 +440,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const launchCopilotButton = {
|
const launchCopilotButton: CommandButtonComponentProps = {
|
||||||
iconSrc: LaunchCopilot,
|
iconSrc: LaunchCopilot,
|
||||||
iconAlt: mainButtonLabel,
|
iconAlt: mainButtonLabel,
|
||||||
onCommandClick: this.launchQueryCopilotChat,
|
onCommandClick: this.launchQueryCopilotChat,
|
||||||
@ -450,9 +453,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
const toggleCopilotButton = {
|
const toggleCopilotButton: CommandButtonComponentProps = {
|
||||||
iconSrc: QueryCommandIcon,
|
iconSrc: QueryCommandIcon,
|
||||||
iconAlt: "Copilot",
|
iconAlt: "Copilot",
|
||||||
|
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
this._toggleCopilot(!this.state.copilotActive);
|
this._toggleCopilot(!this.state.copilotActive);
|
||||||
},
|
},
|
||||||
@ -468,6 +472,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: CancelQueryIcon,
|
iconSrc: CancelQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: () => this.queryAbortController.abort(),
|
onCommandClick: () => this.queryAbortController.abort(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||||
import { Pivot, PivotItem } from "@fluentui/react";
|
import { Pivot, PivotItem } from "@fluentui/react";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
@ -321,6 +322,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveClick,
|
onCommandClick: this.onSaveClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -334,6 +336,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onUpdateClick,
|
onCommandClick: this.onUpdateClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -347,6 +350,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: DiscardIcon,
|
iconSrc: DiscardIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: this.onDiscard,
|
onCommandClick: this.onDiscard,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -360,6 +364,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.EXECUTE_ITEM,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
this.collection.container.openExecuteSprocParamsPanel(this.node);
|
this.collection.container.openExecuteSprocParamsPanel(this.node);
|
||||||
},
|
},
|
||||||
|
@ -6,6 +6,7 @@ import { IpRule } from "Contracts/DataModels";
|
|||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||||
@ -13,6 +14,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
|||||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||||
@ -41,6 +43,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
||||||
|
|
||||||
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
|
||||||
|
useEffect(() => {
|
||||||
|
setKeyboardHandlers({
|
||||||
|
[KeyboardAction.SELECT_LEFT_TAB]: () => useTabs.getState().selectLeftTab(),
|
||||||
|
[KeyboardAction.SELECT_RIGHT_TAB]: () => useTabs.getState().selectRightTab(),
|
||||||
|
[KeyboardAction.CLOSE_TAB]: () => useTabs.getState().closeActiveTab(),
|
||||||
|
});
|
||||||
|
}, [setKeyboardHandlers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<div className="tabsManagerContainer">
|
||||||
{networkSettingsWarning && (
|
{networkSettingsWarning && (
|
||||||
@ -297,6 +309,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||||
|
// React tabs have no context buttons.
|
||||||
|
useCommandBar.getState().setContextButtons([]);
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
switch (activeReactTab) {
|
switch (activeReactTab) {
|
||||||
case ReactTabKind.Connect:
|
case ReactTabKind.Connect:
|
||||||
@ -325,7 +340,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||||
if (
|
if (
|
||||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
|
||||||
(userContext.apiType === "Cassandra" &&
|
(userContext.apiType === "Cassandra" &&
|
||||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||||
ipRules?.length
|
ipRules?.length
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { TriggerDefinition } from "@azure/cosmos";
|
import { TriggerDefinition } from "@azure/cosmos";
|
||||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
@ -218,6 +219,18 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
return !!value;
|
return !!value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(_prevProps: TriggerTab, prevState: ITriggerTabContentState): void {
|
||||||
|
const { triggerBody, triggerId, triggerType, triggerOperation } = this.state;
|
||||||
|
if (
|
||||||
|
triggerId !== prevState.triggerId ||
|
||||||
|
triggerBody !== prevState.triggerBody ||
|
||||||
|
triggerType !== prevState.triggerType ||
|
||||||
|
triggerOperation !== prevState.triggerOperation
|
||||||
|
) {
|
||||||
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
const label = "Save";
|
const label = "Save";
|
||||||
@ -227,6 +240,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
...this,
|
...this,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveClick,
|
onCommandClick: this.onSaveClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -241,6 +255,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
...this,
|
...this,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onUpdateClick,
|
onCommandClick: this.onUpdateClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -256,6 +271,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
...this,
|
...this,
|
||||||
iconSrc: DiscardIcon,
|
iconSrc: DiscardIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: this.onDiscard,
|
onCommandClick: this.onDiscard,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -287,7 +303,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
};
|
};
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
|
||||||
const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state;
|
const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
|
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { Label, TextField } from "@fluentui/react";
|
import { Label, TextField } from "@fluentui/react";
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
||||||
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
@ -80,6 +81,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
setState: this.setState,
|
setState: this.setState,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveClick,
|
onCommandClick: this.onSaveClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -94,6 +96,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
...this,
|
...this,
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onUpdateClick,
|
onCommandClick: this.onUpdateClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@ -109,6 +112,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
...this,
|
...this,
|
||||||
iconSrc: DiscardIcon,
|
iconSrc: DiscardIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: this.onDiscard,
|
onCommandClick: this.onDiscard,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
157
src/KeyboardShortcuts.tsx
Normal file
157
src/KeyboardShortcuts.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { PropsWithChildren, useEffect } from "react";
|
||||||
|
import { KeyBindingMap, tinykeys } from "tinykeys";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a keyboard shortcut handler.
|
||||||
|
* Return `true` to prevent the default action of the keyboard shortcut.
|
||||||
|
* Any other return value will allow the default action to proceed.
|
||||||
|
*/
|
||||||
|
export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void;
|
||||||
|
|
||||||
|
export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHandler>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The groups of keyboard actions that can be managed by the application.
|
||||||
|
* Each group can be updated separately, but, when updated, must be completely replaced.
|
||||||
|
*/
|
||||||
|
export enum KeyboardActionGroup {
|
||||||
|
TABS = "TABS",
|
||||||
|
COMMAND_BAR = "COMMAND_BAR",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible actions that can be triggered by keyboard shortcuts.
|
||||||
|
*/
|
||||||
|
export enum KeyboardAction {
|
||||||
|
NEW_QUERY = "NEW_QUERY",
|
||||||
|
EXECUTE_ITEM = "EXECUTE_ITEM",
|
||||||
|
CANCEL_OR_DISCARD = "CANCEL_OR_DISCARD",
|
||||||
|
SAVE_ITEM = "SAVE_ITEM",
|
||||||
|
OPEN_QUERY = "OPEN_QUERY",
|
||||||
|
OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK",
|
||||||
|
NEW_SPROC = "NEW_SPROC",
|
||||||
|
NEW_UDF = "NEW_UDF",
|
||||||
|
NEW_TRIGGER = "NEW_TRIGGER",
|
||||||
|
NEW_DATABASE = "NEW_DATABASE",
|
||||||
|
NEW_COLLECTION = "NEW_CONTAINER",
|
||||||
|
NEW_ITEM = "NEW_ITEM",
|
||||||
|
DELETE_ITEM = "DELETE_ITEM",
|
||||||
|
TOGGLE_COPILOT = "TOGGLE_COPILOT",
|
||||||
|
SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
|
||||||
|
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
|
||||||
|
CLOSE_TAB = "CLOSE_TAB",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The keyboard shortcuts for the application.
|
||||||
|
* This record maps each action to the keyboard shortcuts that trigger the action.
|
||||||
|
* Even if an action is specified here, it will not be triggered unless a handler is set for it.
|
||||||
|
*/
|
||||||
|
const bindings: Record<KeyboardAction, string[]> = {
|
||||||
|
// NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS.
|
||||||
|
// See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts.
|
||||||
|
|
||||||
|
[KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"],
|
||||||
|
[KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"],
|
||||||
|
[KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"],
|
||||||
|
[KeyboardAction.SAVE_ITEM]: ["$mod+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+["],
|
||||||
|
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]"],
|
||||||
|
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface KeyboardShortcutState {
|
||||||
|
/**
|
||||||
|
* A set of all the keyboard shortcuts handlers.
|
||||||
|
*/
|
||||||
|
allHandlers: KeyboardHandlerMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of all the groups of keyboard shortcuts handlers.
|
||||||
|
*/
|
||||||
|
groups: Partial<Record<KeyboardActionGroup, KeyboardHandlerMap>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the keyboard shortcut handlers for the given group.
|
||||||
|
*/
|
||||||
|
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the calling component as the manager of the keyboard actions for the given group.
|
||||||
|
* @param group The group of keyboard actions to manage.
|
||||||
|
* @returns A function that can be used to set the keyboard action handlers for the given group.
|
||||||
|
*/
|
||||||
|
export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
|
||||||
|
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
|
||||||
|
|
||||||
|
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
|
||||||
|
allHandlers: {},
|
||||||
|
groups: {},
|
||||||
|
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {
|
||||||
|
const state = get();
|
||||||
|
const groups = { ...state.groups, [group]: handlers };
|
||||||
|
|
||||||
|
// Combine all the handlers from all the groups in the correct order.
|
||||||
|
const allHandlers: KeyboardHandlerMap = {};
|
||||||
|
eachKey(groups).forEach((group) => {
|
||||||
|
const groupHandlers = groups[group];
|
||||||
|
if (groupHandlers) {
|
||||||
|
eachKey(groupHandlers).forEach((action) => {
|
||||||
|
// Check for duplicate handlers in development mode.
|
||||||
|
// We don't want to raise an error here in production, but having duplicate handlers is a mistake.
|
||||||
|
if (process.env.NODE_ENV === "development" && allHandlers[action]) {
|
||||||
|
throw new Error(`Duplicate handler for Keyboard Action "${action}".`);
|
||||||
|
}
|
||||||
|
allHandlers[action] = groupHandlers[action];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
set({ groups, allHandlers });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createHandler(action: KeyboardAction): KeyboardActionHandler {
|
||||||
|
return (e) => {
|
||||||
|
const state = useKeyboardActionHandlers.getState();
|
||||||
|
const handler = state.allHandlers[action];
|
||||||
|
if (handler && handler(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allHandlers: KeyBindingMap = {};
|
||||||
|
eachKey(bindings).forEach((action) => {
|
||||||
|
const shortcuts = bindings[action];
|
||||||
|
shortcuts.forEach((shortcut) => {
|
||||||
|
allHandlers[shortcut] = createHandler(action);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function KeyboardShortcutRoot({ children }: PropsWithChildren<unknown>) {
|
||||||
|
useEffect(() => {
|
||||||
|
// We bind to the body because Fluent UI components sometimes shift focus to the body, which is above the root React component.
|
||||||
|
tinykeys(document.body, allHandlers);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A _typed_ version of `Object.keys` that preserves the original key type */
|
||||||
|
function eachKey<K extends string | number | symbol, V>(record: Partial<Record<K, V>>): K[] {
|
||||||
|
return Object.keys(record) as K[];
|
||||||
|
}
|
@ -21,6 +21,7 @@ import "../externals/jquery.typeahead.min.js";
|
|||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { Platform } from "ConfigContext";
|
import { Platform } from "ConfigContext";
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
|
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
import "../images/favicon.ico";
|
import "../images/favicon.ico";
|
||||||
@ -91,6 +92,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<KeyboardShortcutRoot>
|
||||||
<div 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>
|
||||||
@ -137,6 +139,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
{<MongoQuickstartTutorial />}
|
{<MongoQuickstartTutorial />}
|
||||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||||
</div>
|
</div>
|
||||||
|
</KeyboardShortcutRoot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,11 +31,6 @@ export type Features = {
|
|||||||
readonly mongoProxyAPIs?: string;
|
readonly mongoProxyAPIs?: string;
|
||||||
readonly enableThroughputCap: boolean;
|
readonly enableThroughputCap: boolean;
|
||||||
readonly enableHierarchicalKeys: boolean;
|
readonly enableHierarchicalKeys: boolean;
|
||||||
readonly enableLegacyMongoShellV1: boolean;
|
|
||||||
readonly enableLegacyMongoShellV1Debug: boolean;
|
|
||||||
readonly enableLegacyMongoShellV2: boolean;
|
|
||||||
readonly enableLegacyMongoShellV2Debug: boolean;
|
|
||||||
readonly loadLegacyMongoShellFromBE: boolean;
|
|
||||||
readonly enableCopilot: boolean;
|
readonly enableCopilot: boolean;
|
||||||
readonly copilotVersion?: string;
|
readonly copilotVersion?: string;
|
||||||
readonly disableCopilotPhoenixGateaway: boolean;
|
readonly disableCopilotPhoenixGateaway: boolean;
|
||||||
@ -43,6 +38,7 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
|
readonly enableLegacyMongoShell: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@ -106,11 +102,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||||
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
|
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
|
||||||
enableLegacyMongoShellV1: "true" === get("enablelegacymongoshellv1"),
|
|
||||||
enableLegacyMongoShellV1Debug: "true" === get("enablelegacymongoshellv1debug"),
|
|
||||||
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
|
||||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
|
||||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
|
||||||
enableCopilot: "true" === get("enablecopilot", "true"),
|
enableCopilot: "true" === get("enablecopilot", "true"),
|
||||||
copilotVersion: get("copilotversion") ?? "v2.0",
|
copilotVersion: get("copilotversion") ?? "v2.0",
|
||||||
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
|
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
|
||||||
@ -118,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
|
enableLegacyMongoShell: "true" === get("enablelegacymongoshell"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||||
MongoProxyEndpoints.Development,
|
MongoProxyEndpoints.Local,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { clamp } from "@fluentui/react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||||
@ -29,6 +30,11 @@ export interface TabsState {
|
|||||||
setQueryCopilotTabInitialInput: (input: string) => void;
|
setQueryCopilotTabInitialInput: (input: string) => void;
|
||||||
setIsTabExecuting: (state: boolean) => void;
|
setIsTabExecuting: (state: boolean) => void;
|
||||||
setIsQueryErrorThrown: (state: boolean) => void;
|
setIsQueryErrorThrown: (state: boolean) => void;
|
||||||
|
getCurrentTabIndex: () => number;
|
||||||
|
selectTabByIndex: (index: number) => void;
|
||||||
|
selectLeftTab: () => void;
|
||||||
|
selectRightTab: () => void;
|
||||||
|
closeActiveTab: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReactTabKind {
|
export enum ReactTabKind {
|
||||||
@ -175,4 +181,44 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
setIsQueryErrorThrown: (state: boolean) => {
|
setIsQueryErrorThrown: (state: boolean) => {
|
||||||
set({ isQueryErrorThrown: state });
|
set({ isQueryErrorThrown: state });
|
||||||
},
|
},
|
||||||
|
getCurrentTabIndex: () => {
|
||||||
|
const state = get();
|
||||||
|
if (state.activeReactTab !== undefined) {
|
||||||
|
return state.openedReactTabs.indexOf(state.activeReactTab);
|
||||||
|
} else if (state.activeTab !== undefined) {
|
||||||
|
const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab);
|
||||||
|
if (nonReactTabIndex !== -1) {
|
||||||
|
return state.openedReactTabs.length + nonReactTabIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
|
selectTabByIndex: (index: number) => {
|
||||||
|
const state = get();
|
||||||
|
const totalTabCount = state.openedReactTabs.length + state.openedTabs.length;
|
||||||
|
const clampedIndex = clamp(index, totalTabCount - 1, 0);
|
||||||
|
|
||||||
|
if (clampedIndex < state.openedReactTabs.length) {
|
||||||
|
set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] });
|
||||||
|
} else {
|
||||||
|
set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectLeftTab: () => {
|
||||||
|
const state = get();
|
||||||
|
state.selectTabByIndex(state.getCurrentTabIndex() - 1);
|
||||||
|
},
|
||||||
|
selectRightTab: () => {
|
||||||
|
const state = get();
|
||||||
|
state.selectTabByIndex(state.getCurrentTabIndex() + 1);
|
||||||
|
},
|
||||||
|
closeActiveTab: () => {
|
||||||
|
const state = get();
|
||||||
|
if (state.activeReactTab !== undefined) {
|
||||||
|
state.closeReactTab(state.activeReactTab);
|
||||||
|
} else if (state.activeTab !== undefined) {
|
||||||
|
state.closeTab(state.activeTab);
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<clear />
|
<clear />
|
||||||
<add name="X-Xss-Protection" value="1; mode=block" />
|
<add name="X-Xss-Protection" value="1; mode=block" />
|
||||||
<add name="X-Content-Type-Options" value="nosniff" />
|
<add name="X-Content-Type-Options" value="nosniff" />
|
||||||
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net" />
|
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net cosmos-explorer-preview.azurewebsites.net" />
|
||||||
</customHeaders>
|
</customHeaders>
|
||||||
<redirectHeaders>
|
<redirectHeaders>
|
||||||
<clear />
|
<clear />
|
||||||
|
Loading…
Reference in New Issue
Block a user