Merge branch 'master' into users/tarazou/UpdateFetchPriceAPI

This commit is contained in:
Tara Zou 2024-04-26 16:24:48 -04:00
commit e2d34d5131
61 changed files with 2030 additions and 1778 deletions

View File

@ -8,6 +8,9 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
permissions:
id-token: write
contents: read
jobs: jobs:
codemetrics: codemetrics:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -134,7 +137,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -145,11 +148,18 @@ jobs:
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts - ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.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/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:
- uses: actions/checkout@v4 - 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 - name: Use Node.js 18.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:

View File

@ -9,6 +9,10 @@ on:
# Once every hour # Once every hour
- cron: "0 15 * * *" - 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 # A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:
# This workflow contains a single job called "build" # This workflow contains a single job called "build"
@ -16,10 +20,17 @@ jobs:
name: "Cleanup Test Database Accounts" name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps: steps:
- uses: actions/checkout@v2 - 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 - name: Use Node.js 18.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:

View File

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

View File

@ -2296,6 +2296,17 @@ a:link {
display: none !important; display: none !important;
} }
.monaco-editor .quick-input-list-label {
/* Restore some of Monaco's default styles that are clobbered by our global styles */
padding: 0;
line-height: 22px;
}
.monaco-editor .quick-input-list .highlight {
/* Padding in highlighted text within the quick input list breaks the flow of the text */
padding: 0;
}
td a { td a {
color: #393939; color: #393939;
} }

1902
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -124,8 +124,9 @@ export enum MongoBackendEndpointType {
remote, remote,
} }
export enum BackendApi { export class BackendApi {
GenerateToken, public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings";
} }
export class PortalBackendEndpoints { export class PortalBackendEndpoints {
@ -137,7 +138,7 @@ export class PortalBackendEndpoints {
} }
export class MongoProxyEndpoints { export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238"; public static readonly Local: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";

View File

@ -67,7 +67,7 @@ export function queryDocuments(
query: string, query: string,
continuationToken?: string, continuationToken?: string,
): Promise<QueryResponse> { ): Promise<QueryResponse> {
if (!useMongoProxyEndpoint("resourcelist")) { if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken); return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
} }
@ -106,7 +106,7 @@ export function queryDocuments(
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken; headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
} }
const path = isResourceList ? "/resourcelist" : ""; const path = isResourceList ? "/resourcelist" : "/queryDocuments";
return window return window
.fetch(`${endpoint}${path}`, { .fetch(`${endpoint}${path}`, {
@ -672,6 +672,27 @@ export function getEndpoint(endpoint: string): string {
return url; return url;
} }
export function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
);
}
// TODO: This function throws most of the time except on Forbidden which is a bit strange // TODO: This function throws most of the time except on Forbidden which is a bit strange
// It causes problems for TypeScript understanding the types // It causes problems for TypeScript understanding the types
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> { async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
@ -688,17 +709,3 @@ async function errorHandling(response: Response, action: string, params: unknown
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
} }
function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (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

@ -86,6 +86,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`, `^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",
@ -105,24 +106,20 @@ let configContext: Readonly<ConfigContext> = {
JUNO_ENDPOINT: JunoEndpoints.Prod, JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
NEW_BACKEND_APIS: [BackendApi.GenerateToken],
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
NEW_MONGO_APIS: [ NEW_MONGO_APIS: [
// "resourcelist", "resourcelist",
// "createDocument", "queryDocuments",
// "readDocument", "createDocument",
// "updateDocument", "readDocument",
// "deleteDocument", "updateDocument",
// "createCollectionWithProxy", "deleteDocument",
"createCollectionWithProxy",
"legacyMongoShell",
], ],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
NEW_CASSANDRA_APIS: [ NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
// "postQuery",
// "createOrDelete",
// "getKeys",
// "getSchema",
],
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
isTerminalEnabled: false, isTerminalEnabled: false,
isPhoenixEnabled: false, isPhoenixEnabled: false,

View File

@ -47,6 +47,7 @@ export enum MessageTypes {
GetAllResourceTokens, // Data Explorer -> Fabric GetAllResourceTokens, // Data Explorer -> Fabric
Ready, // Data Explorer -> Fabric Ready, // Data Explorer -> Fabric
OpenCESCVAFeedbackBlade, OpenCESCVAFeedbackBlade,
ActivateTab,
} }
export interface AuthorizationToken { export interface AuthorizationToken {

View File

@ -387,6 +387,7 @@ export interface DataExplorerInputsFrame {
dnsSuffix?: string; dnsSuffix?: string;
serverId?: string; serverId?: string;
extensionEndpoint?: string; extensionEndpoint?: string;
portalBackendEndpoint?: string;
mongoProxyEndpoint?: string; mongoProxyEndpoint?: string;
cassandraProxyEndpoint?: string; cassandraProxyEndpoint?: string;
subscriptionType?: SubscriptionType; subscriptionType?: SubscriptionType;

View File

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

View File

@ -46,9 +46,25 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
}, 100); }, 100);
} }
public componentDidUpdate(previous: EditorReactProps) { public componentDidUpdate() {
if (this.props.content !== previous.content) { if (!this.editor) {
this.editor?.setValue(this.props.content); return;
}
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,
},
]);
}
} }
} }
@ -71,9 +87,14 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor; this.editor = editor;
const queryEditorModel = this.editor.getModel();
if (!this.props.isReadOnly && this.props.onContentChanged) { if (!this.props.isReadOnly && this.props.onContentChanged) {
queryEditorModel.onDidChangeContent(() => { // Hooking the model's onDidChangeContent event because of some event ordering issues.
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
// then there are some inconsistencies as to which event fires first.
// But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event.
// (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.)
// If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first.
this.editor.onDidChangeModelContent(() => {
const queryEditorModel = this.editor.getModel(); const queryEditorModel = this.editor.getModel();
this.props.onContentChanged(queryEditorModel.getValue()); this.props.onContentChanged(queryEditorModel.getValue());
}); });

View File

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

View File

@ -2,10 +2,8 @@ import * as ko from "knockout";
import { AuthType } from "../../../AuthType"; import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels"; import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels"; import { CollectionBase } from "../../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import { useNotebook } from "../../Notebook/useNotebook"; import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@ -72,181 +70,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
}); });
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
portalEnv: "prod",
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
});
afterEach(() => {
updateUserContext({
portalEnv: "prod",
});
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Notebooks is already enabled - button should be hidden", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
it("Account is running on one of the national clouds - button should be hidden", () => {
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
//TODO: modify once notebooks are available
expect(enableNotebookBtn).toBeUndefined();
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(false);
//expect(enableNotebookBtn.tooltipText).toBe("");
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
//TODO: modify once notebooks are available
expect(enableNotebookBtn).toBeUndefined();
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(true);
//expect(enableNotebookBtn.tooltipText).toBe(
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//);
});
});
describe("Open Mongo shell button", () => {
const openMongoShellBtnLabel = "Open Mongo shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
});
afterAll(() => {
updateUserContext({
apiType: "SQL",
});
useNotebook.getState().setIsShellEnabled(false);
});
beforeEach(() => {
updateUserContext({
apiType: "Mongo",
});
useNotebook.getState().setIsShellEnabled(true);
});
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Mongo Api not available - button should be hidden", () => {
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Running on a national cloud - button should be hidden", () => {
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is available - button should be hidden", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
useNotebook.getState().setIsShellEnabled(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
});
describe("Open Cassandra shell button", () => { describe("Open Cassandra shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra shell"; const openCassandraShellBtnLabel = "Open Cassandra shell";
const selectedNodeState = useSelectedNode.getState(); const selectedNodeState = useSelectedNode.getState();
@ -305,42 +128,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined(); expect(openCassandraShellBtn).toBeUndefined();
}); });
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
});
}); });
describe("Open Postgres and vCore Mongo buttons", () => { describe("Open Postgres and vCore Mongo buttons", () => {
@ -368,62 +155,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
}); });
describe("GitHub buttons", () => {
const connectToGitHubBtnLabel = "Connect to GitHub";
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
});
afterEach(() => {
jest.resetAllMocks();
useNotebook.getState().setIsNotebookEnabled(false);
});
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeDefined();
});
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
);
expect(manageGitHubSettingsBtn).toBeDefined();
});
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeUndefined();
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
);
expect(manageGitHubSettingsBtn).toBeUndefined();
});
});
describe("Resource token", () => { describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase; const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection); useSelectedNode.getState().setSelectedNode(mockCollection);

View File

@ -1,3 +1,4 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
@ -7,14 +8,10 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg"; import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg"; import AddUdfIcon from "../../../../images/AddUdf.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg"; import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import HomeIcon from "../../../../images/Home_16.svg"; import HomeIcon from "../../../../images/Home_16.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg"; import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg"; import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import GitHubIcon from "../../../../images/github.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg"; import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg"; import SynapseIcon from "../../../../images/synapse-link.svg";
@ -22,7 +19,6 @@ import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { Platform, configContext } from "../../../ConfigContext"; import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
@ -33,7 +29,6 @@ import { useNotebook } from "../../Notebook/useNotebook";
import { OpenFullScreen } from "../../OpenFullScreen"; import { OpenFullScreen } from "../../OpenFullScreen";
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel"; import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane"; import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
@ -63,6 +58,7 @@ export function createStaticCommandBarButtons(
buttons.push(homeBtn); buttons.push(homeBtn);
const newCollectionBtn = createNewCollectionGroup(container); const newCollectionBtn = createNewCollectionGroup(container);
newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below.
buttons.push(newCollectionBtn); buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") { if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container); const addSynapseLink = createOpenSynapseLinkDialogButton(container);
@ -80,57 +76,6 @@ export function createStaticCommandBarButtons(
} }
} }
if (useNotebook.getState().isNotebookEnabled) {
addDivider();
const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
notebookButtons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container));
}
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
notebookButtons.push(createOpenTerminalButton(container));
}
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
if (
(userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
} else {
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
}
}
notebookButtons.forEach((btn) => {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
} else if (!useNotebook.getState().isPhoenixNotebooks) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
buttons.push(btn);
});
}
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) { if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
@ -151,6 +96,7 @@ export function createStaticCommandBarButtons(
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@ -240,7 +186,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
buttons.push(fullScreenButton); buttons.push(fullScreenButton);
} }
if (configContext.platform !== Platform.Emulator) { if (configContext.platform === Platform.Portal) {
const label = "Feedback"; const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = { const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon, iconSrc: FeedbackIcon,
@ -334,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
return { return {
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_DATABASE,
onCommandClick: async () => { onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) { if (throughputCap && throughputCap !== -1) {
@ -354,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@ -369,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
@ -394,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_SPROC,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
@ -413,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newUserDefinedFunctionBtn: CommandButtonComponentProps = { const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon, iconSrc: AddUdfIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_UDF,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
@ -432,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
const newTriggerBtn: CommandButtonComponentProps = { const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon, iconSrc: AddTriggerIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.NEW_TRIGGER,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
@ -449,45 +401,12 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
return buttons; return buttons;
} }
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
id: "newNotebookBtn",
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onNewNotebookClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.openUploadFilePanel(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps { function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query"; const label = "Open Query";
return { return {
iconSrc: BrowseQueriesIcon, iconSrc: BrowseQueriesIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY,
onCommandClick: () => onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />), useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label, commandButtonLabel: label,
@ -502,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
return { return {
iconSrc: OpenQueryFromDiskIcon, iconSrc: OpenQueryFromDiskIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />), onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@ -510,19 +430,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
}; };
} }
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
iconAlt: label,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createOpenTerminalButtonByKind( function createOpenTerminalButtonByKind(
container: Explorer, container: Explorer,
terminalKind: ViewModels.TerminalKind, terminalKind: ViewModels.TerminalKind,
@ -562,45 +469,6 @@ function createOpenTerminalButtonByKind(
}; };
} }
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
iconAlt: label,
onCommandClick: () => container.resetNotebookWorkspace(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
const junoClient = new JunoClient();
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
useSidePanel
.getState()
.openSidePanel(
label,
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>,
);
},
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createStaticCommandBarButtonsForResourceToken( function createStaticCommandBarButtonsForResourceToken(
container: Explorer, container: Explorer,
selectedNodeState: SelectedNodeState, selectedNodeState: SelectedNodeState,

View File

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

View File

@ -1,4 +1,5 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { useDatabases } from "Explorer/useDatabases";
import React from "react"; import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts"; import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@ -40,97 +41,112 @@ function openCollectionTab(
databases: ViewModels.Database[], databases: ViewModels.Database[],
initialDatabaseIndex = 0, initialDatabaseIndex = 0,
) { ) {
for (let i = initialDatabaseIndex; i < databases.length; i++) { //if databases are not yet loaded, wait until loaded
const database: ViewModels.Database = databases[i]; if (!databases || databases.length === 0) {
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) { const databaseActionHandler = (databases: ViewModels.Database[]) => {
continue; databasesUnsubscription();
} openCollectionTab(action, databases, 0);
return;
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
if (!action.collectionResourceId && collections.length === 0) {
subscription.dispose();
openCollectionTab(action, databases, ++i);
return;
}
for (let j = 0; j < collections.length; j++) {
const collection: ViewModels.Collection = collections[j];
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
continue;
}
// select the collection
collection.expandCollection();
if (
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
) {
collection.onDocumentDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
) {
collection.onMongoDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.TableEntities ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
) {
collection.onTableEntitiesClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.Graph ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
) {
collection.onGraphDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SQLQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) {
collection.onNewQueryClick(
collection,
undefined,
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
);
break;
}
if (
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
) {
collection.onSettingsClick();
break;
}
}
subscription.dispose();
}; };
const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases);
} else {
for (let i = initialDatabaseIndex; i < databases.length; i++) {
const database: ViewModels.Database = databases[i];
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
continue;
}
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections)); //expand database first if not expanded to load the collections
if (database.collections && database.collections() && database.collections().length) { if (!database.isDatabaseExpanded?.()) {
collectionActionHandler(database.collections()); database.expandDatabase?.();
}
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
if (!action.collectionResourceId && collections.length === 0) {
subscription.dispose();
openCollectionTab(action, databases, ++i);
return;
}
for (let j = 0; j < collections.length; j++) {
const collection: ViewModels.Collection = collections[j];
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
continue;
}
// select the collection
collection.expandCollection();
if (
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
) {
collection.onDocumentDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
) {
collection.onMongoDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.TableEntities ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
) {
collection.onTableEntitiesClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.Graph ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
) {
collection.onGraphDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SQLQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) {
collection.onNewQueryClick(
collection,
undefined,
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
);
break;
}
if (
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
) {
collection.onSettingsClick();
break;
}
}
subscription.dispose();
};
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections));
if (database.collections && database.collections() && database.collections().length) {
collectionActionHandler(database.collections());
}
break;
} }
break;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Areas, Areas,
BackendApi,
ConnectionStatusType, ConnectionStatusType,
ContainerStatusType, ContainerStatusType,
HttpStatusCodes, HttpStatusCodes,
@ -30,6 +31,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@ -80,7 +82,11 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
}; };
export const getCopilotEnabled = async (): Promise<boolean> => { export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`; const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
? configContext.PORTAL_BACKEND_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token }; const headers = { [authorizationHeader.header]: authorizationHeader.token };

View File

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

View File

@ -3,6 +3,7 @@ import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
@ -19,7 +20,6 @@ import Explorer from "../Explorer";
import * as TableConstants from "./Constants"; import * as TableConstants from "./Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import * as TableEntityProcessor from "./TableEntityProcessor"; import * as TableEntityProcessor from "./TableEntityProcessor";
import { CassandraProxyAPIs } from "../../Common/Constants";
export interface CassandraTableKeys { export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[]; partitionKeys: CassandraTableKey[];
@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity); deferred.resolve(entity);
}, },
(error) => { (error) => {
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError( handleError(
error, errorText,
"CreateKeyspaceCassandra", "CreateKeyspaceCassandra",
`Error while creating a keyspace with query ${createKeyspaceQuery}`, `Error while creating a keyspace with query ${createKeyspaceQuery}`,
); );
deferred.reject(error); deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(
errorText,
"CreateTableCassandra",
`Error while creating a table with query ${createTableQuery}`,
);
deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@ -458,7 +465,7 @@ export class CassandraAPIDataClient extends TableDataClient {
} }
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> { public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
if (!this.useCassandraProxyEndpoint("getTableKeys")) { if (!this.useCassandraProxyEndpoint("getKeys")) {
return this.getTableKeys_ToBeDeprecated(collection); return this.getTableKeys_ToBeDeprecated(collection);
} }
@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data); deferred.resolve(data);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data); deferred.resolve(data);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns); deferred.resolve(data.columns);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns); deferred.resolve(data.columns);
}, },
(error: any) => { (error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); const errorText = error.responseJSON?.message ?? JSON.stringify(error);
deferred.reject(error); handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@ -732,17 +743,23 @@ export class CassandraAPIDataClient extends TableDataClient {
} }
private useCassandraProxyEndpoint(api: string): boolean { private useCassandraProxyEndpoint(api: string): boolean {
const activeCassandraProxyEndpoints: string[] = [
CassandraProxyEndpoints.Development,
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
];
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (userContext.databaseAccount.properties.ipRules?.length > 0) { if (
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
} }
return ( return (
canAccessCassandraProxy && canAccessCassandraProxy &&
configContext.NEW_CASSANDRA_APIS?.includes(api) && configContext.NEW_CASSANDRA_APIS?.includes(api) &&
[Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes( activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
configContext.CASSANDRA_PROXY_ENDPOINT,
)
); );
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/MessageTypes";
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext"; import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import React from "react"; import React from "react";
@ -54,6 +56,11 @@ export class NewQueryTab extends TabsBase {
); );
} }
public onActivate(): void {
this.propagateTabInformation(MessageTypes.ActivateTab);
super.onActivate();
}
public onTabClick(): void { public onTabClick(): void {
useTabs.getState().activateTab(this); useTabs.getState().activateTab(this);
this.iTabAccessor.onTabClickEvent(); this.iTabAccessor.onTabClickEvent();
@ -61,6 +68,7 @@ export class NewQueryTab extends TabsBase {
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
useTabs.getState().closeTab(this); useTabs.getState().closeTab(this);
this.propagateTabInformation(MessageTypes.CloseTab);
if (this.iTabAccessor) { if (this.iTabAccessor) {
this.iTabAccessor.onCloseClickEvent(true); this.iTabAccessor.onCloseClickEvent(true);
} }
@ -69,4 +77,15 @@ export class NewQueryTab extends TabsBase {
public getContainer(): Explorer { public getContainer(): Explorer {
return this.props.container; return this.props.container;
} }
private propagateTabInformation(type: MessageTypes): void {
sendMessage({
type,
data: {
kind: this.tabKind,
databaseId: this.collection?.databaseId,
collectionId: this.collection?.id?.(),
},
});
}
} }

View File

@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@ -21,6 +22,7 @@ import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format"; import { format } from "react-string-format";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg"; import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg";
@ -134,7 +136,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.state = { this.state = {
toggleState: ToggleState.Result, toggleState: ToggleState.Result,
sqlQueryEditorContent: props.queryText || "SELECT * FROM c", sqlQueryEditorContent: props.isPreferredApiMongoDB ? "{}" : props.queryText || "SELECT * FROM c",
selectedContent: "", selectedContent: "",
queryResults: undefined, queryResults: undefined,
error: "", error: "",
@ -224,6 +226,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
}; };
public onDownloadQueryClick = (): void => {
const text = this.getCurrentEditorQuery();
const queryFile = new File([text], `SavedQuery.txt`, { type: "text/plain" });
// It appears the most consistent to download a file from a blob is to create an anchor element and simulate clicking it
const blobUrl = URL.createObjectURL(queryFile);
const anchor = document.createElement("a");
anchor.href = blobUrl;
anchor.download = queryFile.name;
document.body.appendChild(anchor); // Must put the anchor in the document.
anchor.click();
document.body.removeChild(anchor); // Clean up the anchor.
};
public onSaveQueryClick = (): void => { public onSaveQueryClick = (): void => {
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />); useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
}; };
@ -393,6 +409,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({ buttons.push({
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.EXECUTE_ITEM,
onCommandClick: this.props.isSampleCopilotActive onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore) ? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick, : this.onExecuteQueryClick,
@ -403,14 +420,28 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
if (this.saveQueryButton.visible && configContext.platform !== Platform.Fabric) { if (this.saveQueryButton.visible) {
const label = "Save Query"; if (configContext.platform !== Platform.Fabric) {
const label = "Save Query";
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveQueryButton.enabled,
});
}
buttons.push({ buttons.push({
iconSrc: SaveQueryIcon, iconSrc: DownloadQueryIcon,
iconAlt: label, iconAlt: "Download Query",
onCommandClick: this.onSaveQueryClick, keyboardAction: KeyboardAction.DOWNLOAD_ITEM,
commandButtonLabel: label, onCommandClick: this.onDownloadQueryClick,
ariaLabel: label, commandButtonLabel: "Download Query",
ariaLabel: "Download Query",
hasPopup: false, hasPopup: false,
disabled: !this.saveQueryButton.enabled, disabled: !this.saveQueryButton.enabled,
}); });
@ -437,7 +468,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
hasPopup: false, hasPopup: false,
}; };
const launchCopilotButton = { const launchCopilotButton: CommandButtonComponentProps = {
iconSrc: LaunchCopilot, iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel, iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat, onCommandClick: this.launchQueryCopilotChat,
@ -450,14 +481,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
if (this.props.copilotEnabled) { if (this.props.copilotEnabled) {
const toggleCopilotButton = { const toggleCopilotButton: CommandButtonComponentProps = {
iconSrc: QueryCommandIcon, iconSrc: QueryCommandIcon,
iconAlt: "Copilot", iconAlt: "Query Advisor",
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
onCommandClick: () => { onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive); this._toggleCopilot(!this.state.copilotActive);
}, },
commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
hasPopup: false, hasPopup: false,
}; };
buttons.push(toggleCopilotButton); buttons.push(toggleCopilotButton);
@ -468,6 +500,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({ buttons.push({
iconSrc: CancelQueryIcon, iconSrc: CancelQueryIcon,
iconAlt: label, iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
onCommandClick: () => this.queryAbortController.abort(), onCommandClick: () => this.queryAbortController.abort(),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@ -496,13 +529,16 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}; };
public onChangeContent(newContent: string): void { public onChangeContent(newContent: string): void {
// The copilot store's active query takes precedence over the local state,
// and we can't update both states in a single operation.
// So, we update the copilot store's state first, then update the local state.
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
this.setState({ this.setState({
sqlQueryEditorContent: newContent, sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "", queryCopilotGeneratedQuery: "",
}); });
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
if (this.isPreferredApiMongoDB) { if (this.isPreferredApiMongoDB) {
if (newContent.length > 0) { if (newContent.length > 0) {
this.executeQueryButton = { this.executeQueryButton = {
@ -517,6 +553,8 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
} }
this.saveQueryButton.enabled = newContent.length > 0;
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
@ -544,7 +582,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
public setEditorContent(): string { public getEditorContent(): string {
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) { if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
return this.state.queryCopilotGeneratedQuery; return this.state.queryCopilotGeneratedQuery;
} }
@ -601,7 +639,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
<div className="queryEditor" style={{ height: "100%" }}> <div className="queryEditor" style={{ height: "100%" }}>
<EditorReact <EditorReact
language={"sql"} language={"sql"}
content={this.setEditorContent()} content={this.getEditorContent()}
isReadOnly={false} isReadOnly={false}
wordWrap={"on"} wordWrap={"on"}
ariaLabel={"Editing Query"} ariaLabel={"Editing Query"}

View File

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

View File

@ -6,6 +6,7 @@ import { IpRule } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels"; import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab"; import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab"; import { ConnectTab } from "Explorer/Tabs/ConnectTab";
@ -13,6 +14,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility"; import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils"; import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
@ -41,6 +43,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
showMongoAndCassandraProxiesNetworkSettingsWarningState, showMongoAndCassandraProxiesNetworkSettingsWarningState,
setShowMongoAndCassandraProxiesNetworkSettingsWarningState, setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning()); ] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
useEffect(() => {
setKeyboardHandlers({
[KeyboardAction.SELECT_LEFT_TAB]: () => useTabs.getState().selectLeftTab(),
[KeyboardAction.SELECT_RIGHT_TAB]: () => useTabs.getState().selectRightTab(),
[KeyboardAction.CLOSE_TAB]: () => useTabs.getState().closeActiveTab(),
});
}, [setKeyboardHandlers]);
return ( return (
<div className="tabsManagerContainer"> <div className="tabsManagerContainer">
{networkSettingsWarning && ( {networkSettingsWarning && (
@ -297,6 +309,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
}; };
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
// React tabs have no context buttons.
useCommandBar.getState().setContextButtons([]);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
switch (activeReactTab) { switch (activeReactTab) {
case ReactTabKind.Connect: case ReactTabKind.Connect:
@ -324,13 +339,17 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) { if (
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
(userContext.apiType === "Cassandra" &&
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
ipRules?.length
) {
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
const ipRulesIncludeLegacyPortalBackend: boolean = const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule)) ipAddressesFromIPRules.includes(legacyPortalBackendIP),
?.length === legacyPortalBackendIPs.length; );
if (!ipRulesIncludeLegacyPortalBackend) { if (!ipRulesIncludeLegacyPortalBackend) {
return false; return false;
} }
@ -344,9 +363,9 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]] ? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT]; : MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
const ipRulesIncludeMongoProxy: boolean = const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule)) ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
?.length === mongoProxyOutboundIPs.length; );
if (ipRulesIncludeMongoProxy) { if (ipRulesIncludeMongoProxy) {
updateConfigContext({ updateConfigContext({
@ -368,9 +387,15 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
] ]
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT]; : CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
const ipRulesIncludeCassandraProxy: boolean = const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every(
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule)) (cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
?.length === cassandraProxyOutboundIPs.length; );
if (ipRulesIncludeCassandraProxy) {
updateConfigContext({
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
});
}
return !ipRulesIncludeCassandraProxy; return !ipRulesIncludeCassandraProxy;
} }

View File

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

View File

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

View File

@ -373,11 +373,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => container.onCreateDirectory(item, isGithubTree), onClick: () => container.onCreateDirectory(item, isGithubTree),
}, },
{
label: "New Notebook",
iconSrc: NewNotebookIcon,
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
},
{ {
label: "Upload File", label: "Upload File",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
@ -786,9 +781,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}> <AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} /> <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
</AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
{/* {buildGalleryCallout()} */} {/* {buildGalleryCallout()} */}

View File

@ -800,11 +800,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item), onClick: () => this.container.onCreateDirectory(item),
}, },
{
label: "New Notebook",
iconSrc: NewNotebookIcon,
onClick: () => this.container.onNewNotebookClicked(item),
},
{ {
label: "Upload File", label: "Upload File",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,

159
src/KeyboardShortcuts.tsx Normal file
View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { usePortalBackendEndpoint } from "Utils/EndpointUtils"; import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import * as React from "react"; import * as React from "react";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import ErrorImage from "../../../../images/error.svg"; import ErrorImage from "../../../../images/error.svg";
@ -19,7 +19,7 @@ interface Props {
} }
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => { export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) { if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
return await fetchEncryptedToken_ToBeDeprecated(connectionString); return await fetchEncryptedToken_ToBeDeprecated(connectionString);
} }

View File

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

View File

@ -1,3 +1,7 @@
if (window.parent !== window) { if (window.parent !== window) {
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; try {
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
} catch {
// No-op. We can throw here if the parent is not the same origin (such as in the Azure portal).
}
} }

View File

@ -82,7 +82,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
}; };
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [ export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
@ -145,8 +145,30 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
export const allowedNotebookServerUrls: ReadonlyArray<string> = []; export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
export function usePortalBackendEndpoint(backendApi: BackendApi): boolean { //
const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development]; // Temporary function to determine if a portal backend API is supported by the
const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false; // new backend in this environment.
return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as string); //
// TODO: Remove this function once new backend migration is completed for all environments.
//
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,
],
};
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
return false;
}
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
} }

View File

@ -497,6 +497,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint, MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint, CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint,
}); });
updateUserContext({ updateUserContext({

View File

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

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Cassandra keyspace and table CRUD", async () => { test("Cassandra keyspace and table CRUD", async () => {
const keyspaceId = generateUniqueName("keyspace"); const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table"); 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); page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`);
await page.waitForSelector("iframe"); await page.waitForSelector("iframe");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Graph CRUD", async () => { test("Graph CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); 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); page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`);
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Create new database and graph // Create new database and graph

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); 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); page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`);
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Create new database and collection // Create new database and collection

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); 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); page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`);
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Create new database and collection // Create new database and collection

View File

@ -1,5 +1,10 @@
import { getAzureCLICredentialsToken } from "../utils/shared";
test("Self Serve", async () => { test("Self Serve", async () => {
await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html"); // 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}`);
const handle = await page.waitForSelector("iframe"); const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame(); const frame = await handle.contentFrame();

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("SQL CRUD", async () => { test("SQL CRUD", async () => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container"); 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); page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us"); await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`);
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
await explorer.click('[data-test="New Container"]'); await explorer.click('[data-test="New Container"]');

View File

@ -1,19 +1,15 @@
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { CosmosClient, PermissionMode } from "@azure/cosmos"; import { CosmosClient, PermissionMode } from "@azure/cosmos";
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentials } from "../utils/shared";
jest.setTimeout(120000); jest.setTimeout(120000);
const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4"; const subscriptionId = process.env["AZURE_SUBSCRIPTION_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 resourceGroupName = "runners";
test("Resource token", async () => { test("Resource token", async () => {
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); const credentials = await getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us"); const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us"); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");

View File

@ -1,15 +1,17 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Tables CRUD", async () => { test("Tables CRUD", async () => {
const tableId = generateUniqueName("table"); 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); page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`);
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
await page.waitForSelector('text="Querying databases"', { state: "detached" }); await page.waitForSelector('text="Querying databases"', { state: "detached" });

View File

@ -1,5 +1,4 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { ClientSecretCredential } from "@azure/identity";
import "../../less/hostedexplorer.less"; import "../../less/hostedexplorer.less";
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
import { updateUserContext } from "../../src/UserContext"; import { updateUserContext } from "../../src/UserContext";
@ -11,29 +10,13 @@ const urlSearchParams = new URLSearchParams(window.location.search);
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us"; const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
const selfServeType = urlSearchParams.get("selfServeType") || "example"; const selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache"; 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("Resource Group:", resourceGroup);
console.log("Subcription: ", subscriptionId); console.log("Subcription: ", subscriptionId);
console.log("Account Name: ", accountName); console.log("Account Name: ", accountName);
const initTestExplorer = async (): Promise<void> => { const initTestExplorer = async (): Promise<void> => {
const { token } = await credentials.getToken("https://management.azure.com//.default");
updateUserContext({ updateUserContext({
authorizationToken: `bearer ${token}`, authorizationToken: `bearer ${token}`,
}); });
@ -52,6 +35,9 @@ const initTestExplorer = async (): Promise<void> => {
dnsSuffix: "documents.azure.com", dnsSuffix: "documents.azure.com",
serverId: "prod1", serverId: "prod1",
extensionEndpoint: "/proxy", 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, subscriptionType: 3,
quotaId: "Internal_2014-09-01", quotaId: "Internal_2014-09-01",
isTryCosmosDBSubscription: false, isTryCosmosDBSubscription: false,

View File

@ -1,3 +1,4 @@
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
import crypto from "crypto"; import crypto from "crypto";
export function generateUniqueName(baseName = "", length = 4): string { export function generateUniqueName(baseName = "", length = 4): string {
@ -7,3 +8,13 @@ export function generateUniqueName(baseName = "", length = 4): string {
export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string { export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; 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,10 +2,7 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
const ms = require("ms"); const ms = require("ms");
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; const subscriptionId = process.env["AZURE_SUBSCRIPTION_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 resourceGroupName = "runners";
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
@ -19,7 +16,7 @@ function friendlyTime(date) {
} }
async function main() { async function main() {
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); const credentials = await msRestNodeAuth.AzureCliCredentials.create();
const client = new CosmosDBManagementClient(credentials, subscriptionId); const client = new CosmosDBManagementClient(credentials, subscriptionId);
const accounts = await client.databaseAccounts.list(resourceGroupName); const accounts = await client.databaseAccounts.list(resourceGroupName);
for (const account of accounts) { for (const account of accounts) {
@ -38,7 +35,7 @@ async function main() {
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) { } else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces( const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
resourceGroupName, resourceGroupName,
account.name account.name,
); );
for (const database of cassandraDatabases) { for (const database of cassandraDatabases) {
const timestamp = Number(database.resource._ts) * 1000; const timestamp = Number(database.resource._ts) * 1000;

View File

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