Compare commits

..

15 Commits

Author SHA1 Message Date
Senthamil Sindhu
10a8505b9a Support data plane RBAC 2024-06-14 12:12:30 -07:00
Senthamil Sindhu
ef7c2fe2f7 Remove dev endpoint 2024-04-10 11:59:57 -07:00
Senthamil Sindhu
4c7aca95e1 Merge branch 'users/aisayas/mp-cp-activate-prod' of https://github.com/Azure/cosmos-explorer into users/sindhuba/activate-prod 2024-04-09 12:27:51 -07:00
Senthamil Sindhu
2243ad895a Remove prod endpoint 2024-04-09 12:16:13 -07:00
Senthamil Sindhu
b2d5f91fe1 Remove prod 2024-04-09 11:22:17 -07:00
Asier Isayas
a712193477 fix pr check tests 2024-04-09 11:43:24 -04:00
Senthamil Sindhu
5ee411693c Add prod endpoint 2024-04-09 08:41:47 -07:00
Asier Isayas
16c7b2567b fix bug that blocked local mongo proxy and cassandra proxy development 2024-04-09 11:39:11 -04:00
Senthamil Sindhu
78d9a0cd8d Revert code 2024-04-08 16:20:40 -07:00
Senthamil Sindhu
c6ad538559 Run npm format and tests 2024-04-08 15:58:10 -07:00
Senthamil Sindhu
2bc09a6efe Add CP Prod endpoint 2024-04-08 15:37:19 -07:00
Senthamil Sindhu
d3a3033b25 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-04-08 15:32:50 -07:00
Asier Isayas
6bdc714e11 activate Mongo Proxy and Cassandra Proxy in Prod 2024-04-08 16:52:09 -04:00
Senthamil Sindhu
5042f28229 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-03-25 15:11:53 -07:00
Senthamil Sindhu
e1430fd06f Fix API endpoint for CassandraProxy query API 2024-03-18 10:25:17 -07:00
56 changed files with 1345 additions and 1862 deletions

View File

@@ -8,9 +8,6 @@ on:
pull_request:
branches:
- master
permissions:
id-token: write
contents: read
jobs:
codemetrics:
runs-on: ubuntu-latest
@@ -137,7 +134,7 @@ jobs:
runs-on: ubuntu-latest
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
strategy:
fail-fast: false
matrix:
@@ -148,18 +145,11 @@ jobs:
- ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
- ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts
steps:
- uses: actions/checkout@v4
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:

View File

@@ -9,10 +9,6 @@ on:
# Once every hour
- cron: "0 15 * * *"
permissions:
id-token: write
contents: read
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
@@ -20,17 +16,10 @@ jobs:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v2
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x
uses: actions/setup-node@v1
with:

View File

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

1888
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.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
"@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7",
"@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
@@ -46,7 +46,6 @@
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@xmldom/xmldom": "0.7.13",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",
@@ -55,7 +54,7 @@
"copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "7.8.5",
"d3": "6.1.1",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
@@ -70,14 +69,12 @@
"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",
@@ -101,12 +98,10 @@
"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",
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"underscore": "1.9.1",
"utility-types": "3.10.0",
"zustand": "3.5.0"
},
@@ -175,25 +170,25 @@
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "7.1.0",
"node-fetch": "2.6.7",
"node-fetch": "2.6.1",
"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": "12.0.1",
"react-dev-utils": "11.0.4",
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
"ts-loader": "9.2.4",
"typedoc": "0.22.15",
"typedoc": "0.21.5",
"typescript": "4.3.5",
"url-loader": "4.1.1",
"wait-on": "4.0.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.2"
"webpack-dev-server": "4.15.1"
},
"scripts": {
"postinstall": "patch-package",

View File

@@ -138,7 +138,7 @@ export class PortalBackendEndpoints {
}
export class MongoProxyEndpoints {
public static readonly Local: string = "https://localhost:7238";
public static readonly Development: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
@@ -179,6 +179,9 @@ export class CassandraProxyAPIs {
export class Queries {
public static CustomPageOption: string = "custom";
public static UnlimitedPageOption: string = "unlimited";
public static setAutomaticRBACOption: string = "Automatic";
public static setTrueRBACOption: string = "True";
public static setFalseRBACOption: string = "False";
public static itemsPerPage: number = 100;
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
public static containersPerPage: number = 50;

View File

@@ -672,27 +672,6 @@ 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> {
@@ -709,3 +688,24 @@ 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,7 +83,6 @@ 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/",
@@ -109,7 +108,6 @@ let configContext: Readonly<ConfigContext> = {
"updateDocument",
"deleteDocument",
"createCollectionWithProxy",
"legacyMongoShell",
],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,

View File

@@ -1,7 +1,6 @@
/**
* 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";
@@ -31,7 +30,7 @@ export interface CommandButtonComponentProps {
/**
* Click handler for command button click
*/
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
onCommandClick: (e: React.SyntheticEvent) => void;
/**
* Label for the button
@@ -108,17 +107,10 @@ 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

@@ -54,17 +54,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
const existingContent = this.editor.getModel().getValue();
if (this.props.content !== existingContent) {
if (this.props.isReadOnly) {
this.editor.setValue(this.props.content);
} else {
this.editor.pushUndoStop();
this.editor.executeEdits("", [
{
range: this.editor.getModel().getFullModelRange(),
text: this.props.content,
},
]);
}
this.editor.pushUndoStop();
this.editor.executeEdits("", [
{
range: this.editor.getModel().getFullModelRange(),
text: this.props.content,
},
]);
}
}

View File

@@ -5,7 +5,6 @@
*/
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";
@@ -41,7 +40,6 @@ 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 =
@@ -107,10 +105,6 @@ 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,4 +1,3 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
@@ -58,7 +57,6 @@ 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);
@@ -96,7 +94,6 @@ 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);
@@ -280,7 +277,6 @@ 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) {
@@ -301,7 +297,6 @@ 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);
@@ -317,7 +312,6 @@ 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);
@@ -343,7 +337,6 @@ 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);
@@ -363,7 +356,6 @@ 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);
@@ -383,7 +375,6 @@ 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);
@@ -406,7 +397,6 @@ 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,
@@ -421,7 +411,6 @@ 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,7 +7,6 @@ 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";
@@ -234,28 +233,3 @@ 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

@@ -41,6 +41,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption,
);
const [enableDataPlaneRBACOption, setEnableDataPlaneRBACOption] = useState<string>(
LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.Queries.setAutomaticRBACOption
? Constants.Queries.setAutomaticRBACOption
: LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.Queries.setTrueRBACOption
? Constants.Queries.setTrueRBACOption
: Constants.Queries.setFalseRBACOption
);
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
@@ -110,7 +117,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
StorageKey.ActualItemPerPage,
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryString(
StorageKey.DataPlaneRbacEnabled,
enableDataPlaneRBACOption
);
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
@@ -197,6 +211,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: Constants.PriorityLevel.High, text: "High" },
];
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
{ key: Constants.Queries.setAutomaticRBACOption, text: "Automatic" },
{ key: Constants.Queries.setTrueRBACOption, text: "True" },
{ key: Constants.Queries.setFalseRBACOption, text: "False"}
];
const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption,
@@ -208,6 +228,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setPageOption(option.key);
};
const handleOnDataPlaneRBACOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setEnableDataPlaneRBACOption(option.key);
};
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
setRUThresholdEnabled(checked);
};
@@ -361,6 +385,27 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</div>
</div>
)}
{(
<div className="settingsSection">
<div className="settingsSectionPart">
<fieldset>
<legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
Enable DataPlane RBAC
</legend>
<InfoTooltip>
Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable DataPlane RBAC
</InfoTooltip>
<ChoiceGroup
ariaLabelledBy="enableDataPlaneRBACOptions"
selectedKey={enableDataPlaneRBACOption}
options={dataPlaneRBACOptionsList}
styles={choiceButtonStyles}
onChange={handleOnDataPlaneRBACOptionChange}
/>
</fieldset>
</div>
</div>
)}
{userContext.apiType === "SQL" && (
<>
<div className="settingsSection">
@@ -630,7 +675,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
Enable sample database
<InfoTooltip>
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is
created by, and maintained by Microsoft at no cost to you.
</InfoTooltip>
</div>
@@ -640,7 +685,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for Query Advisor"
ariaLabel="Enable sample db for Copilot"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
/>

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

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
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";
@@ -463,22 +462,7 @@ export default class DocumentsTab extends TabsBase {
private initializeNewDocument = (): void => {
this.selectedDocumentId(null);
const newDocument: any = {
id: "replace_with_new_document_id",
};
this.partitionKeyProperties.forEach((partitionKeyProperty) => {
let target = newDocument;
const keySegments = partitionKeyProperty.split(".");
const finalSegment = keySegments.pop();
// Initialize nested objects as needed
keySegments.forEach((segment) => {
target = target[segment] = target[segment] || {};
});
target[finalSegment] = "replace_with_new_partition_key_value";
});
const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
@@ -909,7 +893,6 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: NewDocumentIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_ITEM,
onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -924,7 +907,6 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -939,7 +921,6 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -955,7 +936,6 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveExistingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -970,7 +950,6 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -986,7 +965,6 @@ 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

@@ -1,4 +1,3 @@
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
import React, { Component } from "react";
import * as Constants from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext";
@@ -10,6 +9,7 @@ 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
@@ -50,15 +50,13 @@ 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 = useMongoProxyEndpoint("legacyMongoShell");
this.state = {
url: getMongoShellUrl(this._useMongoProxyEndpoint),
url: getMongoShellUrl(),
};
props.onMongoShellTabAccessor({
@@ -121,10 +119,9 @@ export default class MongoShellTabComponent extends Component<
) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.props.collection.databaseId;
const collectionId = this.props.collection.id();
const apiEndpoint = this._useMongoProxyEndpoint
? configContext.MONGO_PROXY_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const apiEndpoint = configContext.BACKEND_ENDPOINT;
const encryptedAuthToken: string = userContext.accessToken;
const targetOrigin = getMongoShellOrigin();
shellIframe.contentWindow.postMessage(
{
@@ -140,7 +137,7 @@ export default class MongoShellTabComponent extends Component<
apiEndpoint: apiEndpoint,
},
},
window.origin,
targetOrigin,
);
}

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
import { extractFeatures } from "Platform/Hosted/extractFeatures";
import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext";
import { updateUserContext, userContext } from "../../../UserContext";
import { getMongoShellUrl } from "./getMongoShellUrl";
import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl";
const mongoBackendEndpoint = "https://localhost:1234";
const hostedExplorerURL = "https://cosmos.azure.com/";
describe("getMongoShellUrl", () => {
let queryString = "";
@@ -13,7 +13,6 @@ describe("getMongoShellUrl", () => {
updateConfigContext({
BACKEND_ENDPOINT: mongoBackendEndpoint,
hostedExplorerURL: hostedExplorerURL,
platform: Platform.Hosted,
});
@@ -33,18 +32,175 @@ 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 /indexv2.html by default", () => {
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
it("should return /mongoshell/indexv2.html by default", () => {
expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`);
});
it("should return /index.html when useMongoProxyEndpoint is true", () => {
const useMongoProxyEndpoint: boolean = true;
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.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");
});
});

View File

@@ -1,11 +1,45 @@
import { configContext, Platform } from "../../../ConfigContext";
import { userContext } from "../../../UserContext";
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
export function getMongoShellUrl(): string {
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}`;
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
if (userContext.features.enableLegacyMongoShellV1 === true) {
return `/mongoshell/index.html?${queryString}`;
}
if (userContext.features.enableLegacyMongoShellV1Debug === true) {
return `/mongoshell/debug/index.html?${queryString}`;
}
if (userContext.features.enableLegacyMongoShellV2 === true) {
return `/mongoshell/indexv2.html?${queryString}`;
}
if (userContext.features.enableLegacyMongoShellV2Debug === true) {
return `/mongoshell/debug/indexv2.html?${queryString}`;
}
if (userContext.portalEnv === "localhost") {
return `/mongoshell/indexv2.html?${queryString}`;
}
if (userContext.features.loadLegacyMongoShellFromBE === true) {
const extensionEndpoint: string = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT);
return `${extensionEndpoint}/content/mongoshell/debug/index.html?${queryString}`;
}
return `/mongoshell/indexv2.html?${queryString}`;
}
export function getExtensionEndpoint(platform: string, backendEndpoint: string): string {
const runtimeEndpoint = platform === Platform.Hosted ? backendEndpoint : "";
const extensionEndpoint: string = backendEndpoint || runtimeEndpoint || "";
return extensionEndpoint;
}

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
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";
@@ -322,7 +321,6 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -336,7 +334,6 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -350,7 +347,6 @@ 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,
@@ -364,7 +360,6 @@ 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,7 +6,6 @@ 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";
@@ -14,7 +13,6 @@ 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";
@@ -43,16 +41,6 @@ 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 && (
@@ -309,9 +297,6 @@ 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:
@@ -340,7 +325,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.Local) ||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
(userContext.apiType === "Cassandra" &&
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
ipRules?.length

View File

@@ -1,6 +1,5 @@
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";
@@ -219,18 +218,6 @@ 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";
@@ -240,7 +227,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -255,7 +241,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -271,7 +256,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this,
iconSrc: DiscardIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onDiscard,
commandButtonLabel: label,
ariaLabel: label,
@@ -303,6 +287,7 @@ 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,13 +1,12 @@
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";
@@ -81,7 +80,6 @@ export default class UserDefinedFunctionTabContent extends Component<
setState: this.setState,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -96,7 +94,6 @@ export default class UserDefinedFunctionTabContent extends Component<
...this,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -112,7 +109,6 @@ export default class UserDefinedFunctionTabContent extends Component<
...this,
iconSrc: DiscardIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: this.onDiscard,
commandButtonLabel: label,
ariaLabel: label,

View File

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

View File

@@ -1,5 +1,5 @@
{
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. You can configure your dedicated gateway cluster and choose which feature to deploy.",
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance.",
"DedicatedGateway": "Dedicated Gateway",
"Provisioned": "Provisioned",
"Deprovisioned": "Deprovisioned",
@@ -8,7 +8,6 @@
"DedicatedGatewayPricing": "Learn more about dedicated gateway pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"DedicatedGatewayTypePlaceHolder": "Select Dedicated Gateway Type",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
@@ -50,8 +49,6 @@
"MetricsText": "Monitor the CPU and memory usage for the dedicated gateway instances in ",
"MetricsBlade": "the metrics blade.",
"MonitorUsage": "Monitor Usage",
"DedicatedGatewayTypeText": "Each dedicated gateway feature comes with different benefits. ",
"DedicatedGatewayTypeLink": "Learn more about the dedicated gateway features.",
"ResizingDecisionText": "To understand if the dedicated gateway is the right size, ",
"ResizingDecisionLink": "learn more about dedicated gateway sizing.",
"WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.",

View File

@@ -21,7 +21,6 @@ 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";
@@ -92,54 +91,52 @@ const App: React.FunctionComponent = () => {
}
return (
<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 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>
)}
<Tabs explorer={explorer} />
</div>
{/* Collections Tree and Tabs - End */}
<div
className="dataExplorerErrorConsoleContainer"
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
>
<NotificationConsole />
</div>
</div>
)}
<Tabs explorer={explorer} />
</div>
{/* Collections Tree and Tabs - End */}
<div
className="dataExplorerErrorConsoleContainer"
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
>
<NotificationConsole />
</div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
</KeyboardShortcutRoot>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
);
};

View File

@@ -14,6 +14,7 @@ export type Features = {
readonly enableTtl: boolean;
readonly executeSproc: boolean;
readonly enableAadDataPlane: boolean;
readonly enableDataPlaneRbac: boolean;
readonly enableResourceGraph: boolean;
readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean;
@@ -31,6 +32,11 @@ 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;
@@ -69,6 +75,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
canExceedMaximumValue: "true" === get("canexceedmaximumvalue"),
cosmosdb: "true" === get("cosmosdb"),
enableAadDataPlane: "true" === get("enableaaddataplane"),
enableDataPlaneRbac: "true" === get("enabledataplanerbac"),
enableResourceGraph: "true" === get("enableresourcegraph"),
enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"),
enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"),
@@ -101,6 +108,11 @@ 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"),

View File

@@ -13,7 +13,7 @@ import {
UpdateDedicatedGatewayRequestParameters,
} from "./SqlxTypes";
const apiVersion = "2024-02-15-preview";
const apiVersion = "2021-04-01-preview";
export enum ResourceStatus {
Running = "Running",
@@ -24,7 +24,6 @@ export enum ResourceStatus {
export interface DedicatedGatewayResponse {
sku: string;
dedicatedGatewayType: string;
instances: number;
status: string;
endpoint: string;
@@ -34,16 +33,11 @@ export const getPath = (subscriptionId: string, resourceGroup: string, name: str
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/SqlDedicatedGateway`;
};
export const updateDedicatedGatewayResource = async (
sku: string,
dedicatedGatewayType: string,
instances: number,
): Promise<string> => {
export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise<string> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const body: UpdateDedicatedGatewayRequestParameters = {
properties: {
instanceSize: sku,
dedicatedGatewayType: dedicatedGatewayType,
instanceCount: instances,
serviceType: "SqlDedicatedGateway",
},
@@ -115,19 +109,12 @@ export const getCurrentProvisioningState = async (): Promise<DedicatedGatewayRes
const response = await getDedicatedGatewayResource();
return {
sku: response.properties.instanceSize,
dedicatedGatewayType: response.properties.dedicatedGatewayType,
instances: response.properties.instanceCount,
status: response.properties.status,
endpoint: response.properties.sqlxEndPoint,
};
} catch (e) {
return {
sku: undefined,
dedicatedGatewayType: undefined,
instances: undefined,
status: undefined,
endpoint: undefined,
};
return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined };
}
};

View File

@@ -70,17 +70,6 @@ const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInpu
return currentValues;
};
const IntegratedCache = "IntegratedCache";
const DistributedQuery = "DistributedQuery";
const onDedicatedGatewayTypeChange = (
newValue: InputType,
currentValues: Map<string, SmartUiInput>,
): Map<string, SmartUiInput> => {
currentValues.set("dedicatedGatewayType", { value: newValue });
return currentValues;
};
const onNumberOfInstancesChange = (
newValue: InputType,
currentValues: Map<string, SmartUiInput>,
@@ -120,7 +109,6 @@ const onEnableDedicatedGatewayChange = (
const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean;
if (dedicatedGatewayOriginallyEnabled === newValue) {
currentValues.set("sku", baselineValues.get("sku"));
currentValues.set("dedicatedGatewayType", baselineValues.get("dedicatedGatewayType"));
currentValues.set("instances", baselineValues.get("instances"));
currentValues.set("costPerHour", baselineValues.get("costPerHour"));
currentValues.set("warningBanner", baselineValues.get("warningBanner"));
@@ -161,7 +149,6 @@ const onEnableDedicatedGatewayChange = (
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
}
const sku = currentValues.get("sku");
const dedicatedGatewayType = currentValues.get("dedicatedGatewayType");
const instances = currentValues.get("instances");
const hideAttributes = newValue === undefined || !(newValue as boolean);
currentValues.set("sku", {
@@ -169,11 +156,6 @@ const onEnableDedicatedGatewayChange = (
hidden: hideAttributes,
disabled: dedicatedGatewayOriginallyEnabled,
});
currentValues.set("dedicatedGatewayType", {
value: dedicatedGatewayType.value,
hidden: hideAttributes,
disabled: dedicatedGatewayOriginallyEnabled,
});
currentValues.set("instances", {
value: instances.value,
hidden: hideAttributes,
@@ -203,15 +185,6 @@ const getSkus = async (): Promise<ChoiceItem[]> => {
return skuDropDownItems;
};
const dedicatedGatewayTypeDropDownItems: ChoiceItem[] = [
{ labelTKey: "Integrated Cache", key: IntegratedCache },
{ labelTKey: "Distributed Query", key: DistributedQuery },
];
const getDedicatedGatewayType = async (): Promise<ChoiceItem[]> => {
return dedicatedGatewayTypeDropDownItems;
};
const getInstancesMin = async (): Promise<number> => {
return 1;
};
@@ -220,14 +193,6 @@ const getInstancesMax = async (): Promise<number> => {
return 5;
};
const DedicatedGatewayTypeDropdownInfo: Info = {
messageTKey: "DedicatedGatewayTypeText",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-size",
textTKey: "DedicatedGatewayTypeLink",
},
};
const NumberOfInstancesDropdownInfo: Info = {
messageTKey: "ResizingDecisionText",
link: {
@@ -347,9 +312,8 @@ export default class SqlX extends SelfServeBaseClass {
};
} else {
const sku = currentValues.get("sku")?.value as string;
const dedicatedGatewayType = currentValues.get("dedicatedGatewayType")?.value as string;
const instances = currentValues.get("instances").value as number;
const operationStatusUrl = await updateDedicatedGatewayResource(sku, dedicatedGatewayType, instances);
const operationStatusUrl = await updateDedicatedGatewayResource(sku, instances);
return {
operationStatusUrl: operationStatusUrl,
portalNotification: {
@@ -370,9 +334,8 @@ export default class SqlX extends SelfServeBaseClass {
}
} else {
const sku = currentValues.get("sku")?.value as string;
const dedicatedGatewayType = currentValues.get("dedicatedGatewayType")?.value as string;
const instances = currentValues.get("instances").value as number;
const operationStatusUrl = await updateDedicatedGatewayResource(sku, dedicatedGatewayType, instances);
const operationStatusUrl = await updateDedicatedGatewayResource(sku, instances);
return {
operationStatusUrl: operationStatusUrl,
portalNotification: {
@@ -398,7 +361,6 @@ export default class SqlX extends SelfServeBaseClass {
const defaults = new Map<string, SmartUiInput>();
defaults.set("enableDedicatedGateway", { value: false });
defaults.set("sku", { value: CosmosD4s, hidden: true });
defaults.set("dedicatedGatewayType", { value: IntegratedCache, hidden: true });
defaults.set("instances", { value: await getInstancesMin(), hidden: true });
defaults.set("costPerHour", undefined);
defaults.set("connectionString", undefined);
@@ -416,7 +378,6 @@ export default class SqlX extends SelfServeBaseClass {
if (response.status && response.status !== "Deleting") {
defaults.set("enableDedicatedGateway", { value: true });
defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("dedicatedGatewayType", { value: response.dedicatedGatewayType || IntegratedCache, disabled: true });
defaults.set("instances", { value: response.instances, disabled: false });
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
defaults.set("connectionString", {
@@ -458,15 +419,6 @@ export default class SqlX extends SelfServeBaseClass {
})
enableDedicatedGateway: boolean;
@OnChange(onDedicatedGatewayTypeChange)
@PropertyInfo(DedicatedGatewayTypeDropdownInfo)
@Values({
labelTKey: "Dedicated Gateway Features",
choices: getDedicatedGatewayType,
placeholderTKey: "DedicatedGatewayTypePlaceHolder",
})
dedicatedGatewayType: ChoiceItem;
@OnChange(onSKUChange)
@Values({
labelTKey: "SKUs",

View File

@@ -10,7 +10,6 @@ export type SqlxServiceProps = {
creationTime: string;
status: string;
instanceSize: string;
dedicatedGatewayType: string;
instanceCount: number;
sqlxEndPoint: string;
};
@@ -27,7 +26,6 @@ export type UpdateDedicatedGatewayRequestParameters = {
export type UpdateDedicatedGatewayRequestProperties = {
instanceSize: string;
dedicatedGatewayType: string;
instanceCount: number;
serviceType: string;
};

View File

@@ -5,6 +5,9 @@ import * as StringUtility from "./StringUtility";
export { LocalStorageUtility, SessionStorageUtility };
export enum StorageKey {
ActualItemPerPage,
DataPlaneRbacEnabled,
DataPlaneRbacDisabled,
isDataPlaneRbacAutomatic,
RUThresholdEnabled,
RUThreshold,
QueryTimeoutEnabled,

View File

@@ -101,6 +101,7 @@ interface UserContext {
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
readonly dataPlaneRbacEnabled?: boolean;
}
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";

View File

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

View File

@@ -4,6 +4,7 @@ import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesCon
import Explorer from "Explorer/Explorer";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
@@ -270,8 +271,30 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
}
}
try {
if (!account.properties.disableLocalAuth) {
keys = await listKeys(subscriptionId, resourceGroup, account.name);
if(LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
var isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
if (isDataPlaneRbacSetting == "Automatic")
{
if (!account.properties.disableLocalAuth) {
keys = await listKeys(subscriptionId, resourceGroup, account.name);
}
else {
updateUserContext({
dataPlaneRbacEnabled: true
});
}
}
else if(isDataPlaneRbacSetting == "True") {
updateUserContext({
dataPlaneRbacEnabled: true
});
}
else {
keys = await listKeys(subscriptionId, resourceGroup, account.name);
updateUserContext({
dataPlaneRbacEnabled: false
});
}
}
} catch (e) {
if (userContext.features.enableAadDataPlane) {
@@ -393,8 +416,9 @@ async function configurePortal(): Promise<Explorer> {
updateUserContext({
authType: AuthType.AAD,
});
let explorer: Explorer;
return new Promise((resolve) => {
return new Promise(async (resolve) => {
// In development mode, try to load the iframe message from session storage.
// This allows webpack hot reload to function properly in the portal
if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) {
@@ -407,6 +431,7 @@ async function configurePortal(): Promise<Explorer> {
console.dir(message);
updateContextsFromPortalMessage(message);
explorer = new Explorer();
// In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
@@ -415,11 +440,11 @@ async function configurePortal(): Promise<Explorer> {
resolve(explorer);
}
}
// In the Portal, configuration of Explorer happens via iframe message
window.addEventListener(
"message",
(event) => {
async (event) => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
@@ -449,6 +474,37 @@ async function configurePortal(): Promise<Explorer> {
setTimeout(() => explorer.openNPSSurveyDialog(), 3000);
}
let dbAccount = userContext.databaseAccount;
let keys: DatabaseAccountListKeysResult = {};
const account = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
if(LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
var isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
if (isDataPlaneRbacSetting == "Automatic")
{
if (!account.properties.disableLocalAuth) {
keys = await listKeys(subscriptionId, resourceGroup, account.name);
}
else {
updateUserContext({
dataPlaneRbacEnabled: true
});
}
}
else if(isDataPlaneRbacSetting == "True") {
updateUserContext({
dataPlaneRbacEnabled: true
});
}
else {
keys = await listKeys(subscriptionId, resourceGroup, account.name);
updateUserContext({
dataPlaneRbacEnabled: false
});
}
}
if (openAction) {
handleOpenAction(openAction, useDatabases.getState().databases, explorer);
}
@@ -469,9 +525,11 @@ async function configurePortal(): Promise<Explorer> {
},
false,
);
sendReadyMessage();
});
}
function shouldForwardMessage(message: PortalMessage, messageOrigin: string) {

View File

@@ -1,4 +1,3 @@
import { clamp } from "@fluentui/react";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels";
@@ -30,11 +29,6 @@ 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 {
@@ -181,44 +175,4 @@ 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

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000);
test("Cassandra keyspace and table CRUD", async () => {
const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner");
await page.waitForSelector("iframe");
const explorer = await waitForExplorer();

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000);
test("Graph CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner");
const explorer = await waitForExplorer();
// Create new database and graph

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000);
test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
const explorer = await waitForExplorer();
// Create new database and collection

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000);
test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
const explorer = await waitForExplorer();
// Create new database and collection

View File

@@ -1,10 +1,5 @@
import { getAzureCLICredentialsToken } from "../utils/shared";
test("Self Serve", async () => {
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`);
await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000);
test("SQL CRUD", async () => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
const explorer = await waitForExplorer();
await explorer.click('[data-test="New Container"]');

View File

@@ -1,15 +1,19 @@
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { CosmosClient, PermissionMode } from "@azure/cosmos";
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateUniqueName, getAzureCLICredentials } from "../utils/shared";
import { generateUniqueName } from "../utils/shared";
jest.setTimeout(120000);
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? "";
const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4";
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners";
test("Resource token", async () => {
const credentials = await getAzureCLICredentials();
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");

View File

@@ -1,17 +1,15 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000);
test("Tables CRUD", async () => {
const tableId = generateUniqueName("table");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
const explorer = await waitForExplorer();
await page.waitForSelector('text="Querying databases"', { state: "detached" });

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-console */
import { ClientSecretCredential } from "@azure/identity";
import "../../less/hostedexplorer.less";
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
import { updateUserContext } from "../../src/UserContext";
@@ -10,13 +11,29 @@ const urlSearchParams = new URLSearchParams(window.location.search);
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
const selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
const token = urlSearchParams.get("token");
if (!process.env.AZURE_CLIENT_SECRET) {
throw new Error(
"process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server",
);
}
// Azure SDK clients accept the credential as a parameter
const credentials = new ClientSecretCredential(
process.env.AZURE_TENANT_ID,
process.env.AZURE_CLIENT_ID,
process.env.AZURE_CLIENT_SECRET,
{
authorityHost: "https://localhost:1234",
},
);
console.log("Resource Group:", resourceGroup);
console.log("Subcription: ", subscriptionId);
console.log("Account Name: ", accountName);
const initTestExplorer = async (): Promise<void> => {
const { token } = await credentials.getToken("https://management.azure.com//.default");
updateUserContext({
authorizationToken: `bearer ${token}`,
});
@@ -35,9 +52,6 @@ const initTestExplorer = async (): Promise<void> => {
dnsSuffix: "documents.azure.com",
serverId: "prod1",
extensionEndpoint: "/proxy",
portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com",
cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com",
subscriptionType: 3,
quotaId: "Internal_2014-09-01",
isTryCosmosDBSubscription: false,

View File

@@ -1,4 +1,3 @@
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
import crypto from "crypto";
export function generateUniqueName(baseName = "", length = 4): string {
@@ -8,13 +7,3 @@ export function generateUniqueName(baseName = "", length = 4): string {
export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
}
export async function getAzureCLICredentials(): Promise<AzureCliCredentials> {
return await AzureCliCredentials.create();
}
export async function getAzureCLICredentialsToken(): Promise<string> {
const credentials = await getAzureCLICredentials();
const token = (await credentials.getToken()).accessToken;
return token;
}

View File

@@ -2,7 +2,10 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
const ms = require("ms");
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"];
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners";
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
@@ -16,7 +19,7 @@ function friendlyTime(date) {
}
async function main() {
const credentials = await msRestNodeAuth.AzureCliCredentials.create();
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const client = new CosmosDBManagementClient(credentials, subscriptionId);
const accounts = await client.databaseAccounts.list(resourceGroupName);
for (const account of accounts) {
@@ -35,7 +38,7 @@ async function main() {
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
resourceGroupName,
account.name,
account.name
);
for (const database of cassandraDatabases) {
const timestamp = Number(database.resource._ts) * 1000;

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 cosmos-explorer-preview.azurewebsites.net" />
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net" />
</customHeaders>
<redirectHeaders>
<clear />