mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 19:46:53 +00:00
Compare commits
17 Commits
ashleyst/m
...
ashleyst/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba3f39d10 | ||
|
|
e0cb3da6aa | ||
|
|
6c9673975a | ||
|
|
d35e2a325e | ||
|
|
00a816c488 | ||
|
|
953bef404b | ||
|
|
dfcb771939 | ||
|
|
6925fa8e4e | ||
|
|
7f6338b68b | ||
|
|
db50f42832 | ||
|
|
f533eeb0fc | ||
|
|
3c5d899e47 | ||
|
|
b44778b00a | ||
|
|
1464745659 | ||
|
|
18cc2a4195 | ||
|
|
86f2bc171f | ||
|
|
cabedf4a29 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -8,6 +8,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
codemetrics:
|
codemetrics:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -134,7 +137,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -145,11 +148,18 @@ jobs:
|
|||||||
- ./test/mongo/container.spec.ts
|
- ./test/mongo/container.spec.ts
|
||||||
- ./test/mongo/container32.spec.ts
|
- ./test/mongo/container32.spec.ts
|
||||||
- ./test/selfServe/selfServeExample.spec.ts
|
- ./test/selfServe/selfServeExample.spec.ts
|
||||||
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
|
|
||||||
- ./test/sql/resourceToken.spec.ts
|
- ./test/sql/resourceToken.spec.ts
|
||||||
- ./test/tables/container.spec.ts
|
- ./test/tables/container.spec.ts
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: "Az CLI login"
|
||||||
|
uses: azure/login@v1
|
||||||
|
with:
|
||||||
|
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
15
.github/workflows/cleanup.yml
vendored
15
.github/workflows/cleanup.yml
vendored
@@ -9,6 +9,10 @@ on:
|
|||||||
# Once every hour
|
# Once every hour
|
||||||
- cron: "0 15 * * *"
|
- cron: "0 15 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
jobs:
|
jobs:
|
||||||
# This workflow contains a single job called "build"
|
# This workflow contains a single job called "build"
|
||||||
@@ -16,10 +20,17 @@ jobs:
|
|||||||
name: "Cleanup Test Database Accounts"
|
name: "Cleanup Test Database Accounts"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: "Az CLI login"
|
||||||
|
uses: azure/login@v1
|
||||||
|
with:
|
||||||
|
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -124,8 +124,9 @@ export enum MongoBackendEndpointType {
|
|||||||
remote,
|
remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BackendApi {
|
export class BackendApi {
|
||||||
GenerateToken,
|
public static readonly GenerateToken: string = "GenerateToken";
|
||||||
|
public static readonly PortalSettings: string = "PortalSettings";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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;
|
|
||||||
}>;
|
|
||||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
|||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
if (!useMongoProxyEndpoint("resourcelist")) {
|
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
||||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ export function queryDocuments(
|
|||||||
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = isResourceList ? "/resourcelist" : "";
|
const path = isResourceList ? "/resourcelist" : "/queryDocuments";
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}${path}`, {
|
.fetch(`${endpoint}${path}`, {
|
||||||
@@ -690,9 +690,16 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useMongoProxyEndpoint(api: string): boolean {
|
function useMongoProxyEndpoint(api: string): boolean {
|
||||||
const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development];
|
const activeMongoProxyEndpoints: string[] = [
|
||||||
|
MongoProxyEndpoints.Development,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
];
|
||||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
if (
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,24 +99,19 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
NEW_BACKEND_APIS: [BackendApi.GenerateToken],
|
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
NEW_MONGO_APIS: [
|
NEW_MONGO_APIS: [
|
||||||
// "resourcelist",
|
"resourcelist",
|
||||||
// "createDocument",
|
"queryDocuments",
|
||||||
// "readDocument",
|
"createDocument",
|
||||||
// "updateDocument",
|
"readDocument",
|
||||||
// "deleteDocument",
|
"updateDocument",
|
||||||
// "createCollectionWithProxy",
|
"deleteDocument",
|
||||||
|
"createCollectionWithProxy",
|
||||||
],
|
],
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: [
|
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||||
// "postQuery",
|
|
||||||
// "createOrDelete",
|
|
||||||
// "getKeys",
|
|
||||||
// "getSchema",
|
|
||||||
],
|
|
||||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export enum MessageTypes {
|
|||||||
GetAllResourceTokens, // Data Explorer -> Fabric
|
GetAllResourceTokens, // Data Explorer -> Fabric
|
||||||
Ready, // Data Explorer -> Fabric
|
Ready, // Data Explorer -> Fabric
|
||||||
OpenCESCVAFeedbackBlade,
|
OpenCESCVAFeedbackBlade,
|
||||||
|
ActivateTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
export interface AuthorizationToken {
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
dnsSuffix?: string;
|
dnsSuffix?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
extensionEndpoint?: string;
|
extensionEndpoint?: string;
|
||||||
|
portalBackendEndpoint?: string;
|
||||||
mongoProxyEndpoint?: string;
|
mongoProxyEndpoint?: string;
|
||||||
cassandraProxyEndpoint?: string;
|
cassandraProxyEndpoint?: string;
|
||||||
subscriptionType?: SubscriptionType;
|
subscriptionType?: SubscriptionType;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 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";
|
||||||
@@ -31,7 +30,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
onCommandClick: (e: React.SyntheticEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@@ -108,16 +107,10 @@ 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> {
|
||||||
|
|||||||
@@ -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 { MonacoNamespace, loadMonaco, monaco } from "../../LazyMonaco";
|
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||||
// import "./EditorReact.less";
|
// import "./EditorReact.less";
|
||||||
|
|
||||||
interface EditorReactStates {
|
interface EditorReactStates {
|
||||||
@@ -21,7 +21,6 @@ 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> {
|
||||||
@@ -47,9 +46,25 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(previous: EditorReactProps) {
|
public componentDidUpdate() {
|
||||||
if (this.props.content !== previous.content) {
|
if (!this.editor) {
|
||||||
this.editor?.setValue(this.props.content);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingContent = this.editor.getModel().getValue();
|
||||||
|
|
||||||
|
if (this.props.content !== existingContent) {
|
||||||
|
if (this.props.isReadOnly) {
|
||||||
|
this.editor.setValue(this.props.content);
|
||||||
|
} else {
|
||||||
|
this.editor.pushUndoStop();
|
||||||
|
this.editor.executeEdits("", [
|
||||||
|
{
|
||||||
|
range: this.editor.getModel().getFullModelRange(),
|
||||||
|
text: this.props.content,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +85,16 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected configureEditor(monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) {
|
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
const queryEditorModel = this.editor.getModel();
|
|
||||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||||
queryEditorModel.onDidChangeContent(() => {
|
// Hooking the model's onDidChangeContent event because of some event ordering issues.
|
||||||
|
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
|
||||||
|
// then there are some inconsistencies as to which event fires first.
|
||||||
|
// But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event.
|
||||||
|
// (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.)
|
||||||
|
// If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first.
|
||||||
|
this.editor.onDidChangeModelContent(() => {
|
||||||
const queryEditorModel = this.editor.getModel();
|
const queryEditorModel = this.editor.getModel();
|
||||||
this.props.onContentChanged(queryEditorModel.getValue());
|
this.props.onContentChanged(queryEditorModel.getValue());
|
||||||
});
|
});
|
||||||
@@ -88,16 +108,12 @@ 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: (monaco: MonacoNamespace, e: monaco.editor.IStandaloneCodeEditor) => void) {
|
private async createEditor(createCallback: (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,
|
||||||
@@ -116,7 +132,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, monaco?.editor?.create(this.rootNode, options));
|
createCallback(monaco?.editor?.create(this.rootNode, options));
|
||||||
|
|
||||||
if (this.rootNode.innerHTML) {
|
if (this.rootNode.innerHTML) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -754,7 +754,6 @@ 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,
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ 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>>;
|
|
||||||
|
|||||||
@@ -4,11 +4,9 @@
|
|||||||
* 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";
|
||||||
@@ -107,13 +105,8 @@ 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)}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import * as ko from "knockout";
|
|||||||
import { AuthType } from "../../../AuthType";
|
import { AuthType } from "../../../AuthType";
|
||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { updateUserContext } from "../../../UserContext";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import NotebookManager from "../../Notebook/NotebookManager";
|
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -72,181 +70,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Enable notebook button", () => {
|
|
||||||
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
|
|
||||||
const selectedNodeState = useSelectedNode.getState();
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
updateUserContext({
|
|
||||||
portalEnv: "prod",
|
|
||||||
databaseAccount: {
|
|
||||||
properties: {
|
|
||||||
capabilities: [{ name: "EnableTable" }],
|
|
||||||
},
|
|
||||||
} as DatabaseAccount,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
updateUserContext({
|
|
||||||
portalEnv: "prod",
|
|
||||||
});
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(false);
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is already enabled - button should be hidden", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
|
||||||
expect(enableNotebookBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Account is running on one of the national clouds - button should be hidden", () => {
|
|
||||||
updateUserContext({
|
|
||||||
portalEnv: "mooncake",
|
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
|
||||||
expect(enableNotebookBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
|
||||||
|
|
||||||
//TODO: modify once notebooks are available
|
|
||||||
expect(enableNotebookBtn).toBeUndefined();
|
|
||||||
//expect(enableNotebookBtn).toBeDefined();
|
|
||||||
//expect(enableNotebookBtn.disabled).toBe(false);
|
|
||||||
//expect(enableNotebookBtn.tooltipText).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
|
||||||
|
|
||||||
//TODO: modify once notebooks are available
|
|
||||||
expect(enableNotebookBtn).toBeUndefined();
|
|
||||||
//expect(enableNotebookBtn).toBeDefined();
|
|
||||||
//expect(enableNotebookBtn.disabled).toBe(true);
|
|
||||||
//expect(enableNotebookBtn.tooltipText).toBe(
|
|
||||||
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
|
||||||
//);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Open Mongo shell button", () => {
|
|
||||||
const openMongoShellBtnLabel = "Open Mongo shell";
|
|
||||||
const selectedNodeState = useSelectedNode.getState();
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
properties: {
|
|
||||||
capabilities: [{ name: "EnableTable" }],
|
|
||||||
},
|
|
||||||
} as DatabaseAccount,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
updateUserContext({
|
|
||||||
apiType: "SQL",
|
|
||||||
});
|
|
||||||
useNotebook.getState().setIsShellEnabled(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
updateUserContext({
|
|
||||||
apiType: "Mongo",
|
|
||||||
});
|
|
||||||
useNotebook.getState().setIsShellEnabled(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(false);
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Mongo Api not available - button should be hidden", () => {
|
|
||||||
updateUserContext({
|
|
||||||
apiType: "SQL",
|
|
||||||
});
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
|
||||||
expect(openMongoShellBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Running on a national cloud - button should be hidden", () => {
|
|
||||||
updateUserContext({
|
|
||||||
portalEnv: "mooncake",
|
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
|
||||||
expect(openMongoShellBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
|
||||||
expect(openMongoShellBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is not enabled and is available - button should be hidden", () => {
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
|
||||||
expect(openMongoShellBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
|
||||||
expect(openMongoShellBtn).toBeDefined();
|
|
||||||
|
|
||||||
//TODO: modify once notebooks are available
|
|
||||||
expect(openMongoShellBtn.disabled).toBe(true);
|
|
||||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
|
||||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
|
||||||
expect(openMongoShellBtn).toBeDefined();
|
|
||||||
|
|
||||||
//TODO: modify once notebooks are available
|
|
||||||
expect(openMongoShellBtn.disabled).toBe(true);
|
|
||||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
|
||||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
|
||||||
useNotebook.getState().setIsShellEnabled(false);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
|
||||||
expect(openMongoShellBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Open Cassandra shell button", () => {
|
describe("Open Cassandra shell button", () => {
|
||||||
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
@@ -305,42 +128,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
|
||||||
|
|
||||||
expect(openCassandraShellBtn).toBeDefined();
|
|
||||||
|
|
||||||
//TODO: modify once notebooks are available
|
|
||||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
|
||||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
|
||||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
|
||||||
expect(openCassandraShellBtn).toBeDefined();
|
|
||||||
|
|
||||||
//TODO: modify once notebooks are available
|
|
||||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
|
||||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
|
||||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Open Postgres and vCore Mongo buttons", () => {
|
describe("Open Postgres and vCore Mongo buttons", () => {
|
||||||
@@ -368,62 +155,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GitHub buttons", () => {
|
|
||||||
const connectToGitHubBtnLabel = "Connect to GitHub";
|
|
||||||
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
|
|
||||||
const selectedNodeState = useSelectedNode.getState();
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
properties: {
|
|
||||||
capabilities: [{ name: "EnableTable" }],
|
|
||||||
},
|
|
||||||
} as DatabaseAccount,
|
|
||||||
});
|
|
||||||
|
|
||||||
mockExplorer.notebookManager = new NotebookManager();
|
|
||||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
|
||||||
expect(connectToGitHubBtn).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(true);
|
|
||||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const manageGitHubSettingsBtn = buttons.find(
|
|
||||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
|
|
||||||
);
|
|
||||||
expect(manageGitHubSettingsBtn).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
|
|
||||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
|
||||||
expect(connectToGitHubBtn).toBeUndefined();
|
|
||||||
|
|
||||||
const manageGitHubSettingsBtn = buttons.find(
|
|
||||||
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel,
|
|
||||||
);
|
|
||||||
expect(manageGitHubSettingsBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Resource token", () => {
|
describe("Resource token", () => {
|
||||||
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
||||||
useSelectedNode.getState().setSelectedNode(mockCollection);
|
useSelectedNode.getState().setSelectedNode(mockCollection);
|
||||||
|
|||||||
@@ -7,14 +7,10 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
|||||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||||
import AddUdfIcon from "../../../../images/AddUdf.svg";
|
import AddUdfIcon from "../../../../images/AddUdf.svg";
|
||||||
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
||||||
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
|
||||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||||
import HomeIcon from "../../../../images/Home_16.svg";
|
import HomeIcon from "../../../../images/Home_16.svg";
|
||||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||||
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
||||||
import GitHubIcon from "../../../../images/github.svg";
|
|
||||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
|
||||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
|
||||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||||
@@ -22,7 +18,6 @@ import { AuthType } from "../../../AuthType";
|
|||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { Platform, configContext } from "../../../ConfigContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { JunoClient } from "../../../Juno/JunoClient";
|
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||||
@@ -33,7 +28,6 @@ import { useNotebook } from "../../Notebook/useNotebook";
|
|||||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||||
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
|
||||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
@@ -80,57 +74,6 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
|
||||||
addDivider();
|
|
||||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
|
||||||
|
|
||||||
const newNotebookButton = createNewNotebookButton(container);
|
|
||||||
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
|
|
||||||
notebookButtons.push(newNotebookButton);
|
|
||||||
|
|
||||||
if (container.notebookManager?.gitHubOAuthService) {
|
|
||||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
|
||||||
}
|
|
||||||
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
|
||||||
notebookButtons.push(createOpenTerminalButton(container));
|
|
||||||
}
|
|
||||||
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
|
||||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(userContext.apiType === "Mongo" &&
|
|
||||||
useNotebook.getState().isShellEnabled &&
|
|
||||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
|
||||||
userContext.apiType === "Cassandra"
|
|
||||||
) {
|
|
||||||
notebookButtons.push(createDivider());
|
|
||||||
if (userContext.apiType === "Cassandra") {
|
|
||||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra));
|
|
||||||
} else {
|
|
||||||
notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notebookButtons.forEach((btn) => {
|
|
||||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
|
||||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
|
||||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
|
||||||
}
|
|
||||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
|
||||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
|
||||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
|
||||||
}
|
|
||||||
} else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) {
|
|
||||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
|
||||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
|
||||||
}
|
|
||||||
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
|
||||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
|
||||||
}
|
|
||||||
buttons.push(btn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
|
|
||||||
@@ -354,7 +297,6 @@ 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);
|
||||||
@@ -370,7 +312,6 @@ 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);
|
||||||
@@ -451,40 +392,6 @@ 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 {
|
||||||
@@ -512,19 +419,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
|
||||||
const label = "Open Terminal";
|
|
||||||
return {
|
|
||||||
iconSrc: CosmosTerminalIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
|
||||||
commandButtonLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
ariaLabel: label,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOpenTerminalButtonByKind(
|
function createOpenTerminalButtonByKind(
|
||||||
container: Explorer,
|
container: Explorer,
|
||||||
terminalKind: ViewModels.TerminalKind,
|
terminalKind: ViewModels.TerminalKind,
|
||||||
@@ -564,45 +458,6 @@ function createOpenTerminalButtonByKind(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
|
|
||||||
const label = "Reset Workspace";
|
|
||||||
return {
|
|
||||||
iconSrc: ResetWorkspaceIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => container.resetNotebookWorkspace(),
|
|
||||||
commandButtonLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
ariaLabel: label,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
|
|
||||||
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
|
||||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
|
||||||
const junoClient = new JunoClient();
|
|
||||||
return {
|
|
||||||
iconSrc: GitHubIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
label,
|
|
||||||
<GitHubReposPanel
|
|
||||||
explorer={container}
|
|
||||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
|
||||||
junoClientProp={junoClient}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
commandButtonLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
ariaLabel: label,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStaticCommandBarButtonsForResourceToken(
|
function createStaticCommandBarButtonsForResourceToken(
|
||||||
container: Explorer,
|
container: Explorer,
|
||||||
selectedNodeState: SelectedNodeState,
|
selectedNodeState: SelectedNodeState,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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";
|
||||||
@@ -234,16 +233,3 @@ 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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
styles={getTextFieldStyles()}
|
styles={getTextFieldStyles()}
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Type a new keyspace id"
|
placeholder="Type a new keyspace id"
|
||||||
size={40}
|
size={40}
|
||||||
value={newKeyspaceId}
|
value={newKeyspaceId}
|
||||||
@@ -292,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
required={true}
|
required={true}
|
||||||
ariaLabel="addCollection-table Id Create table"
|
ariaLabel="addCollection-table Id Create table"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Enter table Id"
|
placeholder="Enter table Id"
|
||||||
size={20}
|
size={20}
|
||||||
value={tableId}
|
value={tableId}
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
|
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||||
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
|
||||||
try {
|
try {
|
||||||
|
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||||
tableEntityListViewModel.redrawTableThrottled();
|
tableEntityListViewModel.redrawTableThrottled();
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
|
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
|
||||||
}\r\n`;
|
}\r\n`;
|
||||||
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
|
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
|
||||||
const lastQuery = generatedQuery && query ? `${query}\r\n` : "";
|
const lastQuery = query ? `${query}\r\n` : "";
|
||||||
setQuery(`${lastQuery}${currentGeneratedQuery}`);
|
setQuery(`${lastQuery}${currentGeneratedQuery}`);
|
||||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||||
|
|||||||
@@ -104,16 +104,15 @@ 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}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
wordWrap={"on"}
|
wordWrap={"on"}
|
||||||
ariaLabel={"Editing Query"}
|
ariaLabel={"Editing Query"}
|
||||||
lineNumbers={"on"}
|
lineNumbers={"on"}
|
||||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||||
/>
|
/>
|
||||||
<QueryCopilotResults />
|
<QueryCopilotResults />
|
||||||
</SplitterLayout>
|
</SplitterLayout>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
|
BackendApi,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
ContainerStatusType,
|
ContainerStatusType,
|
||||||
HttpStatusCodes,
|
HttpStatusCodes,
|
||||||
@@ -30,6 +31,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||||
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
@@ -80,7 +82,11 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||||
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
|
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||||
|
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||||
|
: configContext.BACKEND_ENDPOINT;
|
||||||
|
|
||||||
|
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as ko from "knockout";
|
|||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
@@ -19,7 +20,6 @@ import Explorer from "../Explorer";
|
|||||||
import * as TableConstants from "./Constants";
|
import * as TableConstants from "./Constants";
|
||||||
import * as Entities from "./Entities";
|
import * as Entities from "./Entities";
|
||||||
import * as TableEntityProcessor from "./TableEntityProcessor";
|
import * as TableEntityProcessor from "./TableEntityProcessor";
|
||||||
import { CassandraProxyAPIs } from "../../Common/Constants";
|
|
||||||
|
|
||||||
export interface CassandraTableKeys {
|
export interface CassandraTableKeys {
|
||||||
partitionKeys: CassandraTableKey[];
|
partitionKeys: CassandraTableKey[];
|
||||||
@@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(entity);
|
deferred.resolve(entity);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
@@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
errorText,
|
||||||
"CreateKeyspaceCassandra",
|
"CreateKeyspaceCassandra",
|
||||||
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
|
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
|
||||||
);
|
);
|
||||||
deferred.reject(error);
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
@@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(
|
||||||
|
errorText,
|
||||||
|
"CreateTableCassandra",
|
||||||
|
`Error while creating a table with query ${createTableQuery}`,
|
||||||
|
);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
@@ -458,7 +465,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||||
if (!this.useCassandraProxyEndpoint("getTableKeys")) {
|
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||||
return this.getTableKeys_ToBeDeprecated(collection);
|
return this.getTableKeys_ToBeDeprecated(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
@@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
@@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data.columns);
|
deferred.resolve(data.columns);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
@@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
deferred.resolve(data.columns);
|
deferred.resolve(data.columns);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
@@ -732,17 +743,23 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private useCassandraProxyEndpoint(api: string): boolean {
|
private useCassandraProxyEndpoint(api: string): boolean {
|
||||||
|
const activeCassandraProxyEndpoints: string[] = [
|
||||||
|
CassandraProxyEndpoints.Development,
|
||||||
|
CassandraProxyEndpoints.Mpac,
|
||||||
|
CassandraProxyEndpoints.Prod,
|
||||||
|
];
|
||||||
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
if (
|
||||||
|
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
|
||||||
|
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||||
|
) {
|
||||||
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
canAccessCassandraProxy &&
|
canAccessCassandraProxy &&
|
||||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||||
[Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes(
|
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||||
configContext.CASSANDRA_PROXY_ENDPOINT,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 editable from "../../Common/EditableUtility";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
|
||||||
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
|
||||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
|
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
|
||||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||||
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
|
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
|
||||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||||
|
import editable from "../../Common/EditableUtility";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
|
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
||||||
|
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||||
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,7 +621,6 @@ 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,
|
||||||
|
|||||||
@@ -921,7 +921,6 @@ 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,
|
||||||
@@ -951,7 +950,6 @@ 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,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -54,6 +56,11 @@ export class NewQueryTab extends TabsBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onActivate(): void {
|
||||||
|
this.propagateTabInformation(MessageTypes.ActivateTab);
|
||||||
|
super.onActivate();
|
||||||
|
}
|
||||||
|
|
||||||
public onTabClick(): void {
|
public onTabClick(): void {
|
||||||
useTabs.getState().activateTab(this);
|
useTabs.getState().activateTab(this);
|
||||||
this.iTabAccessor.onTabClickEvent();
|
this.iTabAccessor.onTabClickEvent();
|
||||||
@@ -61,6 +68,7 @@ export class NewQueryTab extends TabsBase {
|
|||||||
|
|
||||||
public onCloseTabButtonClick(): void {
|
public onCloseTabButtonClick(): void {
|
||||||
useTabs.getState().closeTab(this);
|
useTabs.getState().closeTab(this);
|
||||||
|
this.propagateTabInformation(MessageTypes.CloseTab);
|
||||||
if (this.iTabAccessor) {
|
if (this.iTabAccessor) {
|
||||||
this.iTabAccessor.onCloseClickEvent(true);
|
this.iTabAccessor.onCloseClickEvent(true);
|
||||||
}
|
}
|
||||||
@@ -69,4 +77,15 @@ export class NewQueryTab extends TabsBase {
|
|||||||
public getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
return this.props.container;
|
return this.props.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private propagateTabInformation(type: MessageTypes): void {
|
||||||
|
sendMessage({
|
||||||
|
type,
|
||||||
|
data: {
|
||||||
|
kind: this.tabKind,
|
||||||
|
databaseId: this.collection?.databaseId,
|
||||||
|
collectionId: this.collection?.id?.(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
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";
|
||||||
@@ -41,6 +39,7 @@ 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";
|
||||||
@@ -135,7 +134,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
toggleState: ToggleState.Result,
|
toggleState: ToggleState.Result,
|
||||||
sqlQueryEditorContent: props.queryText || "SELECT * FROM c",
|
sqlQueryEditorContent: props.isPreferredApiMongoDB ? "{}" : props.queryText || "SELECT * FROM c",
|
||||||
selectedContent: "",
|
selectedContent: "",
|
||||||
queryResults: undefined,
|
queryResults: undefined,
|
||||||
error: "",
|
error: "",
|
||||||
@@ -469,7 +468,6 @@ 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,
|
||||||
@@ -498,13 +496,16 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onChangeContent(newContent: string): void {
|
public onChangeContent(newContent: string): void {
|
||||||
|
// The copilot store's active query takes precedence over the local state,
|
||||||
|
// and we can't update both states in a single operation.
|
||||||
|
// So, we update the copilot store's state first, then update the local state.
|
||||||
|
if (this.state.copilotActive) {
|
||||||
|
this.props.copilotStore?.setQuery(newContent);
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
sqlQueryEditorContent: newContent,
|
sqlQueryEditorContent: newContent,
|
||||||
queryCopilotGeneratedQuery: "",
|
queryCopilotGeneratedQuery: "",
|
||||||
});
|
});
|
||||||
if (this.state.copilotActive) {
|
|
||||||
this.props.copilotStore?.setQuery(newContent);
|
|
||||||
}
|
|
||||||
if (this.isPreferredApiMongoDB) {
|
if (this.isPreferredApiMongoDB) {
|
||||||
if (newContent.length > 0) {
|
if (newContent.length > 0) {
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
@@ -546,7 +547,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEditorContent(): string {
|
public getEditorContent(): string {
|
||||||
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
|
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
|
||||||
return this.state.queryCopilotGeneratedQuery;
|
return this.state.queryCopilotGeneratedQuery;
|
||||||
}
|
}
|
||||||
@@ -586,15 +587,6 @@ 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">
|
||||||
@@ -612,15 +604,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
<div className="queryEditor" style={{ height: "100%" }}>
|
<div className="queryEditor" style={{ height: "100%" }}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
language={"sql"}
|
language={"sql"}
|
||||||
content={this.setEditorContent()}
|
content={this.getEditorContent()}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
wordWrap={"on"}
|
wordWrap={"on"}
|
||||||
ariaLabel={"Editing Query"}
|
ariaLabel={"Editing Query"}
|
||||||
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 ? (
|
||||||
|
|||||||
@@ -238,7 +238,6 @@ 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,
|
||||||
|
|||||||
@@ -347,7 +347,6 @@ 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,
|
||||||
|
|||||||
@@ -324,13 +324,17 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
|
|
||||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||||
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
|
if (
|
||||||
|
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.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 =
|
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule))
|
ipAddressesFromIPRules.includes(legacyPortalBackendIP),
|
||||||
?.length === legacyPortalBackendIPs.length;
|
);
|
||||||
|
|
||||||
if (!ipRulesIncludeLegacyPortalBackend) {
|
if (!ipRulesIncludeLegacyPortalBackend) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -344,9 +348,9 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||||
|
|
||||||
const ipRulesIncludeMongoProxy: boolean =
|
const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
|
||||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule))
|
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||||
?.length === mongoProxyOutboundIPs.length;
|
);
|
||||||
|
|
||||||
if (ipRulesIncludeMongoProxy) {
|
if (ipRulesIncludeMongoProxy) {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
@@ -368,9 +372,15 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
]
|
]
|
||||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||||
|
|
||||||
const ipRulesIncludeCassandraProxy: boolean =
|
const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every(
|
||||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule))
|
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||||
?.length === cassandraProxyOutboundIPs.length;
|
);
|
||||||
|
|
||||||
|
if (ipRulesIncludeCassandraProxy) {
|
||||||
|
updateConfigContext({
|
||||||
|
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return !ipRulesIncludeCassandraProxy;
|
return !ipRulesIncludeCassandraProxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ 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,
|
||||||
|
|||||||
@@ -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,7 +109,6 @@ 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,
|
||||||
|
|||||||
@@ -373,11 +373,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
iconSrc: NewNotebookIcon,
|
iconSrc: NewNotebookIcon,
|
||||||
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "New Notebook",
|
|
||||||
iconSrc: NewNotebookIcon,
|
|
||||||
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Upload File",
|
label: "Upload File",
|
||||||
iconSrc: NewNotebookIcon,
|
iconSrc: NewNotebookIcon,
|
||||||
@@ -786,9 +781,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
|
||||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
|
||||||
</AccordionItemComponent>
|
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
|
|
||||||
{/* {buildGalleryCallout()} */}
|
{/* {buildGalleryCallout()} */}
|
||||||
|
|||||||
@@ -800,11 +800,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
iconSrc: NewNotebookIcon,
|
iconSrc: NewNotebookIcon,
|
||||||
onClick: () => this.container.onCreateDirectory(item),
|
onClick: () => this.container.onCreateDirectory(item),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "New Notebook",
|
|
||||||
iconSrc: NewNotebookIcon,
|
|
||||||
onClick: () => this.container.onNewNotebookClicked(item),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Upload File",
|
label: "Upload File",
|
||||||
iconSrc: NewNotebookIcon,
|
iconSrc: NewNotebookIcon,
|
||||||
|
|||||||
26
src/Main.tsx
26
src/Main.tsx
@@ -1,3 +1,6 @@
|
|||||||
|
// Import this first, to ensure that the dev tools hook is copied before React is loaded.
|
||||||
|
import "./ReactDevTools";
|
||||||
|
|
||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
@@ -18,7 +21,6 @@ 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";
|
||||||
@@ -62,28 +64,6 @@ 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);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { usePortalBackendEndpoint } from "Utils/EndpointUtils";
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||||
import ErrorImage from "../../../../images/error.svg";
|
import ErrorImage from "../../../../images/error.svg";
|
||||||
@@ -19,7 +19,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||||
if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) {
|
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
if (window.parent !== window) {
|
if (window.parent !== window) {
|
||||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
try {
|
||||||
|
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||||
|
} catch {
|
||||||
|
// No-op. We can throw here if the parent is not the same origin (such as in the Azure portal).
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,8 +145,22 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
|||||||
|
|
||||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||||
|
|
||||||
export function usePortalBackendEndpoint(backendApi: BackendApi): boolean {
|
//
|
||||||
const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development];
|
// Temporary function to determine if a portal backend API is supported by the
|
||||||
const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false;
|
// new backend in this environment.
|
||||||
return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as string);
|
//
|
||||||
|
// TODO: Remove this function once new backend migration is completed for all environments.
|
||||||
|
//
|
||||||
|
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
||||||
|
// This maps backend APIs to the environments supported by the new backend.
|
||||||
|
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
|
||||||
|
[BackendApi.GenerateToken]: [PortalBackendEndpoints.Development],
|
||||||
|
[BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -497,6 +497,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||||
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
||||||
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
|
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
|
||||||
|
PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import "expect-playwright";
|
import "expect-playwright";
|
||||||
import { generateUniqueName } from "../utils/shared";
|
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
test("Cassandra keyspace and table CRUD", async () => {
|
test("Cassandra keyspace and table CRUD", async () => {
|
||||||
const keyspaceId = generateUniqueName("keyspace");
|
const keyspaceId = generateUniqueName("keyspace");
|
||||||
const tableId = generateUniqueName("table");
|
const tableId = generateUniqueName("table");
|
||||||
|
|
||||||
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
|
const token = await getAzureCLICredentialsToken();
|
||||||
page.setDefaultTimeout(50000);
|
page.setDefaultTimeout(50000);
|
||||||
|
|
||||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner");
|
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`);
|
||||||
await page.waitForSelector("iframe");
|
await page.waitForSelector("iframe");
|
||||||
const explorer = await waitForExplorer();
|
const explorer = await waitForExplorer();
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import "expect-playwright";
|
import "expect-playwright";
|
||||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||||
jest.setTimeout(240000);
|
jest.setTimeout(240000);
|
||||||
|
|
||||||
test("Graph CRUD", async () => {
|
test("Graph CRUD", async () => {
|
||||||
const databaseId = generateDatabaseNameWithTimestamp();
|
const databaseId = generateDatabaseNameWithTimestamp();
|
||||||
const containerId = generateUniqueName("container");
|
const containerId = generateUniqueName("container");
|
||||||
|
|
||||||
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
|
const token = await getAzureCLICredentialsToken();
|
||||||
page.setDefaultTimeout(50000);
|
page.setDefaultTimeout(50000);
|
||||||
|
|
||||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner");
|
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`);
|
||||||
const explorer = await waitForExplorer();
|
const explorer = await waitForExplorer();
|
||||||
|
|
||||||
// Create new database and graph
|
// Create new database and graph
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import "expect-playwright";
|
import "expect-playwright";
|
||||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||||
jest.setTimeout(240000);
|
jest.setTimeout(240000);
|
||||||
|
|
||||||
test("Mongo CRUD", async () => {
|
test("Mongo CRUD", async () => {
|
||||||
const databaseId = generateDatabaseNameWithTimestamp();
|
const databaseId = generateDatabaseNameWithTimestamp();
|
||||||
const containerId = generateUniqueName("container");
|
const containerId = generateUniqueName("container");
|
||||||
|
|
||||||
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
|
const token = await getAzureCLICredentialsToken();
|
||||||
page.setDefaultTimeout(50000);
|
page.setDefaultTimeout(50000);
|
||||||
|
|
||||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
|
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`);
|
||||||
const explorer = await waitForExplorer();
|
const explorer = await waitForExplorer();
|
||||||
|
|
||||||
// Create new database and collection
|
// Create new database and collection
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import "expect-playwright";
|
import "expect-playwright";
|
||||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||||
jest.setTimeout(240000);
|
jest.setTimeout(240000);
|
||||||
|
|
||||||
test("Mongo CRUD", async () => {
|
test("Mongo CRUD", async () => {
|
||||||
const databaseId = generateDatabaseNameWithTimestamp();
|
const databaseId = generateDatabaseNameWithTimestamp();
|
||||||
const containerId = generateUniqueName("container");
|
const containerId = generateUniqueName("container");
|
||||||
|
|
||||||
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
|
const token = await getAzureCLICredentialsToken();
|
||||||
page.setDefaultTimeout(50000);
|
page.setDefaultTimeout(50000);
|
||||||
|
|
||||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
|
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`);
|
||||||
const explorer = await waitForExplorer();
|
const explorer = await waitForExplorer();
|
||||||
|
|
||||||
// Create new database and collection
|
// Create new database and collection
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import { getAzureCLICredentialsToken } from "../utils/shared";
|
||||||
|
|
||||||
test("Self Serve", async () => {
|
test("Self Serve", async () => {
|
||||||
await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
|
const token = await getAzureCLICredentialsToken();
|
||||||
|
|
||||||
|
await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`);
|
||||||
const handle = await page.waitForSelector("iframe");
|
const handle = await page.waitForSelector("iframe");
|
||||||
const frame = await handle.contentFrame();
|
const frame = await handle.contentFrame();
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import "expect-playwright";
|
import "expect-playwright";
|
||||||
import { generateUniqueName } from "../utils/shared";
|
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
test("SQL CRUD", async () => {
|
test("SQL CRUD", async () => {
|
||||||
const databaseId = generateUniqueName("db");
|
const databaseId = generateUniqueName("db");
|
||||||
const containerId = generateUniqueName("container");
|
const containerId = generateUniqueName("container");
|
||||||
|
|
||||||
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
|
const token = await getAzureCLICredentialsToken();
|
||||||
page.setDefaultTimeout(50000);
|
page.setDefaultTimeout(50000);
|
||||||
|
|
||||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
|
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`);
|
||||||
const explorer = await waitForExplorer();
|
const explorer = await waitForExplorer();
|
||||||
|
|
||||||
await explorer.click('[data-test="New Container"]');
|
await explorer.click('[data-test="New Container"]');
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||||
import { CosmosClient, PermissionMode } from "@azure/cosmos";
|
import { CosmosClient, PermissionMode } from "@azure/cosmos";
|
||||||
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
|
|
||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import "expect-playwright";
|
import "expect-playwright";
|
||||||
import { generateUniqueName } from "../utils/shared";
|
import { generateUniqueName, getAzureCLICredentials } from "../utils/shared";
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4";
|
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? "";
|
||||||
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
|
|
||||||
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
|
|
||||||
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
|
|
||||||
const resourceGroupName = "runners";
|
const resourceGroupName = "runners";
|
||||||
|
|
||||||
test("Resource token", async () => {
|
test("Resource token", async () => {
|
||||||
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
|
const credentials = await getAzureCLICredentials();
|
||||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||||
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
|
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
|
||||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");
|
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import "expect-playwright";
|
import "expect-playwright";
|
||||||
import { generateUniqueName } from "../utils/shared";
|
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||||
|
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
test("Tables CRUD", async () => {
|
test("Tables CRUD", async () => {
|
||||||
const tableId = generateUniqueName("table");
|
const tableId = generateUniqueName("table");
|
||||||
|
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||||
|
const token = await getAzureCLICredentialsToken();
|
||||||
page.setDefaultTimeout(50000);
|
page.setDefaultTimeout(50000);
|
||||||
|
|
||||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
|
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`);
|
||||||
const explorer = await waitForExplorer();
|
const explorer = await waitForExplorer();
|
||||||
|
|
||||||
await page.waitForSelector('text="Querying databases"', { state: "detached" });
|
await page.waitForSelector('text="Querying databases"', { state: "detached" });
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { ClientSecretCredential } from "@azure/identity";
|
|
||||||
import "../../less/hostedexplorer.less";
|
import "../../less/hostedexplorer.less";
|
||||||
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
|
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../src/UserContext";
|
import { updateUserContext } from "../../src/UserContext";
|
||||||
@@ -11,29 +10,13 @@ const urlSearchParams = new URLSearchParams(window.location.search);
|
|||||||
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
|
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
|
||||||
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
||||||
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
||||||
|
const token = urlSearchParams.get("token");
|
||||||
if (!process.env.AZURE_CLIENT_SECRET) {
|
|
||||||
throw new Error(
|
|
||||||
"process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Azure SDK clients accept the credential as a parameter
|
|
||||||
const credentials = new ClientSecretCredential(
|
|
||||||
process.env.AZURE_TENANT_ID,
|
|
||||||
process.env.AZURE_CLIENT_ID,
|
|
||||||
process.env.AZURE_CLIENT_SECRET,
|
|
||||||
{
|
|
||||||
authorityHost: "https://localhost:1234",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Resource Group:", resourceGroup);
|
console.log("Resource Group:", resourceGroup);
|
||||||
console.log("Subcription: ", subscriptionId);
|
console.log("Subcription: ", subscriptionId);
|
||||||
console.log("Account Name: ", accountName);
|
console.log("Account Name: ", accountName);
|
||||||
|
|
||||||
const initTestExplorer = async (): Promise<void> => {
|
const initTestExplorer = async (): Promise<void> => {
|
||||||
const { token } = await credentials.getToken("https://management.azure.com//.default");
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authorizationToken: `bearer ${token}`,
|
authorizationToken: `bearer ${token}`,
|
||||||
});
|
});
|
||||||
@@ -52,6 +35,9 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
dnsSuffix: "documents.azure.com",
|
dnsSuffix: "documents.azure.com",
|
||||||
serverId: "prod1",
|
serverId: "prod1",
|
||||||
extensionEndpoint: "/proxy",
|
extensionEndpoint: "/proxy",
|
||||||
|
portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||||
|
mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com",
|
||||||
|
cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com",
|
||||||
subscriptionType: 3,
|
subscriptionType: 3,
|
||||||
quotaId: "Internal_2014-09-01",
|
quotaId: "Internal_2014-09-01",
|
||||||
isTryCosmosDBSubscription: false,
|
isTryCosmosDBSubscription: false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
export function generateUniqueName(baseName = "", length = 4): string {
|
export function generateUniqueName(baseName = "", length = 4): string {
|
||||||
@@ -7,3 +8,13 @@ export function generateUniqueName(baseName = "", length = 4): string {
|
|||||||
export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string {
|
export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string {
|
||||||
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
|
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAzureCLICredentials(): Promise<AzureCliCredentials> {
|
||||||
|
return await AzureCliCredentials.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAzureCLICredentialsToken(): Promise<string> {
|
||||||
|
const credentials = await getAzureCLICredentials();
|
||||||
|
const token = (await credentials.getToken()).accessToken;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
|
|||||||
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
||||||
const ms = require("ms");
|
const ms = require("ms");
|
||||||
|
|
||||||
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
|
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"];
|
||||||
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
|
|
||||||
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
|
|
||||||
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
|
|
||||||
const resourceGroupName = "runners";
|
const resourceGroupName = "runners";
|
||||||
|
|
||||||
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
|
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
|
||||||
@@ -19,7 +16,7 @@ function friendlyTime(date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
|
const credentials = await msRestNodeAuth.AzureCliCredentials.create();
|
||||||
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||||
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
@@ -38,7 +35,7 @@ async function main() {
|
|||||||
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
|
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
|
||||||
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
|
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
|
||||||
resourceGroupName,
|
resourceGroupName,
|
||||||
account.name
|
account.name,
|
||||||
);
|
);
|
||||||
for (const database of cassandraDatabases) {
|
for (const database of cassandraDatabases) {
|
||||||
const timestamp = Number(database.resource._ts) * 1000;
|
const timestamp = Number(database.resource._ts) * 1000;
|
||||||
|
|||||||
Reference in New Issue
Block a user