Merge branch 'master' into users/languye/improve-filter-view

This commit is contained in:
Laurent Nguyen 2024-04-22 16:36:03 +02:00
commit fd040fa29c
29 changed files with 1220 additions and 965 deletions

1322
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/cosmos": "4.0.1-beta.2",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7",
"@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
"@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
@ -47,6 +47,7 @@
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@uiw/react-split": "5.9.3",
"@xmldom/xmldom": "0.7.13",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",
@ -70,12 +71,14 @@
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12",
"is-ci": "2.0.0",
"jquery": "3.7.1",
"jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.2",
"knockout": "3.5.1",
"loader-utils": "2.0.3",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
@ -100,10 +103,12 @@
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3",
"shell-quote": "1.7.3",
"styled-components": "5.0.1",
"swr": "0.4.0",
"terser-webpack-plugin": "5.3.9",
"underscore": "1.9.1",
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"zustand": "3.5.0"
},
@ -173,13 +178,13 @@
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "7.1.0",
"node-fetch": "2.6.1",
"node-fetch": "2.6.7",
"playwright": "1.13.0",
"prettier": "3.0.3",
"process": "0.11.10",
"querystring-es3": "0.2.1",
"raw-loader": "0.5.1",
"react-dev-utils": "11.0.4",
"react-dev-utils": "12.0.1",
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
@ -241,4 +246,4 @@
"printWidth": 120,
"endOfLine": "auto"
}
}
}

View File

@ -138,7 +138,7 @@ export class PortalBackendEndpoints {
}
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 Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";

View File

@ -672,6 +672,27 @@ export function getEndpoint(endpoint: string): string {
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
// It causes problems for TypeScript understanding the types
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 {
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

@ -83,6 +83,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
@ -108,6 +109,7 @@ let configContext: Readonly<ConfigContext> = {
"updateDocument",
"deleteDocument",
"createCollectionWithProxy",
"legacyMongoShell",
],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,

View File

@ -1,6 +1,7 @@
/**
* React component for Command button component.
*/
import { KeyboardAction } from "KeyboardShortcuts";
import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants";
@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
/**
* Click handler for command button click
*/
onCommandClick: (e: React.SyntheticEvent) => void;
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
/**
* Label for the button
@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
* Vertical bar to divide buttons
*/
isDivider?: boolean;
/**
* Aria-label for the button
*/
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> {

View File

@ -5,6 +5,7 @@
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight;
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
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 (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar

View File

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

View File

@ -7,6 +7,7 @@ import {
IDropdownStyles,
} from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
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}
autoComplete="off"
styles={getTextFieldStyles()}
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Type a new keyspace id"
size={40}
value={newKeyspaceId}
@ -292,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true}
ariaLabel="addCollection-table Id Create table"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Enter table Id"
size={20}
value={tableId}

View File

@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try {
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();

View File

@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity);
},
(error) => {
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(
error,
errorText,
"CreateKeyspaceCassandra",
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
);
deferred.reject(error);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(
errorText,
"CreateTableCassandra",
`Error while creating a table with query ${createTableQuery}`,
);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data);
},
(error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data);
},
(error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns);
},
(error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns);
},
(error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);

View File

@ -1,6 +1,7 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as ko from "knockout";
@ -893,6 +894,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: NewDocumentIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_ITEM,
onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@ -907,6 +909,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@ -921,6 +924,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@ -936,6 +940,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveExistingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@ -950,6 +955,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@ -965,6 +971,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: DeleteDocumentIcon,
iconAlt: label,
keyboardAction: KeyboardAction.DELETE_ITEM,
onCommandClick: this.onDeleteExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,

View File

@ -9,7 +9,6 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import TabsBase from "../TabsBase";
import { getMongoShellOrigin } from "./getMongoShellOrigin";
import { getMongoShellUrl } from "./getMongoShellUrl";
//eslint-disable-next-line
@ -35,7 +34,7 @@ export interface IMongoShellTabAccessor {
}
export interface IMongoShellTabComponentStates {
url: string;
url: URL;
}
export interface IMongoShellTabComponentProps {
@ -50,13 +49,16 @@ export default class MongoShellTabComponent extends Component<
IMongoShellTabComponentStates
> {
private _logTraces: Map<string, number>;
private _useMongoProxyEndpoint: boolean;
constructor(props: IMongoShellTabComponentProps) {
super(props);
this._logTraces = new Map();
this._useMongoProxyEndpoint = userContext.features.enableLegacyMongoShell;
// this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
this.state = {
url: getMongoShellUrl(),
url: getMongoShellUrl(this._useMongoProxyEndpoint),
};
props.onMongoShellTabAccessor({
@ -119,9 +121,10 @@ export default class MongoShellTabComponent extends Component<
) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.props.collection.databaseId;
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 targetOrigin = getMongoShellOrigin();
shellIframe.contentWindow.postMessage(
{
@ -137,7 +140,7 @@ export default class MongoShellTabComponent extends Component<
apiEndpoint: apiEndpoint,
},
},
targetOrigin,
window.origin,
);
}
@ -218,7 +221,7 @@ export default class MongoShellTabComponent extends Component<
name="explorer"
className="iframe"
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}
onLoad={(event) => this.setContentFocus(event)}
title="Mongo Shell"

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { extractFeatures } from "Platform/Hosted/extractFeatures";
import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
import { updateUserContext, userContext } from "../../../UserContext";
import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl";
import { getMongoShellUrl } from "./getMongoShellUrl";
const mongoBackendEndpoint = "https://localhost:1234";
const hostedExplorerURL = "https://cosmos.azure.com/";
describe("getMongoShellUrl", () => {
let queryString = "";
@ -13,6 +13,7 @@ describe("getMongoShellUrl", () => {
updateConfigContext({
BACKEND_ENDPOINT: mongoBackendEndpoint,
hostedExplorerURL: hostedExplorerURL,
platform: Platform.Hosted,
});
@ -32,175 +33,18 @@ describe("getMongoShellUrl", () => {
cassandraEndpoint: "fakeCassandraEndpoint",
},
},
features: extractFeatures(
new URLSearchParams({
"feature.enableLegacyMongoShellV1": "false",
"feature.enableLegacyMongoShellV2": "false",
"feature.enableLegacyMongoShellV1Debug": "false",
"feature.enableLegacyMongoShellV2Debug": "false",
"feature.loadLegacyMongoShellFromBE": "false",
}),
),
portalEnv: "prod",
});
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
});
it("should return /mongoshell/indexv2.html by default", () => {
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
it("should return /indexv2.html by default", () => {
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
});
it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => {
updateUserContext({
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");
it("should return /index.html when useMongoProxyEndpoint is true", () => {
const useMongoProxyEndpoint: boolean = true;
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
});
});

View File

@ -1,45 +1,15 @@
import { configContext, Platform } from "../../../ConfigContext";
import { configContext } from "ConfigContext";
import { userContext } from "../../../UserContext";
export function getMongoShellUrl(): string {
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL {
const { databaseAccount: account } = userContext;
const resourceId = account?.id;
const accountName = account?.name;
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
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 `/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;
return new URL(path, configContext.hostedExplorerURL);
}

View File

@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
@ -393,6 +394,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.EXECUTE_ITEM,
onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick,
@ -408,6 +410,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
@ -437,7 +440,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
hasPopup: false,
};
const launchCopilotButton = {
const launchCopilotButton: CommandButtonComponentProps = {
iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat,
@ -450,9 +453,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}
if (this.props.copilotEnabled) {
const toggleCopilotButton = {
const toggleCopilotButton: CommandButtonComponentProps = {
iconSrc: QueryCommandIcon,
iconAlt: "Copilot",
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive);
},
@ -468,6 +472,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: CancelQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: () => this.queryAbortController.abort(),
commandButtonLabel: label,
ariaLabel: label,

View File

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

View File

@ -6,6 +6,7 @@ import { IpRule } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
@ -13,6 +14,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
import { userContext } from "UserContext";
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
@ -41,6 +43,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
showMongoAndCassandraProxiesNetworkSettingsWarningState,
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
] = 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 (
<div className="tabsManagerContainer">
{networkSettingsWarning && (
@ -297,6 +309,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
};
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
// React tabs have no context buttons.
useCommandBar.getState().setContextButtons([]);
// eslint-disable-next-line no-console
switch (activeReactTab) {
case ReactTabKind.Connect:
@ -325,7 +340,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if (
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
(userContext.apiType === "Cassandra" &&
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
ipRules?.length

View File

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

View File

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

157
src/KeyboardShortcuts.tsx Normal file
View 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[];
}

View File

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

View File

@ -31,11 +31,6 @@ export type Features = {
readonly mongoProxyAPIs?: string;
readonly enableThroughputCap: boolean;
readonly enableHierarchicalKeys: boolean;
readonly enableLegacyMongoShellV1: boolean;
readonly enableLegacyMongoShellV1Debug: boolean;
readonly enableLegacyMongoShellV2: boolean;
readonly enableLegacyMongoShellV2Debug: boolean;
readonly loadLegacyMongoShellFromBE: boolean;
readonly enableCopilot: boolean;
readonly copilotVersion?: string;
readonly disableCopilotPhoenixGateaway: boolean;
@ -43,6 +38,7 @@ export type Features = {
readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
readonly enableLegacyMongoShell: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@ -106,11 +102,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"),
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"),
copilotVersion: get("copilotversion") ?? "v2.0",
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
@ -118,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
enableLegacyMongoShell: "true" === get("enablelegacymongoshell"),
};
}

View File

@ -82,7 +82,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
};
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,

View File

@ -1,3 +1,4 @@
import { clamp } from "@fluentui/react";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels";
@ -29,6 +30,11 @@ export interface TabsState {
setQueryCopilotTabInitialInput: (input: string) => void;
setIsTabExecuting: (state: boolean) => void;
setIsQueryErrorThrown: (state: boolean) => void;
getCurrentTabIndex: () => number;
selectTabByIndex: (index: number) => void;
selectLeftTab: () => void;
selectRightTab: () => void;
closeActiveTab: () => void;
}
export enum ReactTabKind {
@ -175,4 +181,44 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
setIsQueryErrorThrown: (state: boolean) => {
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 />
<add name="X-Xss-Protection" value="1; mode=block" />
<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>
<redirectHeaders>
<clear />