Compare commits

..

4 Commits

Author SHA1 Message Date
Ashley Stanton-Nurse
08d3318a87 remove QueryEditor component 2024-03-29 11:47:45 -07:00
Ashley Stanton-Nurse
8b6d857ddb focus on the Copilotv2 editor 2024-03-28 15:26:13 -07:00
Ashley Stanton-Nurse
2598760a11 tinker with an "Execute Query" action in Monaco 2024-03-28 13:08:15 -07:00
Ashley Stanton-Nurse
44d886b4a0 restore some Monaco styles clobbered by our global styles 2024-03-28 10:26:52 -07:00
51 changed files with 662 additions and 276 deletions

View File

@@ -8,9 +8,6 @@ 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
@@ -137,7 +134,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -148,18 +145,11 @@ 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,10 +9,6 @@ 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"
@@ -20,17 +16,10 @@ jobs:
name: "Cleanup Test Database Accounts" name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps: 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

@@ -124,9 +124,8 @@ export enum MongoBackendEndpointType {
remote, remote,
} }
export class BackendApi { export enum BackendApi {
public static readonly GenerateToken: string = "GenerateToken"; GenerateToken,
public static readonly PortalSettings: string = "PortalSettings";
} }
export class PortalBackendEndpoints { export class PortalBackendEndpoints {

View File

@@ -0,0 +1,25 @@
import { KeyMap } from "react-hotkeys";
export const keyMap: KeyMap = {
NEW_QUERY: {
name: "New Query",
sequence: "ctrl+j",
action: "keydown",
},
CANCEL_QUERY: {
name: "Cancel Query",
sequence: "f8",
action: "keydown",
},
DISCARD: {
name: "Discard Changes",
sequence: "ctrl+x",
action: "keydown"
}
};
export type KeyboardShortcutName = keyof typeof keyMap;
export type KeyboardShortcutHandlers = Partial<{
[key in KeyboardShortcutName]: (keyEvent?: KeyboardEvent) => void;
}>;

View File

@@ -67,7 +67,7 @@ export function queryDocuments(
query: string, query: string,
continuationToken?: string, continuationToken?: string,
): Promise<QueryResponse> { ): Promise<QueryResponse> {
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) { if (!useMongoProxyEndpoint("resourcelist")) {
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" : "/queryDocuments"; const path = isResourceList ? "/resourcelist" : "";
return window return window
.fetch(`${endpoint}${path}`, { .fetch(`${endpoint}${path}`, {
@@ -690,16 +690,9 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
} }
function useMongoProxyEndpoint(api: string): boolean { function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [ const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development];
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if ( if (userContext.databaseAccount.properties.ipRules?.length > 0) {
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
} }

View File

@@ -99,19 +99,24 @@ 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",
"queryDocuments", // "createDocument",
"createDocument", // "readDocument",
"readDocument", // "updateDocument",
"updateDocument", // "deleteDocument",
"deleteDocument", // "createCollectionWithProxy",
"createCollectionWithProxy",
], ],
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: ["postQuery", "createOrDelete", "getKeys", "getSchema"], NEW_CASSANDRA_APIS: [
// "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,7 +47,6 @@ 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,7 +387,6 @@ 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 { KeyboardShortcutName } from "Common/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,16 @@ 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;
/**
* A keyboard shortcut that can be used to activate this button.
*/
keyboardShortcut?: KeyboardShortcutName;
} }
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> { export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {

View File

@@ -1,6 +1,6 @@
import { Spinner, SpinnerSize } from "@fluentui/react"; import { Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { loadMonaco, monaco } from "../../LazyMonaco"; import { MonacoNamespace, loadMonaco, monaco } from "../../LazyMonaco";
// import "./EditorReact.less"; // import "./EditorReact.less";
interface EditorReactStates { interface EditorReactStates {
@@ -21,6 +21,7 @@ export interface EditorReactProps {
minimap?: monaco.editor.IEditorOptions["minimap"]; minimap?: monaco.editor.IEditorOptions["minimap"];
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"]; scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
monacoContainerStyles?: React.CSSProperties; monacoContainerStyles?: React.CSSProperties;
configureEditor?: (monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) => void;
} }
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> { export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
@@ -46,25 +47,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
}, 100); }, 100);
} }
public componentDidUpdate() { public componentDidUpdate(previous: EditorReactProps) {
if (!this.editor) { if (this.props.content !== previous.content) {
return; this.editor?.setValue(this.props.content);
}
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,
},
]);
}
} }
} }
@@ -85,16 +70,11 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
); );
} }
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { protected configureEditor(monaco: MonacoNamespace, 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) {
// Hooking the model's onDidChangeContent event because of some event ordering issues. queryEditorModel.onDidChangeContent(() => {
// 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());
}); });
@@ -108,12 +88,16 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
}, },
); );
} }
if (this.props.configureEditor) {
this.props.configureEditor(monaco, this.editor);
}
} }
/** /**
* Create the monaco editor and attach to DOM * Create the monaco editor and attach to DOM
*/ */
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) { private async createEditor(createCallback: (monaco: MonacoNamespace, e: monaco.editor.IStandaloneCodeEditor) => void) {
const options: monaco.editor.IStandaloneEditorConstructionOptions = { const options: monaco.editor.IStandaloneEditorConstructionOptions = {
language: this.props.language, language: this.props.language,
value: this.props.content, value: this.props.content,
@@ -132,7 +116,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.rootNode.innerHTML = ""; this.rootNode.innerHTML = "";
const monaco = await loadMonaco(); const monaco = await loadMonaco();
createCallback(monaco?.editor?.create(this.rootNode, options)); createCallback(monaco, monaco?.editor?.create(this.rootNode, options));
if (this.rootNode.innerHTML) { if (this.rootNode.innerHTML) {
this.setState({ this.setState({

View File

@@ -754,6 +754,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onRevertClick, onCommandClick: this.onRevertClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -66,7 +66,6 @@ export interface MongoNotificationMessage {
export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): boolean => { export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): boolean => {
const database: ViewModels.Database = collection.getDatabase(); const database: ViewModels.Database = collection.getDatabase();
console.log(database?.isDatabaseShared(), collection.offer());
return database?.isDatabaseShared() && !collection.offer(); return database?.isDatabaseShared() && !collection.offer();
}; };

View File

@@ -98,11 +98,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
// Only call when expand has actually changed // Only call when expand has actually changed
if (this.state.isExpanded !== prevState.isExpanded) { if (this.state.isExpanded !== prevState.isExpanded) {
if (this.state.isExpanded) { if (this.state.isExpanded) {
console.log("IN HERE"); this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent.callbackDelayMS);
this.props.node.onExpanded &&
setTimeout(async () => {
await this.props.node.onExpanded();
}, TreeNodeComponent.callbackDelayMS);
} else { } else {
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS); this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS);
} }

View File

@@ -3,3 +3,4 @@ export type { monaco };
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const loadMonaco = () => import(/* webpackChunkName: "lazy-monaco" */ "monaco-editor/esm/vs/editor/editor.api"); export const loadMonaco = () => import(/* webpackChunkName: "lazy-monaco" */ "monaco-editor/esm/vs/editor/editor.api");
export type MonacoNamespace = Awaited<ReturnType<typeof loadMonaco>>;

View File

@@ -4,9 +4,11 @@
* and update any knockout observables passed from the parent. * and update any knockout observables passed from the parent.
*/ */
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { keyMap } from "Common/KeyboardShortcuts";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import { GlobalHotKeys } from "react-hotkeys";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants"; import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants"; import { StyleConstants } from "../../../Common/StyleConstants";
@@ -105,8 +107,13 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
}, },
}; };
const handlers = CommandBarUtil.createKeyboardHandlers(staticButtons.concat(contextButtons).concat(controlButtons));
return ( return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}> <div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
{/* Handles keyboard shortcuts for command bar buttons when focus is OUTSIDE monaco. Even though it's placed here in the DOM, it hooks keydown on 'document' */}
<GlobalHotKeys keyMap={keyMap} handlers={handlers} allowChanges={true} />
<FluentCommandBar <FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands" ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)} items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}

View File

@@ -2,8 +2,10 @@ 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";
@@ -70,6 +72,181 @@ 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();
@@ -128,6 +305,42 @@ 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", () => {
@@ -155,6 +368,62 @@ 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

@@ -7,10 +7,14 @@ 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";
@@ -18,6 +22,7 @@ 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";
@@ -28,6 +33,7 @@ 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";
@@ -74,6 +80,57 @@ 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";
@@ -297,6 +354,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "NEW_QUERY",
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@@ -312,6 +370,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn", id: "newQueryBtn",
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "NEW_QUERY",
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
@@ -392,6 +451,40 @@ 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 {
@@ -419,6 +512,19 @@ 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,
@@ -458,6 +564,45 @@ 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

@@ -6,6 +6,7 @@ import {
IDropdownOption, IDropdownOption,
IDropdownStyles, IDropdownStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { KeyboardShortcutHandlers } from "Common/KeyboardShortcuts";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as React from "react"; import * as React from "react";
import _ from "underscore"; import _ from "underscore";
@@ -233,3 +234,16 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
onRender: () => <ConnectionStatus container={container} poolId={poolId} />, onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
}; };
}; };
export const createKeyboardHandlers = (buttons: CommandButtonComponentProps[]): KeyboardShortcutHandlers => {
const handlers: KeyboardShortcutHandlers = {};
buttons.forEach((button) => {
if (button.keyboardShortcut) {
handlers[button.keyboardShortcut] = (e) => {
button.onCommandClick(e);
e.preventDefault();
};
}
});
return handlers;
}

View File

@@ -336,8 +336,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4} directionalHint={4}
> >
<Icon <Icon
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
impacting the performance of transactional workloads."
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
tabIndex={0} tabIndex={0}

View File

@@ -104,6 +104,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
)} )}
<Stack className="tabPaneContentContainer"> <Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}> <SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
COPILOT
<EditorReact <EditorReact
language={"sql"} language={"sql"}
content={query} content={query}

View File

@@ -1,7 +1,6 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Areas, Areas,
BackendApi,
ConnectionStatusType, ConnectionStatusType,
ContainerStatusType, ContainerStatusType,
HttpStatusCodes, HttpStatusCodes,
@@ -31,7 +30,6 @@ 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";
@@ -82,11 +80,7 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
}; };
export const getCopilotEnabled = async (): Promise<boolean> => { export const getCopilotEnabled = async (): Promise<boolean> => {
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings) const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
? 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

@@ -3,7 +3,6 @@ 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";
@@ -20,6 +19,7 @@ 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[];
@@ -458,7 +458,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("getKeys")) { if (!this.useCassandraProxyEndpoint("getTableKeys")) {
return this.getTableKeys_ToBeDeprecated(collection); return this.getTableKeys_ToBeDeprecated(collection);
} }
@@ -732,23 +732,17 @@ 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 ( if (userContext.databaseAccount.properties.ipRules?.length > 0) {
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) &&
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT) [Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes(
configContext.CASSANDRA_PROXY_ENDPOINT,
)
); );
} }
} }

View File

@@ -6,16 +6,16 @@ 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 { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants"; import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility"; import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
@@ -621,6 +621,7 @@ export default class ConflictsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onDiscardClick, onCommandClick: this.onDiscardClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -921,6 +921,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onRevertNewDocumentClick, onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -950,6 +951,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onRevertExisitingDocumentClick, onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -109,8 +109,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
: JSON.stringify(queryResults.documents, undefined, 4) : JSON.stringify(queryResults.documents, undefined, 4)
: ""; : "";
console.log("QUERY RESULT: ", queryResultsString);
const onErrorDetailsClick = (): boolean => { const onErrorDetailsClick = (): boolean => {
useNotificationConsole.getState().expandConsole(); useNotificationConsole.getState().expandConsole();

View File

@@ -1,5 +1,3 @@
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";
@@ -56,11 +54,6 @@ 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();
@@ -68,7 +61,6 @@ 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);
} }
@@ -77,15 +69,4 @@ 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

@@ -3,6 +3,8 @@
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { MonacoNamespace, monaco } from "Explorer/LazyMonaco";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar"; import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
@@ -39,7 +41,6 @@ import { userContext } from "../../../UserContext";
import * as QueryUtils from "../../../Utils/QueryUtils"; import * as QueryUtils from "../../../Utils/QueryUtils";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
@@ -134,7 +135,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.state = { this.state = {
toggleState: ToggleState.Result, toggleState: ToggleState.Result,
sqlQueryEditorContent: props.isPreferredApiMongoDB ? "{}" : props.queryText || "SELECT * FROM c", sqlQueryEditorContent: props.queryText || "SELECT * FROM c",
selectedContent: "", selectedContent: "",
queryResults: undefined, queryResults: undefined,
error: "", error: "",
@@ -357,7 +358,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
firstItemIndex, firstItemIndex,
queryDocuments, queryDocuments,
); );
console.log("SETTING QUERY RESULTS", queryResults);
this.setState({ queryResults, error: "" }); this.setState({ queryResults, error: "" });
} catch (error) { } catch (error) {
this.props.tabsBaseInstance.isExecutionError(true); this.props.tabsBaseInstance.isExecutionError(true);
@@ -469,6 +469,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({ buttons.push({
iconSrc: CancelQueryIcon, iconSrc: CancelQueryIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "CANCEL_QUERY",
onCommandClick: () => this.queryAbortController.abort(), onCommandClick: () => this.queryAbortController.abort(),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -497,16 +498,13 @@ 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 = {
@@ -548,7 +546,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
public getEditorContent(): string { public setEditorContent(): string {
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) { if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
return this.state.queryCopilotGeneratedQuery; return this.state.queryCopilotGeneratedQuery;
} }
@@ -588,6 +586,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
private getEditorAndQueryResult(): JSX.Element { private getEditorAndQueryResult(): JSX.Element {
const configureEditor = (monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) => {
editor.addAction({
id: "execute-query",
label: "Execute Query",
keybindings: [monaco.KeyMod.Shift | monaco.KeyCode.Enter],
run: () => this.onExecuteQueryClick(),
});
}
return ( return (
<Fragment> <Fragment>
<div className="tab-pane" id={this.props.tabId} role="tabpanel"> <div className="tab-pane" id={this.props.tabId} role="tabpanel">
@@ -605,14 +612,15 @@ 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.getEditorContent()} content={this.setEditorContent()}
isReadOnly={false} isReadOnly={false}
wordWrap={"on"} wordWrap={"on"}
ariaLabel={"Editing Query"} ariaLabel={"Editing Query"}
lineNumbers={"on"} lineNumbers={"on"}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)} onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)} onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)}
/> configureEditor={configureEditor}
/>;
</div> </div>
</Fragment> </Fragment>
{this.props.isSampleCopilotActive ? ( {this.props.isSampleCopilotActive ? (

View File

@@ -238,6 +238,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onDiscard, onCommandClick: this.onDiscard,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -347,6 +347,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onDiscard, onCommandClick: this.onDiscard,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -324,17 +324,13 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if ( if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
(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 = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) => const ipRulesIncludeLegacyPortalBackend: boolean =
ipAddressesFromIPRules.includes(legacyPortalBackendIP), ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule))
); ?.length === legacyPortalBackendIPs.length;
if (!ipRulesIncludeLegacyPortalBackend) { if (!ipRulesIncludeLegacyPortalBackend) {
return false; return false;
} }
@@ -348,9 +344,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 = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) => const ipRulesIncludeMongoProxy: boolean =
ipAddressesFromIPRules.includes(mongoProxyOutboundIP), ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule))
); ?.length === mongoProxyOutboundIPs.length;
if (ipRulesIncludeMongoProxy) { if (ipRulesIncludeMongoProxy) {
updateConfigContext({ updateConfigContext({
@@ -372,15 +368,9 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
] ]
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT]; : CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every( const ipRulesIncludeCassandraProxy: boolean =
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP), ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule))
); ?.length === cassandraProxyOutboundIPs.length;
if (ipRulesIncludeCassandraProxy) {
updateConfigContext({
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
});
}
return !ipRulesIncludeCassandraProxy; return !ipRulesIncludeCassandraProxy;
} }

View File

@@ -256,6 +256,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this, ...this,
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onDiscard, onCommandClick: this.onDiscard,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -4,9 +4,9 @@ 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";
@@ -109,6 +109,7 @@ export default class UserDefinedFunctionTabContent extends Component<
...this, ...this,
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
keyboardShortcut: "DISCARD",
onCommandClick: this.onDiscard, onCommandClick: this.onDiscard,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -264,16 +264,12 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
public async expandCollection(): Promise<void> { public expandCollection(): void {
if (this.isCollectionExpanded()) { if (this.isCollectionExpanded()) {
return; return;
} }
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
console.log("LOADED OFFERS", this.offer());
this.isCollectionExpanded(true); this.isCollectionExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Collection node", description: "Collection node",
@@ -580,8 +576,8 @@ export default class Collection implements ViewModels.Collection {
public onSettingsClick = async (): Promise<void> => { public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
// const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
// throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer(); throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node", description: "Settings node",

View File

@@ -1,5 +1,4 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { hasDatabaseSharedThroughput } from "Explorer/Controls/Settings/SettingsUtils";
import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
@@ -374,6 +373,11 @@ 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,
@@ -549,11 +553,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
id = database.isDatabaseShared() ? "sampleSettings" : "sampleScaleSettings"; id = database.isDatabaseShared() ? "sampleSettings" : "sampleScaleSettings";
} }
console.log("RUNNING HERE", database.id(), database.offer(), collection.id(), collection.offer());
children.push({ children.push({
id, id,
label: hasDatabaseSharedThroughput(collection) || isServerlessAccount() ? "Settings" : "Scale & Settings", label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection), onClick: collection.onSettingsClick.bind(collection),
isSelected: () => isSelected: () =>
useSelectedNode useSelectedNode
@@ -601,7 +603,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
onClick: () => { onClick: () => {
// Rewritten version of expandCollapseCollection // Rewritten version of expandCollapseCollection
console.log("CLICKED onClick");
useSelectedNode.getState().setSelectedNode(collection); useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
refreshActiveTab( refreshActiveTab(
@@ -609,18 +610,13 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId, tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId,
); );
}, },
onExpanded: async () => { onExpanded: () => {
console.log("CLICKED onExpanded");
await collection.expandCollection();
if (showScriptNodes) { if (showScriptNodes) {
collection.loadStoredProcedures(); collection.loadStoredProcedures();
collection.loadUserDefinedFunctions(); collection.loadUserDefinedFunctions();
collection.loadTriggers(); collection.loadTriggers();
} }
}, },
onCollapsed: () => {
collection.collapseCollection();
},
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
}; };
@@ -790,6 +786,9 @@ 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,6 +800,11 @@ 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,

View File

@@ -1,6 +1,3 @@
// 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";
@@ -21,6 +18,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 * as ReactHotkeys from "react-hotkeys";
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";
@@ -64,6 +62,28 @@ import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
initializeIcons(); initializeIcons();
const tagsIgnoredByReactHotkeys = ["INPUT", "SELECT"];
ReactHotkeys.configure({
ignoreEventsCondition: (evt) => {
// The default react-hotkeys behavior is to ignore events targetting a textarea, but we want the monaco editor's key events to bubble up
// So, we configure it to ignore all events targetting a textarea except when the target is a monaco editor's text area
if (!(evt.target instanceof HTMLElement)) {
return true;
}
if (tagsIgnoredByReactHotkeys.includes(evt.target.tagName)) {
return true;
}
if (evt.target.tagName === "TEXTAREA" && !evt.target.matches(".monaco-editor textarea")) {
return true;
}
return false;
}
})
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true); const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
const isCarouselOpen = useCarousel((state) => state.shouldOpen); const isCarouselOpen = useCarousel((state) => state.shouldOpen);

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 { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import { usePortalBackendEndpoint } 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 (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) { if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) {
return await fetchEncryptedToken_ToBeDeprecated(connectionString); return await fetchEncryptedToken_ToBeDeprecated(connectionString);
} }

View File

@@ -1,7 +1,3 @@
if (window.parent !== window) { if (window.parent !== window) {
try {
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; (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

@@ -145,22 +145,8 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
export const allowedNotebookServerUrls: ReadonlyArray<string> = []; export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
// export function usePortalBackendEndpoint(backendApi: BackendApi): boolean {
// Temporary function to determine if a portal backend API is supported by the const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development];
// new backend in this environment. const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false;
// 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],
[BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
};
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
return false;
}
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
} }

View File

@@ -497,7 +497,6 @@ 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,18 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateUniqueName } 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&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner");
await page.waitForSelector("iframe"); await page.waitForSelector("iframe");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,5 @@
import { getAzureCLICredentialsToken } from "../utils/shared";
test("Self Serve", async () => { test("Self Serve", async () => {
// We can't retrieve AZ CLI credentials from the browser so we get them here. await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
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,18 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateUniqueName } 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&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
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,15 +1,19 @@
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, getAzureCLICredentials } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
jest.setTimeout(120000); jest.setTimeout(120000);
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? ""; const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4";
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners"; const resourceGroupName = "runners";
test("Resource token", async () => { test("Resource token", async () => {
const credentials = await getAzureCLICredentials(); const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
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,17 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateUniqueName } 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&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
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,4 +1,5 @@
/* 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";
@@ -10,13 +11,29 @@ 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}`,
}); });

View File

@@ -1,4 +1,3 @@
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 {
@@ -8,13 +7,3 @@ 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,7 +2,10 @@ 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 subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners"; const resourceGroupName = "runners";
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
@@ -16,7 +19,7 @@ function friendlyTime(date) {
} }
async function main() { async function main() {
const credentials = await msRestNodeAuth.AzureCliCredentials.create(); const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
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) {
@@ -35,7 +38,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;