Compare commits

...

7 Commits

Author SHA1 Message Date
Ashley Stanton-Nurse
2089d8ca9e a system of contributors? 2024-04-15 15:45:22 -07:00
Ashley Stanton-Nurse
e441b75325 initial mousetrap keyboard shortcuts 2024-04-15 15:02:10 -07:00
JustinKol
d35e2a325e Cassandra API create table error messages swallowed by the Portal and shown as "undefined". (#1790)
* changed error message variable

* changed other error messages

* Added check in case responseJSON is missing

* created error const
2024-04-15 15:47:58 -04:00
sunghyunkang1111
00a816c488 set the value in the editor for results (#1799) 2024-04-13 15:19:56 -05:00
jawelton74
953bef404b Set backend endpoints in testExplorer to use MPAC. (#1797) 2024-04-11 15:43:46 -07:00
Asier Isayas
dfcb771939 Activate Mongo Proxy and Cassandra Proxy in Prod (#1794)
* activate Mongo Proxy and Cassandra Proxy in Prod

* fix bug that blocked local mongo proxy and cassandra proxy development

* fix pr check tests

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-04-09 13:45:36 -07:00
jawelton74
6925fa8e4e Replace Entra app client secret auth with OpenID Connect in E2E tests. (#1792)
* Use Az login with OpenID connection to get test credentials.

* Set subscription id environment variable.

* Update testExplorer and cleanup job.

* Retrieve access token in test case and pass to testExplorer.

* Add debug tracing for tests.

* Set up other mongo test to use Az CLI creds.

* Revert subscription id retrieval.

* Add CLI credentials retrieval to rest of tests.

* Fix missing imports.

* Clean up redundant code.

* Remove commented import statement.
2024-04-09 10:55:08 -07:00
25 changed files with 335 additions and 78 deletions

View File

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

View File

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

13
package-lock.json generated
View File

@@ -82,6 +82,7 @@
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"mousetrap": "1.6.5",
"ms": "2.1.3",
"p-retry": "4.6.2",
"patch-package": "8.0.0",
@@ -128,6 +129,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
"@types/mousetrap": "1.6.15",
"@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
@@ -12792,6 +12794,12 @@
"@types/node": "*"
}
},
"node_modules/@types/mousetrap": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.15.tgz",
"integrity": "sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==",
"dev": true
},
"node_modules/@types/node": {
"version": "12.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz",
@@ -31631,6 +31639,11 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
"integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA=="
},
"node_modules/mrmime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",

View File

@@ -77,6 +77,7 @@
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"mousetrap": "1.6.5",
"ms": "2.1.3",
"p-retry": "4.6.2",
"patch-package": "8.0.0",
@@ -123,6 +124,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
"@types/mousetrap": "1.6.15",
"@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",

View File

@@ -690,9 +690,16 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
}
function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac];
const activeMongoProxyEndpoints: string[] = [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
];
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;
}

View File

@@ -1,6 +1,8 @@
/**
* React component for Command button component.
*/
import { KeyboardShortcutAction } from "KeyboardShortcuts";
import { ExtendedKeyboardEvent } from "mousetrap";
import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants";
@@ -30,7 +32,7 @@ export interface CommandButtonComponentProps {
/**
* Click handler for command button click
*/
onCommandClick: (e: React.SyntheticEvent) => void;
onCommandClick: (e: React.SyntheticEvent | ExtendedKeyboardEvent) => void;
/**
* Label for the button
@@ -107,10 +109,13 @@ export interface CommandButtonComponentProps {
* Vertical bar to divide buttons
*/
isDivider?: boolean;
/**
* Aria-label for the button
*/
ariaLabel: string;
keyboardShortcut?: KeyboardShortcutAction;
}
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {

View File

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

View File

@@ -5,6 +5,7 @@
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardShortcutAction, KeyboardShortcutContributor, KeyboardShortcutHandler, useKeyboardShortcutContributor } from "KeyboardShortcuts";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
@@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight;
const setKeyboardShortcutHandlers = useKeyboardShortcutContributor(KeyboardShortcutContributor.COMMAND_BAR);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
@@ -105,6 +107,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
},
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>> = {};
allButtons.forEach((button) => {
if(button.keyboardShortcut) {
handlers[button.keyboardShortcut] = (e) => {
button.onCommandClick(e);
return false;
}
}
});
setKeyboardShortcutHandlers(handlers);
return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar

View File

@@ -1,3 +1,4 @@
import { KeyboardShortcutAction } from "KeyboardShortcuts";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
@@ -297,6 +298,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
keyboardShortcut: KeyboardShortcutAction.NEW_QUERY,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@@ -312,6 +314,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
keyboardShortcut: KeyboardShortcutAction.NEW_QUERY,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);

View File

@@ -3,6 +3,7 @@ import * as ko from "knockout";
import Q from "q";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
import { handleError } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { createDocument } from "../../Common/dataAccess/createDocument";
@@ -19,7 +20,6 @@ import Explorer from "../Explorer";
import * as TableConstants from "./Constants";
import * as Entities from "./Entities";
import * as TableEntityProcessor from "./TableEntityProcessor";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[];
@@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity);
},
(error) => {
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(
error,
errorText,
"CreateKeyspaceCassandra",
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
);
deferred.reject(error);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(
errorText,
"CreateTableCassandra",
`Error while creating a table with query ${createTableQuery}`,
);
deferred.reject(errorText);
},
)
.finally(clearInProgressMessage);
@@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data);
},
(error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data);
},
(error: any) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns);
},
(error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns);
},
(error: any) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(error);
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
},
)
.done(clearInProgressMessage);
@@ -732,9 +743,16 @@ export class CassandraAPIDataClient extends TableDataClient {
}
private useCassandraProxyEndpoint(api: string): boolean {
const activeCassandraProxyEndpoints: string[] = [CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Mpac];
const activeCassandraProxyEndpoints: string[] = [
CassandraProxyEndpoints.Development,
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
];
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;
}

View File

@@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardShortcutAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -393,6 +394,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
keyboardShortcut: KeyboardShortcutAction.EXECUTE_ITEM,
onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick,

View File

@@ -324,7 +324,12 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
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 ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>

150
src/KeyboardShortcuts.tsx Normal file
View File

@@ -0,0 +1,150 @@
import Mousetrap, { ExtendedKeyboardEvent } from "mousetrap";
import * as React from "react";
import create, { UseStore } from "zustand";
// Provides a system of Keyboard Shortcuts that can be contributed to by different parts of the application.
//
// The goals of this system are:
// * Shortcuts can be contributed from different parts of the application (e.g. the command bar, specified editor tabs, etc.)
// * Contributors may only provide some of their shortcuts, others may be out-of-scope for the current context.
// * Contributors don't have to add/remove handlers individually, they can use a declarative pattern to set all their handlers at once.
//
// So, in order to do that, we store handlers in a two-level hierarchy:
// 1. We store a mapping of contributors to their Contributions.
// 2. Each Contribution is a mapping of actions to their handlers.
//
// Thus, a Contributor sets all its handlers at once, replacing all handlers previously contributed by that Contributor.
// The system then merges all Contributions into a single set of handlers, with duplicate handlers being handled in the order that the Contributors are processed.
export type KeyboardShortcutHandler = (e: ExtendedKeyboardEvent, combo: string) => boolean | void;
/**
* Lists all the possible contributors to keyboard shortcut handlers.
*
* A "Contributor" is a part of the application that can contribute keyboard shortcut handlers.
* The contributor must set all it's keyboard shortcut handlers at once.
* This allows the contributor to easily replace all it's keyboard shortcuts at once.
*
* For example, the command bar adds/removes keyboard shortcut handlers based on the current context, using the existing logic that determines which buttons are shown.
* Isolating contributors like this allow the command bar to easily replace all it's keyboard shortcuts when the context changes without breaking keyboard shortcuts contributed by other parts of the application.
*/
export enum KeyboardShortcutContributor {
COMMAND_BAR = "COMMAND_BAR",
}
/**
* The order in which contributors are processed.
* This is important because the last contributor to set a handler for a given action will be the one that is used.
*/
const contributorOrder: KeyboardShortcutContributor[] = [
KeyboardShortcutContributor.COMMAND_BAR,
];
/**
* The possible actions that can be triggered by keyboard shortcuts.
*/
export enum KeyboardShortcutAction {
NEW_QUERY = "NEW_QUERY",
EXECUTE_ITEM = "EXECUTE_ITEM",
}
/**
* The default keyboard shortcuts for the application.
* This record maps each action to the keyboard shortcuts that trigger the action.
* Even if an action is specified here though, it will not be triggered unless a handler is set for it.
*/
const bindings: Record<KeyboardShortcutAction, string[]> = {
[KeyboardShortcutAction.NEW_QUERY]: ["ctrl+j"],
[KeyboardShortcutAction.EXECUTE_ITEM]: ["shift+enter"],
};
/**
* Represents all the handlers provided by a contributor.
*/
export type KeyboardShortcutContribution = Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>;
interface KeyboardShortcutState {
/**
* Collects all the contributions from different contributors.
*/
contributions: Partial<Record<KeyboardShortcutContributor, KeyboardShortcutContribution>>;
/**
* A merged set of all the handlers from all contributors.
*/
allHandlers: KeyboardShortcutContribution;
/**
* Sets the keyboard shortcut handlers for a given contributor.
*/
setHandlers: (contributor: KeyboardShortcutContributor, handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => void;
}
/**
* Gets the setHandlers function for a given contributor.
* @param contributor The contributor to get the setHandlers function for.
* @returns A function that sets the keyboard shortcut handlers for the given contributor.
*/
export const useKeyboardShortcutContributor = (contributor: KeyboardShortcutContributor) => {
const setHandlers = useKeyboardShortcutHandlers.getState().setHandlers;
return (handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => {
setHandlers(contributor, handlers);
};
}
const useKeyboardShortcutHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
contributions: {},
allHandlers: {},
setHandlers: (contributor: KeyboardShortcutContributor, handlers: Partial<Record<KeyboardShortcutAction, KeyboardShortcutHandler>>) => {
const current = get();
// Update the list of contributions.
const newContributions = { ...current.contributions, [contributor]: handlers };
// Merge all the contributions into a single set of handlers.
const allHandlers: KeyboardShortcutContribution = {};
contributorOrder.forEach((contributor) => {
const contribution = newContributions[contributor];
if (contribution) {
(Object.keys(contribution) as KeyboardShortcutAction[]).forEach((action) => {
allHandlers[action] = contribution[action];
});
}
});
set({ contributions: newContributions, allHandlers })
}
}));
function createHandler(action: KeyboardShortcutAction): KeyboardShortcutHandler {
return (e, combo) => {
const handlers = useKeyboardShortcutHandlers.getState().allHandlers;
const handler = handlers[action];
if (handler) {
return handler(e, combo);
}
};
}
export function KeyboardShortcutRoot(props: React.HTMLProps<HTMLDivElement>) {
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const m = new Mousetrap(ref.current);
const existingStopCallback = m.stopCallback;
m.stopCallback = (e, element, combo) => {
// Don't block mousetrap callback in the Monaco editor.
if (element.matches(".monaco-editor textarea")) {
return false;
}
return existingStopCallback(e, element, combo);
};
(Object.keys(bindings) as KeyboardShortcutAction[]).forEach((action) => {
m.bind(bindings[action], createHandler(action));
});
}, [ref]); // We only need to re-render the component when the ref changes.
return <div ref={ref} {...props}>
</div>;
}

View File

@@ -21,6 +21,7 @@ import "../externals/jquery.typeahead.min.js";
// Image Dependencies
import { Platform } from "ConfigContext";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import "../images/favicon.ico";
@@ -91,7 +92,7 @@ const App: React.FunctionComponent = () => {
}
return (
<div className="flexContainer" aria-hidden="false">
<KeyboardShortcutRoot className="flexContainer" aria-hidden="false">
<div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */}
@@ -136,7 +137,7 @@ const App: React.FunctionComponent = () => {
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
</KeyboardShortcutRoot>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,10 @@
import { getAzureCLICredentialsToken } from "../utils/shared";
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 frame = await handle.contentFrame();

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
/* eslint-disable no-console */
import { ClientSecretCredential } from "@azure/identity";
import "../../less/hostedexplorer.less";
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
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 selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
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",
},
);
const token = urlSearchParams.get("token");
console.log("Resource Group:", resourceGroup);
console.log("Subcription: ", subscriptionId);
console.log("Account Name: ", accountName);
const initTestExplorer = async (): Promise<void> => {
const { token } = await credentials.getToken("https://management.azure.com//.default");
updateUserContext({
authorizationToken: `bearer ${token}`,
});
@@ -52,6 +35,9 @@ const initTestExplorer = async (): Promise<void> => {
dnsSuffix: "documents.azure.com",
serverId: "prod1",
extensionEndpoint: "/proxy",
portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com",
cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com",
subscriptionType: 3,
quotaId: "Internal_2014-09-01",
isTryCosmosDBSubscription: false,

View File

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

View File

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