Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate/QueryTablesTab

This commit is contained in:
hardiknai-techm
2021-07-02 17:06:58 +05:30
171 changed files with 5256 additions and 7160 deletions
-10
View File
@@ -158,16 +158,6 @@ export class DocumentsGridMetrics {
public static DocumentEditorMaxWidthRatio: number = 0.4;
}
export class ExplorerMetrics {
public static SplitterMinWidth: number = 240;
public static SplitterMaxWidth: number = 400;
public static CollapsedResourceTreeWidth: number = 36;
}
export class SplitterMetrics {
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
}
export class Areas {
public static ResourceTree: string = "Resource Tree";
public static ContextualPane: string = "Contextual Pane";
-1
View File
@@ -5,7 +5,6 @@ import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { updateUserContext } from "../UserContext";
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
const databaseId = "testDB";
+11 -11
View File
@@ -111,7 +111,7 @@ export function queryDocuments(
headers: response.headers,
};
}
errorHandling(response, "querying documents", params);
await errorHandling(response, "querying documents", params);
return undefined;
});
}
@@ -153,11 +153,11 @@ export function readDocument(
),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "reading document", params);
return await errorHandling(response, "reading document", params);
});
}
@@ -192,11 +192,11 @@ export function createDocument(
...authHeaders(),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "creating document", params);
return await errorHandling(response, "creating document", params);
});
}
@@ -238,11 +238,11 @@ export function updateDocument(
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "updating document", params);
return await errorHandling(response, "updating document", params);
});
}
@@ -278,11 +278,11 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return undefined;
}
return errorHandling(response, "deleting document", params);
return await errorHandling(response, "deleting document", params);
});
}
@@ -325,11 +325,11 @@ export function createMongoCollectionWithProxy(
},
}
)
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "creating collection", mongoParams);
return await errorHandling(response, "creating collection", mongoParams);
});
}
+27 -39
View File
@@ -1,19 +1,17 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId";
import { useDatabases } from "../Explorer/useDatabases";
import { userContext } from "../UserContext";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { createCollection } from "./dataAccess/createCollection";
import { createDocument } from "./dataAccess/createDocument";
import { deleteDocument } from "./dataAccess/deleteDocument";
import { queryDocuments } from "./dataAccess/queryDocuments";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
import { handleError } from "./ErrorHandlingUtils";
export class QueriesClient {
@@ -100,45 +98,35 @@ export class QueriesClient {
const options: any = { enableCrossPartitionQuery: true };
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries");
const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
const results = await queryDocuments(
SavedQueries.DatabaseName,
SavedQueries.CollectionName,
this.fetchQueriesQuery(),
options
);
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
return QueryUtils.queryAllPages(fetchQueries)
.then(
(results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
if (!document) {
return undefined;
}
const { id, resourceId, query, queryName } = document;
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id,
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
return Promise.resolve(queries);
},
(error: any) => {
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
return Promise.reject(error);
}
)
.finally(() => clearMessage());
).fetchAll();
let queries: DataModels.Query[] = _.map(results.resources, (document: DataModels.Query) => {
if (!document) {
return undefined;
}
const { id, resourceId, query, queryName } = document;
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id,
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
clearMessage();
return queries;
}
public async deleteQuery(query: DataModels.Query): Promise<void> {
@@ -189,7 +177,7 @@ export class QueriesClient {
private findQueriesCollection(): ViewModels.Collection {
const queriesDatabase: ViewModels.Database = _.find(
this.container.databases(),
useDatabases.getState().databases,
(database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName
);
if (!queriesDatabase) {
-25
View File
@@ -1,7 +1,3 @@
import * as ko from "knockout";
import { SplitterMetrics } from "./Constants";
export enum SplitterDirection {
Horizontal = "horizontal",
Vertical = "vertical",
@@ -28,14 +24,12 @@ export class Splitter {
public lastX!: number;
public lastWidth!: number;
private isCollapsed: ko.Observable<boolean>;
private bounds: SplitterBounds;
private direction: SplitterDirection;
constructor(options: SplitterOptions) {
this.splitterId = options.splitterId;
this.leftSideId = options.leftId;
this.isCollapsed = ko.observable<boolean>(false);
this.bounds = options.bounds;
this.direction = options.direction;
this.initialize();
@@ -83,23 +77,4 @@ export class Splitter {
};
private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto");
public collapseLeft() {
this.lastX = $(this.splitter).position().left;
this.lastWidth = $(this.leftSide).width();
$(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft);
$(this.leftSide).css("width", "");
$(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible
$(this.splitter).removeClass("ui-resizable-e");
this.isCollapsed(true);
}
public expandLeft() {
$(this.splitter).addClass("ui-resizable-e");
$(this.leftSide).css("width", this.lastWidth);
$(this.splitter).css("left", this.lastX);
$(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing
$(this.leftSide).resizable("enable");
this.isCollapsed(false);
}
}
+8
View File
@@ -120,6 +120,14 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
const armAPIVersion = params.get("armAPIVersion") || "";
updateConfigContext({ armAPIVersion });
}
if (params.has("armEndpoint")) {
const ARM_ENDPOINT = params.get("armEndpoint") || "";
updateConfigContext({ ARM_ENDPOINT });
}
if (params.has("aadEndpoint")) {
const AAD_ENDPOINT = params.get("aadEndpoint") || "";
updateConfigContext({ AAD_ENDPOINT });
}
if (params.has("platform")) {
const platform = params.get("platform");
switch (platform) {
+2 -3
View File
@@ -6,7 +6,7 @@ import {
UserDefinedFunctionDefinition,
} from "@azure/cosmos";
import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
@@ -89,7 +89,6 @@ export interface Database extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
selectDatabase(): void;
expandDatabase(): Promise<void>;
collapseDatabase(): void;
@@ -275,7 +274,6 @@ export interface TabOptions {
tabKind: CollectionTabKind;
title: string;
tabPath: string;
hashLocation: string;
isTabsContentExpanded?: ko.Observable<boolean>;
onLoadStartKey?: number;
@@ -286,6 +284,7 @@ export interface TabOptions {
rid?: string;
node?: TreeNode;
theme?: string;
index?: number;
}
export interface DocumentsTabOptions extends TabOptions {
-172
View File
@@ -1,172 +0,0 @@
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import AddUdfIcon from "../../images/AddUdf.svg";
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
export interface CollectionContextMenuButtonParams {
databaseId: string;
collectionId: string;
}
export interface DatabaseContextMenuButtonParams {
databaseId: string;
}
/**
* New resource tree (in ReactJS)
*/
export class ResourceTreeContextMenuButtonFactory {
public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(databaseId),
label: `New ${getCollectionName()}`,
},
];
if (userContext.apiType !== "Tables") {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () => container.openDeleteDatabaseConfirmationPane(),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
}
return items;
}
public static createCollectionContextMenuButton(
container: Explorer,
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
label: "New SQL Query",
});
}
if (userContext.apiType === "Mongo") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),
label: "New Query",
});
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
},
label: "New Trigger",
});
}
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () => container.openDeleteCollectionConfirmationPane(),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
return items;
}
public static createStoreProcedureContextMenuItems(
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: "Delete Store Procedure",
},
];
}
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: "Delete Trigger",
},
];
}
public static createUserDefinedFunctionContextMenuItems(
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: "Delete User Defined Function",
},
];
}
}
+182
View File
@@ -0,0 +1,182 @@
import React from "react";
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import AddUdfIcon from "../../images/AddUdf.svg";
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { useSidePanel } from "../hooks/useSidePanel";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import { useSelectedNode } from "./useSelectedNode";
export interface CollectionContextMenuButtonParams {
databaseId: string;
collectionId: string;
}
export interface DatabaseContextMenuButtonParams {
databaseId: string;
}
/**
* New resource tree (in ReactJS)
*/
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
const items: TreeNodeMenuItem[] = [
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(databaseId),
label: `New ${getCollectionName()}`,
},
];
if (userContext.apiType !== "Tables") {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () =>
useSidePanel
.getState()
.openSidePanel("Delete " + getDatabaseName(), <DeleteDatabaseConfirmationPanel explorer={container} />),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
}
return items;
};
export const createCollectionContextMenuButton = (
container: Explorer,
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] => {
const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
label: "New SQL Query",
});
}
if (userContext.apiType === "Mongo") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
label: "New Query",
});
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: "New Trigger",
});
}
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () =>
useSidePanel
.getState()
.openSidePanel("Delete " + getCollectionName(), <DeleteCollectionConfirmationPane explorer={container} />),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
return items;
};
export const createStoreProcedureContextMenuItems = (
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] => {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: "Delete Store Procedure",
},
];
};
export const createTriggerContextMenuItems = (container: Explorer, trigger: Trigger): TreeNodeMenuItem[] => {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: "Delete Trigger",
},
];
};
export const createUserDefinedFunctionContextMenuItems = (
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] => {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: "Delete User Defined Function",
},
];
};
+25 -5
View File
@@ -1,6 +1,11 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react";
import { loadMonaco, monaco } from "../../LazyMonaco";
// import "./EditorReact.less";
interface EditorReactStates {
showEditor: boolean;
}
export interface EditorReactProps {
language: string;
content: string;
@@ -12,22 +17,26 @@ export interface EditorReactProps {
theme?: string; // Monaco editor theme
}
export class EditorReact extends React.Component<EditorReactProps> {
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
private rootNode: HTMLElement;
private editor: monaco.editor.IStandaloneCodeEditor;
private selectionListener: monaco.IDisposable;
public constructor(props: EditorReactProps) {
super(props);
this.state = {
showEditor: false,
};
}
public componentDidMount(): void {
this.createEditor(this.configureEditor.bind(this));
}
public shouldComponentUpdate(): boolean {
// Prevents component re-rendering
return false;
public componentDidUpdate(previous: EditorReactProps) {
if (this.props.content !== previous.content) {
this.editor.setValue(this.props.content);
}
}
public componentWillUnmount(): void {
@@ -35,7 +44,12 @@ export class EditorReact extends React.Component<EditorReactProps> {
}
public render(): JSX.Element {
return <div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />;
return (
<React.Fragment>
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
<div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />
</React.Fragment>
);
}
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
@@ -76,6 +90,12 @@ export class EditorReact extends React.Component<EditorReactProps> {
this.rootNode.innerHTML = "";
const monaco = await loadMonaco();
createCallback(monaco.editor.create(this.rootNode, options));
if (this.rootNode.innerHTML) {
this.setState({
showEditor: true,
});
}
}
private setRef(element: HTMLElement): void {
@@ -1,20 +0,0 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent";
export class GitHubReposComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private props: GitHubReposComponentProps) {
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <GitHubReposComponent {...this.props} />;
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}
@@ -55,7 +55,7 @@ export class NotebookViewerComponent
databaseAccountName: undefined,
defaultExperience: "NotebookViewer",
isReadOnly: true,
cellEditorType: "monaco",
cellEditorType: "codemirror",
autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year
contentProvider: contents.JupyterContentProvider, // NotebookViewer only knows how to talk to Jupyter contents API
});
@@ -38,7 +38,6 @@ describe("SettingsComponent", () => {
title: "Scale & Settings",
tabPath: "",
node: undefined,
hashLocation: "settings",
}),
};
@@ -127,7 +126,6 @@ describe("SettingsComponent", () => {
isDatabaseExpanded: undefined,
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: undefined,
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,
@@ -16,8 +16,8 @@ import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/T
import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
@@ -110,6 +110,7 @@ export interface SettingsComponentState {
initialNotification: DataModels.Notification;
selectedTab: SettingsV2TabTypes;
offerLoaded: boolean;
}
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
@@ -122,7 +123,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private collection: ViewModels.Collection;
private database: ViewModels.Database;
private offer: DataModels.Offer;
private container: Explorer;
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
private shouldShowIndexingPolicyEditor: boolean;
@@ -134,7 +134,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.isCollectionSettingsTab = this.props.settingsTab.tabKind === ViewModels.CollectionTabKind.CollectionSettingsV2;
if (this.isCollectionSettingsTab) {
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.container = this.collection?.container;
this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
@@ -146,7 +145,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
userContext.apiType === "Mongo" && (!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
} else {
this.database = this.props.settingsTab.database;
this.container = this.database?.container;
this.offer = this.database?.offer();
}
@@ -197,6 +195,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
initialNotification: undefined,
selectedTab: SettingsV2TabTypes.ScaleTab,
offerLoaded: !!this.offer,
};
this.saveSettingsButton = {
@@ -218,6 +217,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.isCollectionSettingsTab) {
this.refreshIndexTransformationProgress();
this.loadMongoIndexes();
this.loadCollectionOffer();
}
this.setAutoPilotStates();
@@ -294,7 +294,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean =>
userContext?.databaseAccount?.properties?.enableMultipleWriteLocations &&
@@ -372,6 +372,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
};
private async loadCollectionOffer() {
try {
this.props.settingsTab.isExecuting(true);
await this.collection.loadOffer();
this.props.settingsTab.tabTitle(this.collection.offer() ? "Settings" : "Scale & Settings");
this.setState({ offerLoaded: true });
} catch (error) {
this.props.settingsTab.isExecutionError(true);
const errorMessage = getErrorMessage(error);
traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error),
},
this.props.settingsTab.onLoadStartKey
);
logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`);
} finally {
this.props.settingsTab.isExecuting(false);
}
}
private getMongoIndexesToSave = (): MongoIndex[] => {
let finalIndexes: MongoIndex[] = [];
this.state.currentMongoIndexes?.map((mongoIndex: MongoIndex, index: number) => {
@@ -884,7 +912,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const scaleComponentProps: ScaleComponentProps = {
collection: this.collection,
database: this.database,
container: this.container,
isFixedContainer: this.isFixedContainer,
onThroughputChange: this.onThroughputChange,
throughput: this.state.throughput,
@@ -910,9 +937,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
);
}
if (!this.state.offerLoaded) {
return <></>;
}
const subSettingsComponentProps: SubSettingsComponentProps = {
collection: this.collection,
container: this.container,
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,
changeFeedPolicyVisible: this.changeFeedPolicyVisible,
timeToLive: this.state.timeToLive,
@@ -965,7 +995,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
collection: this.collection,
container: this.container,
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
conflictResolutionPolicyModeBaseline: this.state.conflictResolutionPolicyModeBaseline,
onConflictResolutionPolicyModeChange: this.onConflictResolutionPolicyModeChange,
@@ -1,14 +0,0 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { SettingsComponent, SettingsComponentProps } from "./SettingsComponent";
export class SettingsComponentAdapter implements ReactAdapter {
public parameters: ko.Computed<boolean>;
constructor(private props: SettingsComponentProps) {}
public renderComponent(): JSX.Element {
return this.parameters() ? <SettingsComponent {...this.props} /> : <></>;
}
}
@@ -1,13 +1,12 @@
import { shallow } from "enzyme";
import React from "react";
import { ConflictResolutionComponentProps, ConflictResolutionComponent } from "./ConflictResolutionComponent";
import { container, collection } from "../TestUtils";
import * as DataModels from "../../../../Contracts/DataModels";
import { collection } from "../TestUtils";
import { ConflictResolutionComponent, ConflictResolutionComponentProps } from "./ConflictResolutionComponent";
describe("ConflictResolutionComponent", () => {
const baseProps: ConflictResolutionComponentProps = {
collection: collection,
container: container,
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode.Custom,
onConflictResolutionPolicyModeChange: () => {
@@ -1,21 +1,19 @@
import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import Explorer from "../../../Explorer";
import * as ViewModels from "../../../../Contracts/ViewModels";
import {
getTextFieldStyles,
conflictResolutionLwwTooltip,
conflictResolutionCustomToolTip,
subComponentStackProps,
conflictResolutionLwwTooltip,
getChoiceGroupStyles,
getTextFieldStyles,
subComponentStackProps,
} from "../SettingsRenderUtils";
import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
import { isDirty } from "../SettingsUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface ConflictResolutionComponentProps {
collection: ViewModels.Collection;
container: Explorer;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void;
@@ -7,20 +7,17 @@ import * as SharedConstants from "../../../../Shared/Constants";
import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { throughputUnit } from "../SettingsRenderUtils";
import { collection, container } from "../TestUtils";
import { collection } from "../TestUtils";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer();
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
const targetThroughput = 6000;
const baseProps: ScaleComponentProps = {
collection: collection,
database: undefined,
container: container,
isFixedContainer: false,
onThroughputChange: () => {
return;
@@ -111,7 +108,7 @@ describe("ScaleComponent", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
let newProps = { ...baseProps, container: nonNationalCloudContainer };
let newProps = { ...baseProps };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
@@ -124,7 +121,7 @@ describe("ScaleComponent", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
const newProps = { ...baseProps, container: nonNationalCloudContainer };
const newProps = { ...baseProps };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
});
@@ -7,7 +7,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import Explorer from "../../../Explorer";
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
import {
getTextFieldStyles,
getThroughputApplyLongDelayMessage,
@@ -23,7 +23,6 @@ import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents
export interface ScaleComponentProps {
collection: ViewModels.Collection;
database: ViewModels.Database;
container: Explorer;
isFixedContainer: boolean;
onThroughputChange: (newThroughput: number) => void;
throughput: number;
@@ -109,11 +108,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
};
public canThroughputExceedMaximumValue = (): boolean => {
return (
!this.props.isFixedContainer &&
configContext.platform === Platform.Portal &&
!this.props.container.isRunningOnNationalCloud()
);
return !this.props.isFixedContainer && configContext.platform === Platform.Portal && !isRunningOnNationalCloud();
};
public getInitialNotificationElement = (): JSX.Element => {
@@ -4,14 +4,12 @@ import { DatabaseAccount } from "../../../../Contracts/DataModels";
import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { ChangeFeedPolicyState, GeospatialConfigType, TtlOff, TtlOn, TtlOnNoDefault, TtlType } from "../SettingsUtils";
import { collection, container } from "../TestUtils";
import { collection } from "../TestUtils";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
describe("SubSettingsComponent", () => {
const baseProps: SubSettingsComponentProps = {
collection: collection,
container: container,
timeToLive: TtlType.On,
timeToLiveBaseline: TtlType.On,
onTtlChange: () => {
@@ -2,7 +2,6 @@ import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text,
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
changeFeedPolicyToolTip,
@@ -28,8 +27,6 @@ import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface SubSettingsComponentProps {
collection: ViewModels.Collection;
container: Explorer;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
@@ -36,7 +36,6 @@ describe("SettingsUtils", () => {
isDatabaseExpanded: ko.observable(false),
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: ko.observable(undefined),
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,
@@ -30,17 +30,11 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
@@ -55,38 +49,16 @@ exports[`SettingsComponent renders 1`] = `
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -110,73 +82,6 @@ exports[`SettingsComponent renders 1`] = `
"usageSizeInKB": [Function],
}
}
container={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
isAutoPilotSelected={false}
isFixedContainer={false}
onAutoPilotSelected={[Function]}
@@ -211,17 +116,11 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
@@ -236,38 +135,16 @@ exports[`SettingsComponent renders 1`] = `
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -291,73 +168,6 @@ exports[`SettingsComponent renders 1`] = `
"usageSizeInKB": [Function],
}
}
container={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
geospatialConfigType="Geometry"
geospatialConfigTypeBaseline="Geometry"
isAnalyticalStorageEnabled={false}
@@ -58,7 +58,7 @@ export interface TreeComponentProps {
export class TreeComponent extends React.Component<TreeComponentProps> {
public render(): JSX.Element {
return (
<div style={this.props.style} className={`treeComponent ${this.props.className}`}>
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
</div>
);
@@ -172,6 +172,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
role="treeitem"
>
<div
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
@@ -3,6 +3,7 @@
exports[`TreeComponent renders a simple tree 1`] = `
<div
className="treeComponent tree"
role="tree"
>
<TreeNodeComponent
generation={0}
@@ -37,6 +38,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -137,6 +139,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
className="nodeClassname main12 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -285,6 +288,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -356,6 +360,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
className="nodeClassname main12 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -523,6 +528,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -2,22 +2,22 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout";
import Q from "q";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve();
return explorerStub;
};
let explorerStub: Explorer;
beforeAll(() => {
explorerStub = {
refreshAllDatabases: () => {},
} as Explorer;
});
beforeEach(() => {
(createDocument as jest.Mock).mockResolvedValue(undefined);
@@ -59,8 +59,7 @@ describe("ContainerSampleGenerator", () => {
loadCollections: () => {},
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
const explorerStub = createExplorerStub(database);
useDatabases.getState().addDatabases([database]);
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
@@ -108,8 +107,8 @@ describe("ContainerSampleGenerator", () => {
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
collection.databaseId = database.id();
useDatabases.getState().addDatabases([database]);
const explorerStub = createExplorerStub(database);
updateUserContext({
databaseAccount: {
properties: {
@@ -126,7 +125,6 @@ describe("ContainerSampleGenerator", () => {
it("should not create any sample for Mongo API account", async () => {
const experience = "Sample generation not supported for this API Mongo";
const explorerStub = createExplorerStub(undefined);
updateUserContext({
databaseAccount: {
properties: {
@@ -141,7 +139,6 @@ describe("ContainerSampleGenerator", () => {
it("should not create any sample for Table API account", async () => {
const experience = "Sample generation not supported for this API Tables";
const explorerStub = createExplorerStub(undefined);
updateUserContext({
databaseAccount: {
properties: {
@@ -163,7 +160,6 @@ describe("ContainerSampleGenerator", () => {
},
} as DatabaseAccount,
});
const explorerStub = createExplorerStub(undefined);
// Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});
@@ -7,6 +7,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import GraphTab from ".././Tabs/GraphTab";
import Explorer from "../Explorer";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import { useDatabases } from "../useDatabases";
interface SampleDataFile extends DataModels.CreateCollectionParams {
data: any[];
@@ -59,7 +60,7 @@ export class ContainerSampleGenerator {
await createCollection(createRequest);
await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) {
return undefined;
}
@@ -2,6 +2,7 @@ import * as ko from "knockout";
import * as sinon from "sinon";
import { Collection, Database } from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { DataSamplesUtil } from "./DataSamplesUtil";
@@ -16,8 +17,8 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]),
} as Database;
const explorer = {} as Explorer;
explorer.databases = ko.observableArray<Database>([database]);
explorer.showOkModalDialog = () => {};
useDatabases.getState().addDatabases([database]);
const dataSamplesUtil = new DataSamplesUtil(explorer);
const fakeGenerator = sinon.createStubInstance<ContainerSampleGenerator>(ContainerSampleGenerator as any);
+2 -1
View File
@@ -2,6 +2,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil {
@@ -17,7 +18,7 @@ export class DataSamplesUtil {
const databaseName = generator.getDatabaseId();
const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, this.container.databases())) {
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg);
-43
View File
@@ -1,43 +0,0 @@
jest.mock("./../Common/dataAccess/deleteDatabase");
jest.mock("./../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase";
import * as ViewModels from "./../Contracts/ViewModels";
import Explorer from "./Explorer";
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: Explorer;
beforeAll(() => {
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
});
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if only 1 database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
const database = {} as ViewModels.Database;
const database2 = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
const database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});
+73 -407
View File
@@ -6,28 +6,30 @@ import _ from "underscore";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { ExplorerMetrics } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { configContext, Platform } from "../ConfigContext";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { RouteHandler } from "../RouteHandlers/RouteHandler";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import {
get as getWorkspace,
listByDatabaseAccount,
listConnectionInfo,
start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
@@ -38,7 +40,6 @@ import * as ComponentRegisterer from "./ComponentRegisterer";
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
@@ -49,24 +50,15 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
@@ -74,44 +66,28 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure";
import { useDatabases } from "./useDatabases";
import { useSelectedNode } from "./useSelectedNode";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
var tmp = ComponentRegisterer;
export interface ExplorerParams {
setNotificationConsoleData: (consoleData: ConsoleData) => void;
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
tabsManager: TabsManager;
}
export default class Explorer {
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>;
public queriesClient: QueriesClient;
public tableDataClient: TableDataClient;
public splitter: Splitter;
private setNotificationConsoleData: (consoleData: ConsoleData) => void;
private setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
// Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>;
public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>;
public selectedNode: ko.Observable<ViewModels.TreeNode>;
private resourceTree: ResourceTreeAdapter;
// Resource Token
public resourceTokenDatabaseId: ko.Observable<string>;
public resourceTokenCollectionId: ko.Observable<string>;
public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>;
public resourceTokenPartitionKey: ko.Observable<string>;
public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
// Tabs
@@ -119,16 +95,12 @@ export default class Explorer {
public tabsManager: TabsManager;
public gitHubOAuthService: GitHubOAuthService;
// features
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks
public isNotebookEnabled: ko.Observable<boolean>;
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
public notebookWorkspaceManager: NotebookWorkspaceManager;
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
public isSynapseLinkUpdating: ko.Observable<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
@@ -146,9 +118,6 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(params?: ExplorerParams) {
this.setNotificationConsoleData = params?.setNotificationConsoleData;
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
@@ -163,8 +132,6 @@ export default class Explorer {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(true);
RouteHandler.getInstance().initHandler();
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
await this._refreshNotebooksEnabledStateForAccount();
this.isNotebookEnabled(
userContext.authType !== AuthType.ResourceToken &&
@@ -190,56 +157,13 @@ export default class Explorer {
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.queriesClient = new QueriesClient(this);
this.resourceTokenDatabaseId = ko.observable<string>();
this.resourceTokenCollectionId = ko.observable<string>();
this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>();
this.resourceTokenPartitionKey = ko.observable<string>();
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
this.databases = ko.observableArray<ViewModels.Database>();
this.canSaveQueries = ko.computed<boolean>(() => {
const savedQueriesDatabase: ViewModels.Database = _.find(
this.databases(),
(database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName
);
if (!savedQueriesDatabase) {
return false;
}
const savedQueriesCollection: ViewModels.Collection =
savedQueriesDatabase &&
_.find(
savedQueriesDatabase.collections(),
(collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName
);
if (!savedQueriesCollection) {
return false;
}
return true;
});
this.selectedNode = ko.observable<ViewModels.TreeNode>();
this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => {
useSelectedNode.subscribe(() => {
// Make sure switching tabs restores tabs display
this.isTabsContentExpanded(false);
});
this.isResourceTokenCollectionNodeSelected = ko.computed<boolean>(() => {
return (
this.selectedNode() &&
this.resourceTokenCollection() &&
this.selectedNode().id() === this.resourceTokenCollection().id()
);
});
const splitterBounds: SplitterBounds = {
min: ExplorerMetrics.SplitterMinWidth,
max: ExplorerMetrics.SplitterMaxWidth,
};
this.splitter = new Splitter({
splitterId: "h_splitter1",
leftId: "resourcetree",
bounds: splitterBounds,
direction: SplitterDirection.Vertical,
});
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) {
@@ -253,44 +177,10 @@ export default class Explorer {
return isCapabilityEnabled("EnableMongo");
});
this.isServerlessEnabled = ko.computed(
() =>
userContext.databaseAccount?.properties?.capabilities?.find(
(item) => item.name === Constants.CapabilityNames.EnableServerless
) !== undefined
);
this.isHostedDataExplorerEnabled = ko.computed<boolean>(
() =>
configContext.platform === Platform.Portal &&
!this.isRunningOnNationalCloud() &&
userContext.apiType !== "Gremlin"
);
this.selectedDatabaseId = ko.computed<string>(() => {
const selectedNode = this.selectedNode();
if (!selectedNode) {
return "";
}
switch (selectedNode.nodeKind) {
case "Collection":
return (selectedNode as ViewModels.CollectionBase).databaseId || "";
case "Database":
return selectedNode.id() || "";
case "DocumentId":
case "StoredProcedure":
case "Trigger":
case "UserDefinedFunction":
return selectedNode.collection.databaseId || "";
default:
return "";
}
});
this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) {
this.selectedNode(undefined);
useSelectedNode.getState().setSelectedNode(undefined);
useCommandBar.getState().setContextButtons([]);
}
});
@@ -387,6 +277,7 @@ export default class Explorer {
if (configContext.enableSchemaAnalyzer) {
userContext.features.enableSchemaAnalyzer = true;
}
this.isAccountReady(true);
}
public openEnableSynapseLinkDialog(): void {
@@ -441,45 +332,17 @@ export default class Explorer {
// TODO: return result
}
public isDatabaseNodeOrNoneSelected(): boolean {
return this.isNoneSelected() || this.isDatabaseNodeSelected();
}
public isDatabaseNodeSelected(): boolean {
return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false;
}
public isNodeKindSelected(nodeKind: string): boolean {
return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false;
}
public isNoneSelected(): boolean {
return this.selectedNode() == null;
}
public logConsoleData(consoleData: ConsoleData): void {
this.setNotificationConsoleData(consoleData);
}
public deleteInProgressConsoleDataWithId(id: string): void {
this.setInProgressConsoleDataIdToBeDeleted(id);
}
public refreshDatabaseForResourceToken(): Q.Promise<any> {
const databaseId = this.resourceTokenDatabaseId();
const collectionId = this.resourceTokenCollectionId();
public refreshDatabaseForResourceToken(): Promise<void> {
const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId;
if (!databaseId || !collectionId) {
return Q.reject();
return Promise.reject();
}
const deferred: Q.Deferred<void> = Q.defer();
readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
this.selectedNode(this.resourceTokenCollection());
deferred.resolve();
useSelectedNode.getState().setSelectedNode(this.resourceTokenCollection());
});
return deferred.promise;
}
public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> {
@@ -504,11 +367,9 @@ export default class Explorer {
},
startKey
);
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases);
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
() => {
deferred.resolve();
@@ -597,37 +458,19 @@ export default class Explorer {
this._isInitializingNotebooks = true;
await this.ensureNotebookWorkspaceRunning();
let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: undefined,
notebookServerEndpoint: undefined,
};
try {
connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync(
databaseAccount.id,
"default"
);
} catch (error) {
this._isInitializingNotebooks = false;
handleError(
error,
"initNotebooks/getNotebookConnectionInfoAsync",
`Failed to get notebook workspace connection info: ${getErrorMessage(error)}`
);
throw error;
} finally {
// Overwrite with feature flags
if (userContext.features.notebookServerUrl) {
connectionInfo.notebookServerEndpoint = userContext.features.notebookServerUrl;
}
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
if (userContext.features.notebookServerToken) {
connectionInfo.authToken = userContext.features.notebookServerToken;
}
this.notebookServerInfo(connectionInfo);
this.notebookServerInfo.valueHasMutated();
this.refreshNotebookList();
}
this.notebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
this.notebookServerInfo.valueHasMutated();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
@@ -659,7 +502,11 @@ export default class Explorer {
}
try {
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id);
const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name
);
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
@@ -674,8 +521,10 @@ export default class Explorer {
let clearMessage;
try {
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
userContext.databaseAccount.id,
const notebookWorkspace = await getWorkspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
if (
@@ -685,7 +534,7 @@ export default class Explorer {
notebookWorkspace.properties.status.toLowerCase() === "stopped"
) {
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(userContext.databaseAccount.id, "default");
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
}
} catch (error) {
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
@@ -713,73 +562,6 @@ export default class Explorer {
}
};
public findSelectedDatabase(): ViewModels.Database {
if (!this.selectedNode()) {
return null;
}
if (this.selectedNode().nodeKind === "Database") {
return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id());
}
return this.findSelectedCollection().database;
}
public findDatabaseWithId(databaseId: string): ViewModels.Database {
return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId);
}
public isLastNonEmptyDatabase(): boolean {
if (
this.isLastDatabase() &&
this.databases()[0] &&
this.databases()[0].collections &&
this.databases()[0].collections().length > 0
) {
return true;
}
return false;
}
public isLastDatabase(): boolean {
if (this.databases().length > 1) {
return false;
}
return true;
}
public isSelectedDatabaseShared(): boolean {
const database = this.findSelectedDatabase();
if (!!database) {
return database.offer && !!database.offer();
}
return false;
}
public configure(inputs: ViewModels.DataExplorerInputsFrame): void {
if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
}
this.isAccountReady(true);
}
}
public findSelectedCollection(): ViewModels.Collection {
return (this.selectedNode().nodeKind === "Collection"
? this.selectedNode()
: this.selectedNode().collection) as ViewModels.Collection;
}
public isRunningOnNationalCloud(): boolean {
return (
userContext.portalEnv === "blackforest" ||
userContext.portalEnv === "fairfax" ||
userContext.portalEnv === "mooncake"
);
}
private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise<void> {
// we reload collections for all databases so the resource tree reflects any collection-level changes
// i.e addition of stored procedures, etc.
@@ -787,10 +569,11 @@ export default class Explorer {
let loadCollectionPromises: Q.Promise<void>[] = [];
// If the user has a lot of databases, only load expanded databases.
const databases = useDatabases.getState().databases;
const databasesToLoad =
this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand
? this.databases()
: this.databases().filter((db) => db.isDatabaseExpanded());
databases.length <= Explorer.MaxNbDatabasesToAutoExpand
? databases
: databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, {
dataExplorerArea: Constants.Areas.ResourceTree,
@@ -835,37 +618,16 @@ export default class Explorer {
}
}
public findCollection(databaseId: string, collectionId: string): ViewModels.Collection {
const database: ViewModels.Database = this.databases().find(
(database: ViewModels.Database) => database.id() === databaseId
);
return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId);
}
public isLastCollection(): boolean {
let collectionCount = 0;
if (this.databases().length == 0) {
return false;
}
for (let i = 0; i < this.databases().length; i++) {
const database = this.databases()[i];
collectionCount += database.collections().length;
if (collectionCount > 1) {
return false;
}
}
return true;
}
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[]
): {
toAdd: ViewModels.Database[];
toDelete: ViewModels.Database[];
} {
const databases = useDatabases.getState().databases;
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some(
this.databases(),
databases,
(existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id
);
return !databaseExists;
@@ -875,7 +637,7 @@ export default class Explorer {
);
let databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
ko.utils.arrayForEach(databases, (database: ViewModels.Database) => {
const databasePresentInUpdatedList = _.some(
updatedDatabaseList,
(db: DataModels.Database) => db.id === database.id()
@@ -889,24 +651,12 @@ export default class Explorer {
}
private addDatabasesToList(databases: ViewModels.Database[]): void {
this.databases(
this.databases()
.concat(databases)
.sort((database1, database2) => database1.id().localeCompare(database2.id()))
);
useDatabases.getState().addDatabases(databases);
}
private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void {
const databasesToKeep: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id);
if (!shouldRemoveDatabase) {
databasesToKeep.push(database);
}
});
this.databases(databasesToKeep);
const deleteDatabase = useDatabases.getState().deleteDatabase;
databasesToRemove.forEach((database) => deleteDatabase(database));
}
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
@@ -1068,7 +818,6 @@ export default class Explorer {
tabPath: notebookContentItem.path,
collection: null,
masterKey: userContext.masterKey || "",
hashLocation: "notebooks",
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
container: this,
@@ -1368,33 +1117,34 @@ export default class Explorer {
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
let title: string;
let hashLocation: string;
switch (kind) {
case ViewModels.TerminalKind.Default:
title = "Terminal";
hashLocation = "terminal";
break;
case ViewModels.TerminalKind.Mongo:
title = "Mongo Shell";
hashLocation = "mongo-shell";
break;
case ViewModels.TerminalKind.Cassandra:
title = "Cassandra Shell";
hashLocation = "cassandra-shell";
break;
default:
throw new Error("Terminal kind: ${kind} not supported");
}
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) =>
tab.hashLocation().startsWith(hashLocation)
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Terminal,
(tab) => tab.tabTitle() === title
) as TerminalTab[];
const index = terminalTabs.length + 1;
let index = 1;
if (terminalTabs.length > 0) {
index = terminalTabs[terminalTabs.length - 1].index + 1;
}
const newTab = new TerminalTab({
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.Terminal,
@@ -1402,11 +1152,11 @@ export default class Explorer {
title: `${title} ${index}`,
tabPath: `${title} ${index}`,
collection: null,
hashLocation: `${hashLocation} ${index}`,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
container: this,
kind: kind,
index: index,
});
this.tabsManager.activateNewTab(newTab);
@@ -1419,11 +1169,10 @@ export default class Explorer {
isFavorite?: boolean
) {
const title = "Gallery";
const hashLocation = "gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = this.tabsManager
.getTabs(ViewModels.CollectionTabKind.Gallery)
.find((tab) => tab.hashLocation() == hashLocation);
.find((tab) => tab.tabTitle() == title);
if (galleryTab instanceof GalleryTab) {
this.tabsManager.activateTab(galleryTab);
@@ -1432,9 +1181,8 @@ export default class Explorer {
new GalleryTab(
{
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
title,
tabPath: title,
hashLocation: hashLocation,
onLoadStartKey: null,
isTabsContentExpanded: ko.observable(true),
},
@@ -1452,11 +1200,19 @@ export default class Explorer {
}
}
public onNewCollectionClicked(databaseId?: string): void {
public async onNewCollectionClicked(databaseId?: string): Promise<void> {
if (userContext.apiType === "Cassandra") {
this.openCassandraAddCollectionPane();
useSidePanel
.getState()
.openSidePanel(
"Add Table",
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
);
} else {
this.openAddCollectionPanel(databaseId);
await useDatabases.getState().loadDatabaseOffers();
useSidePanel
.getState()
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
}
}
@@ -1500,50 +1256,8 @@ export default class Explorer {
}
}
public async loadDatabaseOffers(): Promise<void> {
await Promise.all(
this.databases()?.map(async (database: ViewModels.Database) => {
await database.loadOffer();
})
);
}
public isFirstResourceCreated(): boolean {
const databases: ViewModels.Database[] = this.databases();
if (!databases || databases.length === 0) {
return false;
}
return databases.some((database) => {
// user has created at least one collection
if (database.collections()?.length > 0) {
return true;
}
// user has created a database with shared throughput
if (database.offer()) {
return true;
}
// use has created an empty database without shared throughput
return false;
});
}
public openDeleteCollectionConfirmationPane(): void {
useSidePanel
.getState()
.openSidePanel("Delete " + getCollectionName(), <DeleteCollectionConfirmationPane explorer={this} />);
}
public openDeleteDatabaseConfirmationPane(): void {
useSidePanel
.getState()
.openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel explorer={this} selectedDatabase={this.findSelectedDatabase()} />
);
}
public openUploadItemsPanePane(): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane explorer={this} />);
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
}
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
useSidePanel
@@ -1551,12 +1265,6 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
}
public async openAddCollectionPanel(databaseId?: string): Promise<void> {
await this.loadDatabaseOffers();
useSidePanel
.getState()
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
}
public openAddDatabasePane(): void {
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this} />);
}
@@ -1579,14 +1287,6 @@ export default class Explorer {
);
}
public openCassandraAddCollectionPane(): void {
useSidePanel
.getState()
.openSidePanel(
"Add Table",
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
);
}
public openGitHubReposPanel(header: string, junoClient?: JunoClient): void {
useSidePanel
.getState()
@@ -1600,43 +1300,9 @@ export default class Explorer {
);
}
public openAddTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
useSidePanel
.getState()
.openSidePanel(
"Add Table Entity",
<AddTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={queryTablesTab}
tableEntityListViewModel={tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public openSetupNotebooksPanel(title: string, description: string): void {
useSidePanel
.getState()
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
}
public openEditTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
useSidePanel
.getState()
.openSidePanel(
"Edit Table Entity",
<EditTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={queryTablesTab}
tableEntityListViewModel={tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
useSidePanel.getState().openSidePanel("Select Column", <TableQuerySelectPanel queryViewModel={queryViewModal} />);
}
public openSettingPane(): void {
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />);
}
}
@@ -10,7 +10,7 @@ import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPag
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
import GraphTab from "../../Tabs/GraphTab";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";
@@ -18,7 +18,7 @@ import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Ut
import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { ArraysByKeyCache } from "./ArraysByKeyCache";
import * as D3ForceGraph from "./D3ForceGraph";
@@ -1,74 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { GraphAccessor, GraphExplorer } from "./GraphExplorer";
interface Parameter {
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
onGraphAccessorCreated: (instance: GraphAccessor) => void;
onIsFilterQueryLoading: (isFilterQueryLoading: boolean) => void;
onIsValidQuery: (isValidQuery: boolean) => void;
onIsPropertyEditing: (isEditing: boolean) => void;
onIsGraphDisplayed: (isDisplayed: boolean) => void;
onResetDefaultGraphConfigValues: () => void;
collectionPartitionKeyProperty: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
masterKey: string;
onLoadStartKey: number;
onLoadStartKeyChange: (newKey: number) => void;
resourceId: string;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
setIConfigUiData?: (data: string[]) => void;
}
interface IGraphExplorerProps {
isChanged: boolean;
}
interface IGraphExplorerStates {
isChangedState: boolean;
}
export interface GraphExplorerAdapter
extends ReactAdapter,
React.Component<IGraphExplorerProps, IGraphExplorerStates> {}
export class GraphExplorerAdapter implements ReactAdapter {
public params: Parameter;
public parameters = {};
public isNewVertexDisabled: boolean;
public constructor(params: Parameter, props?: IGraphExplorerProps) {
this.params = params;
}
public renderComponent(): JSX.Element {
return (
<GraphExplorer
onIsNewVertexDisabledChange={this.params.onIsNewVertexDisabledChange}
onGraphAccessorCreated={this.params.onGraphAccessorCreated}
onIsFilterQueryLoadingChange={this.params.onIsFilterQueryLoading}
onIsValidQueryChange={this.params.onIsValidQuery}
onIsPropertyEditing={this.params.onIsPropertyEditing}
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
graphBackendEndpoint={this.params.graphBackendEndpoint}
databaseId={this.params.databaseId}
collectionId={this.params.collectionId}
masterKey={this.params.masterKey}
onLoadStartKey={this.params.onLoadStartKey}
onLoadStartKeyChange={this.params.onLoadStartKeyChange}
resourceId={this.params.resourceId}
igraphConfigUiData={this.params.igraphConfigUiData}
igraphConfig={this.params.igraphConfig}
setIConfigUiData={this.params.setIConfigUiData}
/>
);
}
}
@@ -1,9 +1,7 @@
import * as GraphUtil from "./GraphUtil";
import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData";
import * as sinon from "sinon";
import { GraphData, GremlinEdge, GremlinVertex } from "./GraphData";
import { GraphExplorer } from "./GraphExplorer";
window.$ = window.jQuery = require("jquery");
import * as GraphUtil from "./GraphUtil";
const OUT_E_MATCHER = "g\\.V\\(.*\\).outE\\(\\).*\\.as\\('e'\\).inV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)";
const IN_E_MATCHER = "g\\.V\\(.*\\).inE\\(\\).*\\.as\\('e'\\).outV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)";
@@ -5,21 +5,26 @@
*/
import * as React from "react";
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent";
import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as EditorNeighbors from "./EditorNeighborsComponent";
import EditIcon from "../../../../images/edit.svg";
import DeleteIcon from "../../../../images/delete.svg";
import CheckIcon from "../../../../images/check.svg";
import CancelIcon from "../../../../images/cancel.svg";
import { GraphExplorer } from "./GraphExplorer";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import CheckIcon from "../../../../images/check.svg";
import DeleteIcon from "../../../../images/delete.svg";
import EditIcon from "../../../../images/edit.svg";
import * as ViewModels from "../../../Contracts/ViewModels";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
import * as EditorNeighbors from "./EditorNeighborsComponent";
import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent";
import {
EditedEdges,
EditedProperties,
GraphExplorer,
GraphHighlightedNodeData,
PossibleVertex,
} from "./GraphExplorer";
import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent";
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
export enum Mode {
READONLY_PROP,
@@ -8,9 +8,9 @@ import * as React from "react";
import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useObservable } from "../../../hooks/useObservable";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
@@ -29,13 +29,13 @@ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
}));
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
useObservable(container.selectedNode);
const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons);
const backgroundColor = StyleConstants.BaseLight;
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container);
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
const contextButtons = (buttons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(container)
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState)
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
@@ -1,17 +1,22 @@
import * as ko from "knockout";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer;
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -23,17 +28,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = () => false;
});
it("Account is not serverless - button should be visible", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@@ -41,9 +41,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Account is serverless - button should be hidden", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableServerless" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@@ -53,10 +58,12 @@ 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" }],
@@ -64,18 +71,19 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
});
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
afterEach(() => {
updateUserContext({
portalEnv: "prod",
});
});
it("Notebooks is already enabled - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
@@ -83,9 +91,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Account is running on one of the national clouds - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
@@ -93,9 +103,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(false);
@@ -105,9 +114,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(true);
@@ -119,6 +127,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Mongo Shell button", () => {
const openMongoShellBtnLabel = "Open Mongo Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -130,9 +139,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
mockExplorer.isShellEnabled = ko.observable(true);
});
@@ -148,7 +154,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
mockExplorer.isShellEnabled = ko.observable(true);
});
@@ -156,21 +162,23 @@ describe("CommandBarComponentButtonFactory tests", () => {
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
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", () => {
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
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);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -178,7 +186,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is available - button should be hidden", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -186,7 +194,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
@@ -197,7 +205,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
@@ -209,7 +217,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isShellEnabled = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -217,6 +225,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Cassandra Shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -228,9 +237,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -243,7 +249,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
});
it("Cassandra Api not available - button should be hidden", () => {
@@ -255,21 +260,23 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
console.log(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Running on a national cloud - button should be hidden", () => {
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@@ -277,7 +284,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@@ -285,7 +292,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
@@ -296,7 +303,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
@@ -307,6 +314,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("GitHub buttons", () => {
const connectToGitHubBtnLabel = "Connect to GitHub";
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -319,12 +327,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -338,7 +344,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeDefined();
});
@@ -347,7 +353,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel
);
@@ -355,7 +361,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeUndefined();
@@ -368,11 +374,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection);
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
mockExplorer.resourceTokenCollection = ko.observable(mockCollection);
updateUserContext({
authType: AuthType.ResourceToken,
});
@@ -384,7 +392,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
expect(buttons[0].disabled).toBe(false);
@@ -24,16 +24,23 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { OpenFullScreen } from "../../OpenFullScreen";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { SelectedNodeState } from "../../useSelectedNode";
let counter = 0;
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container);
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
}
const newCollectionBtn = createNewCollectionGroup(container);
@@ -68,7 +75,9 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) ||
(userContext.apiType === "Mongo" &&
container.isShellEnabled() &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
buttons.push(createDivider());
@@ -79,23 +88,23 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
}
}
} else {
if (!container.isRunningOnNationalCloud()) {
if (!isRunningOnNationalCloud()) {
buttons.push(createEnableNotebooksButton(container));
}
}
if (!container.isDatabaseNodeOrNoneSelected()) {
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isQuerySupported) {
buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(container);
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
buttons.push(newSqlQueryBtn);
}
if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) {
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn);
}
@@ -105,16 +114,16 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(container);
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
buttons.push(newStoredProcedureBtn);
}
}
@@ -122,16 +131,19 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
return buttons;
}
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
export function createContextCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
@@ -141,7 +153,7 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
};
buttons.push(newMongoShellBtn);
}
@@ -154,7 +166,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: container.openSettingPane,
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",
@@ -163,7 +175,10 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
},
];
if (container.isHostedDataExplorerEnabled()) {
const showOpenFullScreen =
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
if (showOpenFullScreen) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
iconSrc: OpenInTabIcon,
@@ -175,7 +190,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: !container.isHostedDataExplorerEnabled(),
disabled: !showOpenFullScreen,
className: "OpenFullScreen",
};
buttons.push(fullScreenButton);
@@ -234,7 +249,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
return undefined;
}
if (container.isServerlessEnabled()) {
if (isServerlessAccount()) {
return undefined;
}
@@ -273,20 +288,20 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
};
}
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query";
return {
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
} else if (userContext.apiType === "Mongo") {
const label = "New Query";
@@ -294,23 +309,24 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
}
return undefined;
}
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
const shouldEnableScriptsCommands: boolean =
!selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
@@ -318,13 +334,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
@@ -335,13 +351,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
@@ -352,13 +368,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
@@ -405,12 +421,12 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
};
}
function createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane explorer={container} />),
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
@@ -531,19 +547,25 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
};
}
function createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(container);
function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton(container);
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
const isResourceTokenCollectionNodeSelected: boolean =
container.resourceTokenCollection() &&
container.resourceTokenCollection().id() === selectedNodeState.selectedNode?.id();
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
};
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
if (!openQueryBtn.disabled) {
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
}
return [newSqlQueryBtn, openQueryBtn];
@@ -1,33 +0,0 @@
/**
* React component for control bar
*/
import * as React from "react";
import {
CommandButtonComponent,
CommandButtonComponentProps,
} from "../../Controls/CommandButton/CommandButtonComponent";
export interface ControlBarComponentProps {
buttons: CommandButtonComponentProps[];
}
export class ControlBarComponent extends React.Component<ControlBarComponentProps> {
private static renderButtons(commandButtonOptions: CommandButtonComponentProps[]): JSX.Element[] {
return commandButtonOptions.map(
(btn: CommandButtonComponentProps, index: number): JSX.Element => {
// Remove label
btn.commandButtonLabel = undefined;
return CommandButtonComponent.renderButton(btn, `${index}`);
}
);
}
public render(): JSX.Element {
if (!this.props.buttons || this.props.buttons.length < 1) {
return <React.Fragment />;
}
return <React.Fragment>{ControlBarComponent.renderButtons(this.props.buttons)}</React.Fragment>;
}
}
@@ -1,28 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { ControlBarComponent } from "./ControlBarComponent";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class ControlBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private buttons: ko.ObservableArray<CommandButtonComponentProps>) {
this.buttons.subscribe(() => this.forceRender());
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <ControlBarComponent buttons={this.buttons()} />;
}
public forceRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}
@@ -0,0 +1,16 @@
/**
* Interface for the data/content that will be recorded
*/
export interface ConsoleData {
type: ConsoleDataType;
date: string;
message: string;
id?: string;
}
export enum ConsoleDataType {
Info = 0,
Error = 1,
InProgress = 2,
}
@@ -1,10 +1,7 @@
import { shallow } from "enzyme";
import React from "react";
import {
ConsoleDataType,
NotificationConsoleComponent,
NotificationConsoleComponentProps,
} from "./NotificationConsoleComponent";
import { ConsoleDataType } from "./ConsoleData";
import { NotificationConsoleComponent, NotificationConsoleComponentProps } from "./NotificationConsoleComponent";
describe("NotificationConsoleComponent", () => {
const createBlankProps = (): NotificationConsoleComponentProps => {
@@ -17,25 +17,7 @@ import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { userContext } from "../../../UserContext";
/**
* Log levels
*/
export enum ConsoleDataType {
Info = 0,
Error = 1,
InProgress = 2,
}
/**
* Interface for the data/content that will be recorded
*/
export interface ConsoleData {
type: ConsoleDataType;
date: string;
message: string;
id?: string;
}
import { ConsoleData, ConsoleDataType } from "./ConsoleData";
export interface NotificationConsoleComponentProps {
isConsoleExpanded: boolean;
@@ -323,14 +305,13 @@ const PrPreview = (props: { pr: string }) => {
);
};
export const NotificationConsole: React.FC<
Pick<NotificationConsoleComponentProps, "consoleData" | "inProgressConsoleDataIdToBeDeleted">
> = ({
consoleData,
inProgressConsoleDataIdToBeDeleted,
}: Pick<NotificationConsoleComponentProps, "consoleData" | "inProgressConsoleDataIdToBeDeleted">) => {
export const NotificationConsole: React.FC = () => {
const setIsExpanded = useNotificationConsole((state) => state.setIsExpanded);
const isExpanded = useNotificationConsole((state) => state.isExpanded);
const consoleData = useNotificationConsole((state) => state.consoleData);
const inProgressConsoleDataIdToBeDeleted = useNotificationConsole(
(state) => state.inProgressConsoleDataIdToBeDeleted
);
// TODO Refactor NotificationConsoleComponent into a functional component and remove this wrapper
// This component only exists so we can use hooks and pass them down to a non-functional component
return (
+19 -14
View File
@@ -21,7 +21,7 @@ import {
makeStateRecord,
makeTransformsRecord,
} from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { configOption, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable";
@@ -242,22 +242,27 @@ export class NotebookClientV2 {
);
// Additional configuration
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "monaco"));
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror"));
this.store.dispatch(
configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs)
);
createConfigCollection({
key: "monaco",
});
defineConfigOption({
label: "Show Line numbers",
key: "monaco.lineNumbers",
values: [
{ label: "Yes", value: true },
{ label: "No", value: false },
],
defaultValue: true,
});
this.store.dispatch(configOption("codeMirror.lineNumbers").action(true));
const readOnlyConfigOption = configOption("codeMirror.readOnly");
const readOnlyValue = params.isReadOnly ? "nocursor" : undefined;
if (!readOnlyConfigOption) {
defineConfigOption({
label: "Read-only",
key: "codeMirror.readOnly",
values: [
{ label: "Read-Only", value: "nocursor" },
{ label: "Not read-only", value: undefined },
],
defaultValue: readOnlyValue,
});
} else {
this.store.dispatch(readOnlyConfigOption.action(readOnlyValue));
}
}
/**
@@ -1,10 +1,10 @@
.notebookComponentContainer {
text-transform:none;
line-height:1.28581;
letter-spacing:0;
font-size:14px;
font-weight:400;
color:#182026;
text-transform: none;
line-height: 1.28581;
letter-spacing: 0;
font-size: 14px;
font-weight: 400;
color: #182026;
height: 100%;
.hotKeys {
@@ -6,6 +6,7 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
export class NotebookContainerClient {
@@ -130,16 +131,18 @@ export class NotebookContainerClient {
}
private async recreateNotebookWorkspaceAsync(): Promise<void> {
const explorer = window.dataExplorer;
const { databaseAccount } = userContext;
if (!databaseAccount?.id) {
throw new Error("DataExplorer not initialized");
}
const notebookWorkspaceManager = explorer.notebookWorkspaceManager;
try {
await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(databaseAccount?.id, "default");
await notebookWorkspaceManager.createNotebookWorkspaceAsync(databaseAccount?.id, "default");
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
await createOrUpdate(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
return Promise.reject(error);
@@ -1,56 +1,68 @@
.NotebookReadOnlyRender {
.nteract-cell-container {
margin-bottom: 10px;
}
.nteract-cell-container {
margin-bottom: 10px;
}
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
}
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
}
.CodeMirror-scroll {
background-color: #f5f5f5;
}
.CodeMirror-scroll {
overflow: hidden !important;
}
.CodeMirror-lines {
cursor: default;
}
.CodeMirror-lines {
cursor: default;
}
.nteract-cell:hover {
border: 1px solid #0078d4;
border-left: 3px solid #0078d4;
.CodeMirror {
height: inherit;
}
.CodeMirror-scroll {
background-color: #ffffff;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #f5f5f5;
}
.nteract-cell-outputs {
border-top: 1px solid #d7d7d7;
}
.nteract-cell:hover {
border: 1px solid #0078d4;
border-left: 3px solid #0078d4;
.nteract-md-cell {
background-color: #ffffff;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #ffffff;
}
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
border-top: 1px solid #d7d7d7;
}
.nteract-md-cell {
background-color: #f5f5f5;
background-color: #ffffff;
}
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
}
.nteract-md-cell {
background-color: #f5f5f5;
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
}
}
@@ -1,6 +1,6 @@
import { actions, ContentRef } from "@nteract/core";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import * as React from "react";
@@ -67,8 +67,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
? () => <SandboxOutputs id={id} contentRef={contentRef} />
: undefined,
editor: {
monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
},
}}
</CodeCell>
@@ -84,8 +84,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<RawCell id={id} contentRef={contentRef} cell_type="raw">
{{
editor: {
monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor {...props} readOnly={true} editorType={"monaco"} />,
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
},
}}
</RawCell>
@@ -3,110 +3,122 @@
@HighlightColor: #0078d4;
.NotebookRendererContainer {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.NotebookRenderer {
overflow: auto;
flex-grow: 1;
overflow: auto;
flex-grow: 1;
.nteract-cells {
padding-top: 0px;
.nteract-cells {
padding-top: 0px;
}
.nteract-cell-container {
margin-bottom: 10px;
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
.CellContextMenuButton {
position: sticky;
z-index: 1;
top: 0px;
right: 0px;
margin: 0px 0px 0px -100%;
float: right;
visibility: hidden;
}
}
.nteract-cell-container {
margin-bottom: 10px;
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
.CellContextMenuButton {
position: sticky;
z-index: 1;
top: 0px;
right: 0px;
margin: 0px 0px 0px -100%;
float: right;
visibility: hidden;
}
}
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
background-color: #f5f5f5;
}
.nteract-cell:hover {
border: 1px solid @HoverColor;
border-left: 3px solid @HoverColor;
.CellContextMenuButton {
visibility: visible;
}
}
.CodeMirror-scroll {
overflow: hidden !important;
}
.nteract-cell-container.selected {
.nteract-cell {
border: 1px solid @HighlightColor;
border-left: 3px solid @HighlightColor;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #f5f5f5;
}
// White background when hovered or selected
.nteract-cell:hover, .nteract-cell-container.selected .nteract-cell {
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
background-color: #ffffff;
}
.CodeMirror {
height: inherit;
}
.CodeMirror-linenumber {
color: #015CDA;
}
.nteract-cell:hover {
border: 1px solid @HoverColor;
border-left: 3px solid @HoverColor;
.nteract-cell-outputs {
border-top: 1px solid @HoverColor;
}
.CellContextMenuButton {
visibility: visible;
}
}
}
.nteract-md-cell {
background-color: #ffffff;
}
.nteract-cell-container.selected {
.nteract-cell {
border: 1px solid @HighlightColor;
border-left: 3px solid @HighlightColor;
}
}
// White background when hovered or selected
.nteract-cell:hover,
.nteract-cell-container.selected .nteract-cell {
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #ffffff;
}
.CodeMirror-linenumber {
color: #015cda;
}
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
border-top: 1px solid @HoverColor;
}
.nteract-md-cell {
background-color: #f5f5f5;
background-color: #ffffff;
}
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
}
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
.nteract-md-cell .ntreact-cell-source {
width: 100%;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
}
.nteract-md-cell {
background-color: #f5f5f5;
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
}
.nteract-md-cell .ntreact-cell-source {
width: 100%;
}
}
// Undo tree.less
.expanded::before {
content: '';
content: "";
}
.monaco-editor .monaco-list .main {
background-color: transparent;
}
background-color: transparent;
}
@@ -2,7 +2,7 @@ import { CellId } from "@nteract/commutable";
import { CellType } from "@nteract/commutable/src";
import { actions, ContentRef, selectors } from "@nteract/core";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import * as React from "react";
import { DndProvider } from "react-dnd";
@@ -120,7 +120,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
<CodeCell id={id} contentRef={contentRef} cell_type="code">
{{
editor: {
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
codemirror: (props: PassedEditorProps) => (
<CodeMirrorEditor {...props} editorType="codemirror" />
),
},
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
<Prompt id={id} contentRef={contentRef} isHovered={false}>
@@ -142,7 +144,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
{{
editor: {
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
codemirror: (props: PassedEditorProps) => (
<CodeMirrorEditor {...props} editorType="codemirror" />
),
},
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
}}
@@ -157,7 +161,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
<RawCell id={id} contentRef={contentRef} cell_type="raw">
{{
editor: {
monaco: (props: PassedEditorProps) => <MonacoEditor {...props} editorType={"monaco"} />,
codemirror: (props: PassedEditorProps) => (
<CodeMirrorEditor {...props} editorType="codemirror" />
),
},
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
}}
@@ -1,8 +1,8 @@
jest.mock("./NotebookComponent/store");
jest.mock("@nteract/core");
import { defineConfigOption } from "@nteract/mythic-configuration";
import { NotebookClientV2 } from "./NotebookClientV2";
import configureStore from "./NotebookComponent/store";
import { defineConfigOption } from "@nteract/mythic-configuration";
describe("auto start kernel", () => {
it("configure autoStartKernelOnNotebookOpen properly depending whether notebook is/is not read-only", async () => {
@@ -24,6 +24,12 @@ describe("auto start kernel", () => {
defaultValue: 1234,
});
defineConfigOption({
label: "Line numbers",
key: "codeMirror.lineNumbers",
defaultValue: true,
});
[true, false].forEach((isReadOnly) => {
new NotebookClientV2({
connectionInfo: {
@@ -1,7 +1,7 @@
import * as ko from "knockout";
import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "./Explorer";
import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import { handleOpenAction } from "./OpenActions";
describe("OpenActions", () => {
@@ -9,7 +9,6 @@ describe("OpenActions", () => {
let explorer: Explorer;
let database: ViewModels.Database;
let collection: ViewModels.Collection;
let databases: ViewModels.Database[];
beforeEach(() => {
explorer = {} as Explorer;
@@ -19,7 +18,6 @@ describe("OpenActions", () => {
id: ko.observable("db"),
collections: ko.observableArray<ViewModels.Collection>([]),
} as ViewModels.Database;
databases = [database];
collection = {
id: ko.observable("coll"),
} as ViewModels.Collection;
@@ -68,7 +66,7 @@ describe("OpenActions", () => {
paneKind: "AddCollection",
};
const actionHandled = handleOpenAction(action, [], explorer);
handleOpenAction(action, [], explorer);
expect(explorer.onNewCollectionClicked).toHaveBeenCalled();
});
@@ -78,7 +76,7 @@ describe("OpenActions", () => {
paneKind: ActionContracts.PaneKind.AddCollection,
};
const actionHandled = handleOpenAction(action, [], explorer);
handleOpenAction(action, [], explorer);
expect(explorer.onNewCollectionClicked).toHaveBeenCalled();
});
});
@@ -1,39 +1,38 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import Explorer from "../Explorer";
import { CassandraAddCollectionPane } from "../Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { SettingsPane } from "../Panes/SettingsPane/SettingsPane";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "./Explorer";
export function handleOpenAction(
action: ActionContracts.DataExplorerAction,
databases: ViewModels.Database[],
explorer: Explorer
): boolean {
if (
action.actionType === ActionContracts.ActionType.OpenCollectionTab ||
(<any>action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenCollectionTab]
) {
openCollectionTab(<ActionContracts.OpenCollectionTab>action, databases);
return true;
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
if (!action.query) {
return "SELECT * FROM c";
} else if (action.query.text) {
return action.query.text;
} else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) {
let query = "SELECT * FROM c WHERE";
for (let i = 0; i < action.query.partitionKeys.length; i++) {
const partitionKey = action.query.partitionKeys[i];
if (!partitionKey) {
// null partition key case
query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`);
} else if (typeof partitionKey !== "string") {
// Undefined partition key case
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`);
} else {
query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`);
}
if (i !== action.query.partitionKeys.length - 1) {
query = query.concat(" OR");
}
}
return query;
}
if (
action.actionType === ActionContracts.ActionType.OpenPane ||
(<any>action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenPane]
) {
openPane(<ActionContracts.OpenPane>action, explorer);
return true;
}
if (
action.actionType === ActionContracts.ActionType.OpenSampleNotebook ||
(<any>action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook]
) {
openFile(<ActionContracts.OpenSampleNotebook>action, explorer);
return true;
}
return false;
return "SELECT * FROM c";
}
function openCollectionTab(
@@ -65,7 +64,7 @@ function openCollectionTab(
if (
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
) {
collection.onDocumentDBDocumentsClick();
break;
@@ -73,7 +72,7 @@ function openCollectionTab(
if (
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
) {
collection.onMongoDBDocumentsClick();
break;
@@ -81,7 +80,7 @@ function openCollectionTab(
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
@@ -89,7 +88,7 @@ function openCollectionTab(
if (
action.tabKind === ActionContracts.TabKind.TableEntities ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
) {
collection.onTableEntitiesClick();
break;
@@ -97,7 +96,7 @@ function openCollectionTab(
if (
action.tabKind === ActionContracts.TabKind.Graph ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
) {
collection.onGraphDocumentsClick();
break;
@@ -105,19 +104,19 @@ function openCollectionTab(
if (
action.tabKind === ActionContracts.TabKind.SQLQuery ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) {
collection.onNewQueryClick(
collection,
null,
generateQueryText(<ActionContracts.OpenQueryTab>action, collection.partitionKeyProperty)
undefined,
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperty)
);
break;
}
if (
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
) {
collection.onSettingsClick();
break;
@@ -138,49 +137,59 @@ function openCollectionTab(
function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
if (
action.paneKind === ActionContracts.PaneKind.AddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
) {
explorer.onNewCollectionClicked();
} else if (
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
) {
explorer.openCassandraAddCollectionPane();
useSidePanel
.getState()
.openSidePanel(
"Add Table",
<CassandraAddCollectionPane explorer={explorer} cassandraApiClient={new CassandraAPIDataClient()} />
);
} else if (
action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) {
explorer.openSettingPane();
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />);
}
}
export function handleOpenAction(
action: ActionContracts.DataExplorerAction,
databases: ViewModels.Database[],
explorer: Explorer
): boolean {
if (
action.actionType === ActionContracts.ActionType.OpenCollectionTab ||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenCollectionTab]
) {
openCollectionTab(action as ActionContracts.OpenCollectionTab, databases);
return true;
}
if (
action.actionType === ActionContracts.ActionType.OpenPane ||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenPane]
) {
openPane(action as ActionContracts.OpenPane, explorer);
return true;
}
if (
action.actionType === ActionContracts.ActionType.OpenSampleNotebook ||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook]
) {
openFile(action as ActionContracts.OpenSampleNotebook, explorer);
return true;
}
return false;
}
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) {
explorer.handleOpenFileAction(decodeURIComponent(action.path));
}
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
if (!action.query) {
return "SELECT * FROM c";
} else if (!!action.query.text) {
return action.query.text;
} else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) {
let query = "SELECT * FROM c WHERE";
for (let i = 0; i < action.query.partitionKeys.length; i++) {
let partitionKey = action.query.partitionKeys[i];
if (!partitionKey) {
// null partition key case
query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`);
} else if (typeof partitionKey !== "string") {
// Undefined partition key case
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`);
} else {
query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`);
}
if (i !== action.query.partitionKeys.length - 1) {
query = query.concat(" OR");
}
}
return query;
}
return "SELECT * FROM c";
}
+10 -11
View File
@@ -31,6 +31,7 @@ import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
@@ -125,6 +126,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
render(): JSX.Element {
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
{this.state.errorMessage && (
@@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!this.state.errorMessage && this.isFreeTierAccount() && (
<PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, this.props.explorer.isFirstResourceCreated(), true)}
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
@@ -240,9 +243,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
@@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
@@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private getDatabaseOptions(): IDropdownOption[] {
return this.props.explorer?.databases()?.map((database) => ({
return useDatabases.getState().databases?.map((database) => ({
key: database.id(),
text: database.id(),
}));
@@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
const selectedDatabase = this.props.explorer
.databases()
?.find((database) => database.id() === this.state.selectedDatabaseId);
const selectedDatabase = useDatabases
.getState()
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
return !!selectedDatabase?.offer();
}
@@ -16,7 +16,9 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../../Utils/PricingUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { getTextFieldStyles } from "../PanelStyles";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface AddDatabasePaneProps {
@@ -171,7 +173,12 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
<RightPaneForm {...props}>
{!formErrors && isFreeTierAccount && (
<PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, container.isFirstResourceCreated(), true)}
message={getUpsellMessage(
userContext.portalEnv,
true,
useDatabases.getState().isFirstResourceCreated(),
true
)}
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
@@ -179,10 +186,12 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
/>
)}
<div className="panelMainContent">
<div>
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{databaseIdLabel}
</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</Stack>
@@ -199,36 +208,37 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
value={databaseId}
onChange={handleonChangeDBId}
autoFocus
style={{ fontSize: 12 }}
styles={{ root: { width: 300 } }}
styles={getTextFieldStyles()}
/>
<Stack horizontal>
<Checkbox
title="Provision shared throughput"
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</Stack>
{!isServerlessAccount() && databaseCreateNewShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
title="Provision shared throughput"
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</Stack>
)}
</div>
</Stack>
{!isServerlessAccount() && databaseCreateNewShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
</div>
</RightPaneForm>
);
@@ -10,16 +10,17 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
<div
className="panelMainContent"
>
<div>
<Stack>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
*
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Database id
@@ -38,13 +39,16 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Type a new database id"
size={40}
style={
Object {
"fontSize": 12,
}
}
styles={
Object {
"field": Object {
"fontSize": 12,
"selectors": Object {
"::placeholder": Object {
"fontSize": 12,
},
},
},
"root": Object {
"width": 300,
},
@@ -82,14 +86,14 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
Provisioned throughput at the database level will be shared across all collections within the database.
</InfoTooltip>
</Stack>
<ThroughputInput
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
/>
</div>
</Stack>
<ThroughputInput
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
/>
</div>
</RightPaneForm>
`;
@@ -1,14 +1,16 @@
import { mount } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { SavedQueries } from "../../../Common/Constants";
import { QueriesClient } from "../../../Common/QueriesClient";
import { Query } from "../../../Contracts/DataModels";
import { Collection, Database } from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { BrowseQueriesPane } from "./BrowseQueriesPane";
describe("Browse queries panel", () => {
const fakeExplorer = {} as Explorer;
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
const fakeClientQuery = {} as QueriesClient;
const fakeQueryData = [] as Query[];
fakeClientQuery.getQueries = async () => fakeQueryData;
@@ -17,6 +19,16 @@ describe("Browse queries panel", () => {
explorer: fakeExplorer,
closePanel: (): void => undefined,
};
useDatabases.getState().addDatabases([
{
id: ko.observable(SavedQueries.DatabaseName),
collections: ko.observableArray([
{
id: ko.observable(SavedQueries.CollectionName),
} as Collection,
]),
} as Database,
]);
it("Should render Default properly", () => {
const wrapper = mount(<BrowseQueriesPane {...props} />);
@@ -12,7 +12,9 @@ import {
QueriesGridComponentProps,
} from "../../Controls/QueriesGridReactComponent/QueriesGridComponent";
import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab";
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
interface BrowseQueriesPaneProps {
explorer: Explorer;
@@ -23,7 +25,7 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
}: BrowseQueriesPaneProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const loadSavedQuery = (savedQuery: Query): void => {
const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
const selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection();
if (!selectedCollection) {
// should never get into this state because this pane is only accessible through the query tab
logError("No collection was selected", "BrowseQueriesPane.loadSavedQuery");
@@ -31,13 +33,13 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
} else if (userContext.apiType === "Mongo") {
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
} else {
selectedCollection.onNewQueryClick(selectedCollection, undefined);
selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query);
}
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab);
queryTab.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
queryTab.initialEditorContent(savedQuery.query);
queryTab.sqlQueryEditorContent(savedQuery.query);
trace(Action.LoadSavedQuery, ActionModifiers.Mark, {
dataExplorerArea: Areas.ContextualPane,
queryName: savedQuery.queryName,
@@ -45,12 +47,13 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
});
closeSidePanel();
};
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
const props: QueriesGridComponentProps = {
queriesClient: explorer.queriesClient,
onQuerySelect: loadSavedQuery,
containerVisible: true,
saveQueryEnabled: explorer.canSaveQueries(),
saveQueryEnabled: isSaveQueryEnabled(),
};
return (
@@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = `
closePanel={[Function]}
explorer={
Object {
"canSaveQueries": [Function],
"queriesClient": Object {
"getQueries": [Function],
},
@@ -1,32 +1,30 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import { CassandraAddCollectionPane } from "./CassandraAddCollectionPane";
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,
cassandraApiClient: new CassandraAPIDataClient(),
};
describe("CassandraAddCollectionPane Pane", () => {
describe("Cassandra add collection pane test", () => {
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,
cassandraApiClient: new CassandraAPIDataClient(),
};
beforeEach(() => render(<CassandraAddCollectionPane {...props} />));
it("should render Default properly", () => {
const wrapper = shallow(<CassandraAddCollectionPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("click on is Create new keyspace", () => {
fireEvent.click(screen.getByLabelText("Create new keyspace"));
expect(screen.getByLabelText("Provision keyspace throughput")).toBeDefined();
});
it("click on Use existing", () => {
fireEvent.click(screen.getByLabelText("Use existing keyspace"));
it("should render default properly", () => {
expect(screen.getByRole("radio", { name: "Create new keyspace", checked: true })).toBeDefined();
expect(screen.getByRole("checkbox", { name: "Provision shared throughput", checked: false })).toBeDefined();
});
it("Enter Keyspace name ", () => {
fireEvent.change(screen.getByLabelText("Keyspace id"), { target: { value: "unittest1" } });
expect(screen.getByLabelText("CREATE TABLE unittest1.")).toBeDefined();
it("click on use existing", () => {
fireEvent.click(screen.getByRole("radio", { name: "Use existing keyspace" }));
expect(screen.getByRole("combobox", { name: "Choose existing keyspace id" })).toBeDefined();
});
it("enter Keyspace name ", () => {
fireEvent.change(screen.getByRole("textbox", { name: "Keyspace id" }), { target: { value: "table1" } });
expect(screen.getByText("CREATE TABLE table1.")).toBeDefined();
});
});
@@ -1,21 +1,19 @@
import { Label, Stack, TextField } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react";
import React, { FunctionComponent, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import * as AddCollectionUtility from "../../../Shared/AddCollectionUtility";
import * as SharedConstants from "../../../Shared/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import { useDatabases } from "../../useDatabases";
import { getTextFieldStyles } from "../PanelStyles";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface CassandraAddCollectionPaneProps {
@@ -27,183 +25,73 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
explorer: container,
cassandraApiClient,
}: CassandraAddCollectionPaneProps) => {
let newKeySpaceThroughput: number;
let isNewKeySpaceAutoscale: boolean;
let tableThroughput: number;
let isTableAutoscale: boolean;
let isCostAcknowledged: boolean;
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const throughputDefaults = userContext.collectionCreationDefaults.throughput;
const [createTableQuery, setCreateTableQuery] = useState<string>("CREATE TABLE ");
const [keyspaceId, setKeyspaceId] = useState<string>("");
const [newKeyspaceId, setNewKeyspaceId] = useState<string>("");
const [existingKeyspaceId, setExistingKeyspaceId] = useState<string>("");
const [tableId, setTableId] = useState<string>("");
const [throughput, setThroughput] = useState<number>(
AddCollectionUtility.getMaxThroughput(userContext.collectionCreationDefaults, container)
);
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(userContext.features.autoscaleDefault);
const [isSharedAutoPilotSelected, setIsSharedAutoPilotSelected] = useState<boolean>(
userContext.features.autoscaleDefault
);
const [userTableQuery, setUserTableQuery] = useState<string>(
"(userid int, name text, email text, PRIMARY KEY (userid))"
);
const [keyspaceHasSharedOffer, setKeyspaceHasSharedOffer] = useState<boolean>(false);
const [keyspaceIds, setKeyspaceIds] = useState<string[]>([]);
const [keyspaceThroughput, setKeyspaceThroughput] = useState<number>(throughputDefaults.shared);
const [isKeyspaceShared, setIsKeyspaceShared] = useState<boolean>(false);
const [keyspaceCreateNew, setKeyspaceCreateNew] = useState<boolean>(true);
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(false);
const [sharedThroughputSpendAck, setSharedThroughputSpendAck] = useState<boolean>(false);
const { minAutoPilotThroughput: selectedAutoPilotThroughput } = AutoPilotUtils;
const { minAutoPilotThroughput: sharedAutoPilotThroughput } = AutoPilotUtils;
const _getAutoPilot = (): DataModels.AutoPilotCreationSettings => {
if (keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected && sharedAutoPilotThroughput) {
return {
maxThroughput: sharedAutoPilotThroughput * 1,
};
}
if (selectedAutoPilotThroughput) {
return {
maxThroughput: selectedAutoPilotThroughput * 1,
};
}
return undefined;
};
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const canConfigureThroughput = !container.isServerlessEnabled();
const keyspaceOffers = new Map();
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formErrors, setFormErrors] = useState<string>("");
useEffect(() => {
if (keyspaceIds.indexOf(keyspaceId) >= 0) {
setKeyspaceHasSharedOffer(keyspaceOffers.has(keyspaceId));
}
setCreateTableQuery(`CREATE TABLE ${keyspaceId}.`);
}, [keyspaceId]);
const [formError, setFormError] = useState<string>("");
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const addCollectionPaneOpenMessage = {
collection: {
id: tableId,
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: throughput,
offerThroughput: newKeySpaceThroughput || tableThroughput,
partitionKey: "",
databaseId: keyspaceId,
databaseId: keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId,
},
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput,
throughput: newKeySpaceThroughput || tableThroughput,
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
useEffect(() => {
if (!container.isServerlessEnabled()) {
setIsAutoPilotSelected(userContext.features.autoscaleDefault);
}
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
}, []);
useEffect(() => {
if (container) {
const newKeyspaceIds: ViewModels.Database[] = container.databases();
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
if (keyspace && keyspace.offer && !!keyspace.offer()) {
keyspaceOffers.set(keyspace.id(), keyspace.offer());
}
return keyspace.id();
});
setKeyspaceIds(cachedKeyspaceIdsList);
}
}, []);
const _isValid = () => {
const sharedAutoscaleThroughput = sharedAutoPilotThroughput * 1;
if (
isSharedAutoPilotSelected &&
sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!sharedThroughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
const dedicatedAutoscaleThroughput = selectedAutoPilotThroughput * 1;
if (
isAutoPilotSelected &&
dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!throughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
if ((keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected) || isAutoPilotSelected) {
const autoPilot = _getAutoPilot();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
return true;
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) {
setFormErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
if (
keyspaceHasSharedOffer &&
keyspaceCreateNew &&
keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!sharedThroughputSpendAck
) {
setFormErrors("Please acknowledge the estimated daily spend");
return false;
}
return true;
};
const onSubmit = async () => {
if (!_isValid()) {
const throughput = keyspaceCreateNew ? newKeySpaceThroughput : tableThroughput;
const keyspaceId = keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId;
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage =
isNewKeySpaceAutoscale || isTableAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
setFormError(errorMessage);
return;
}
setIsExecuting(true);
const autoPilotCommand = `cosmosdb_autoscale_max_throughput`;
const toCreateKeyspace: boolean = keyspaceCreateNew;
const useAutoPilotForKeyspace: boolean = isSharedAutoPilotSelected && !!sharedAutoPilotThroughput;
const createKeyspaceQueryPrefix = `CREATE KEYSPACE ${keyspaceId.trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`;
const createKeyspaceQuery: string = keyspaceHasSharedOffer
? useAutoPilotForKeyspace
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${keyspaceThroughput};`
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${keyspaceThroughput};`
const createKeyspaceQuery: string = isKeyspaceShared
? isNewKeySpaceAutoscale
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${newKeySpaceThroughput};`
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${newKeySpaceThroughput};`
: `${createKeyspaceQueryPrefix};`;
let tableQuery: string;
const createTableQueryPrefix = `${createTableQuery}${tableId.trim()} ${userTableQuery}`;
const createTableQueryPrefix = `CREATE TABLE ${keyspaceId}.${tableId.trim()} ${userTableQuery}`;
if (canConfigureThroughput && (dedicateTableThroughput || !keyspaceHasSharedOffer)) {
if (isAutoPilotSelected && selectedAutoPilotThroughput) {
tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${throughput};`;
if (tableThroughput) {
if (isTableAutoscale) {
tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${tableThroughput};`;
} else {
tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${throughput};`;
tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${tableThroughput};`;
}
} else {
tableQuery = `${createTableQueryPrefix};`;
@@ -215,15 +103,15 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
...addCollectionPaneOpenMessage.collection,
hasDedicatedThroughput: dedicateTableThroughput,
},
keyspaceHasSharedOffer,
toCreateKeyspace,
isKeyspaceShared,
keyspaceCreateNew,
createKeyspaceQuery,
createTableQuery: tableQuery,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
try {
if (toCreateKeyspace) {
if (keyspaceCreateNew) {
await cassandraApiClient.createTableAndKeyspace(
userContext?.databaseAccount?.properties?.cassandraEndpoint,
userContext?.databaseAccount?.id,
@@ -246,7 +134,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneStartMessage, startKey);
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormErrors(errorMessage);
setFormError(errorMessage);
setIsExecuting(false);
const addCollectionPaneFailedMessage = {
...addCollectionPaneStartMessage,
@@ -256,129 +144,160 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
};
const handleOnChangeKeyspaceType = (ev: React.FormEvent<HTMLInputElement>, mode: string): void => {
setKeyspaceCreateNew(mode === "Create new");
};
const props: RightPaneFormProps = {
formError: formErrors,
formError,
isExecuting,
submitButtonText: "Apply",
submitButtonText: "OK",
onSubmit,
};
return (
<RightPaneForm {...props}>
<div className="paneMainContent">
<div className="seconddivpadding">
<p>
<Label required>
<div className="panelMainContent">
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip>
</Label>
</p>
</Text>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
className="panelRadioBtn"
aria-label="Create new keyspace"
checked={keyspaceCreateNew}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeKeyspaceType(e, "Create new")}
onChange={() => {
setKeyspaceCreateNew(true);
setIsKeyspaceShared(false);
setExistingKeyspaceId("");
}}
/>
<span className="throughputInputRadioBtnLabel">Create new</span>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="throughputInputRadioBtn"
className="panelRadioBtn"
aria-label="Use existing keyspace"
checked={!keyspaceCreateNew}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeKeyspaceType(e, "Use existing")}
onChange={() => {
setKeyspaceCreateNew(false);
setIsKeyspaceShared(false);
}}
/>
<span className="panelRadioBtnLabel">Use existing</span>
</Stack>
{keyspaceCreateNew && (
<Stack className="panelGroupSpacing">
<TextField
aria-required="true"
autoComplete="off"
styles={getTextFieldStyles()}
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new keyspace id"
size={40}
value={newKeyspaceId}
onChange={(e, newValue) => setNewKeyspaceId(newValue)}
ariaLabel="Keyspace id"
autoFocus
/>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label="Provision shared throughput"
checked={isKeyspaceShared}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => setIsKeyspaceShared(isChecked)}
/>
<InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within
the keyspace
</InfoTooltip>
</Stack>
)}
</Stack>
)}
{!keyspaceCreateNew && (
<Dropdown
ariaLabel="Choose existing keyspace id"
styles={{ root: { width: 300 }, title: { fontSize: 12 }, dropdownItem: { fontSize: 12 } }}
placeholder="Choose existing keyspace id"
defaultSelectedKey={existingKeyspaceId}
options={useDatabases.getState().databases?.map((keyspace) => ({
key: keyspace.id(),
text: keyspace.id(),
data: {
isShared: !!keyspace.offer(),
},
}))}
onChange={(event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) => {
setExistingKeyspaceId(option.key as string);
setIsKeyspaceShared(option.data.isShared);
}}
responsiveMode={999}
/>
)}
{!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()
}
isDatabase
isSharded
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
</Stack>
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Enter CQL command to create the table.{" "}
<Link href="https://aka.ms/cassandra-create-table" target="_blank">
Learn More
</Link>
</Text>
</Stack>
<Stack horizontal verticalAlign="center">
<Text variant="small" style={{ marginRight: 4 }}>
{`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`}
</Text>
<TextField
underlined
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true"
ariaLabel="addCollection-tableId"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Enter table Id"
size={20}
value={tableId}
onChange={(e, newValue) => setTableId(newValue)}
/>
<span className="throughputInputRadioBtnLabel">Use existing</span>
</Stack>
<TextField
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
list={keyspaceCreateNew ? "" : "keyspacesList"}
placeholder={keyspaceCreateNew ? "Type a new keyspace id" : "Choose existing keyspace id"}
size={40}
data-test="addCollection-keyspaceId"
value={keyspaceId}
onChange={(e, newValue) => setKeyspaceId(newValue)}
ariaLabel="Keyspace id"
autoFocus
/>
<datalist id="keyspacesList">
{keyspaceIds?.map((id: string, index: number) => (
<option key={index}>{id}</option>
))}
</datalist>
{canConfigureThroughput && keyspaceCreateNew && (
<div className="databaseProvision">
<input
tabIndex={0}
type="checkbox"
id="keyspaceSharedThroughput"
title="Provision shared throughput"
checked={keyspaceHasSharedOffer}
onChange={(e) => setKeyspaceHasSharedOffer(e.target.checked)}
/>
<span className="databaseProvisionText" aria-label="Provision keyspace throughput">
Provision keyspace throughput
</span>
<InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the
keyspace
</InfoTooltip>
</div>
)}
{canConfigureThroughput && keyspaceCreateNew && keyspaceHasSharedOffer && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
isDatabase
isSharded
setThroughputValue={(throughput: number) => setKeyspaceThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsSharedAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledge: boolean) => {
setSharedThroughputSpendAck(isAcknowledge);
}}
/>
</div>
)}
</div>
<div className="seconddivpadding">
<p>
<Label required>
Enter CQL command to create the table.
<a href="https://aka.ms/cassandra-create-table" target="_blank" rel="noreferrer">
Learn More
</a>
</Label>
</p>
<div aria-label={createTableQuery} style={{ float: "left", paddingTop: "3px", paddingRight: "3px" }}>
{createTableQuery}
</div>
<TextField
aria-required="true"
ariaLabel="addCollection-tableId"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Enter tableId"
size={20}
className="textfontclr"
value={tableId}
onChange={(e, newValue) => setTableId(newValue)}
style={{ marginBottom: "5px" }}
/>
<TextField
styles={getTextFieldStyles()}
multiline
id="editor-area"
rows={5}
@@ -386,10 +305,10 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
value={userTableQuery}
onChange={(e, newValue) => setUserTableQuery(newValue)}
/>
</div>
</Stack>
{canConfigureThroughput && keyspaceHasSharedOffer && !keyspaceCreateNew && (
<div className="seconddivpadding">
{!isServerlessAccount() && isKeyspaceShared && !keyspaceCreateNew && (
<Stack>
<input
type="checkbox"
id="tableSharedThroughput"
@@ -404,21 +323,17 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
billed in addition to the throughput amount you provisioned at the keyspace level.
</InfoTooltip>
</div>
</Stack>
)}
{canConfigureThroughput && (!keyspaceHasSharedOffer || dedicateTableThroughput) && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
setThroughputValue={(throughput: number) => setThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledge: boolean) => {
setThroughputSpendAck(isAcknowledge);
}}
/>
</div>
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
</div>
</RightPaneForm>
@@ -1,163 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CassandraAddCollectionPane Pane should render Default properly 1`] = `
<RightPaneForm
formError=""
onSubmit={[Function]}
submitButtonText="Apply"
>
<div
className="paneMainContent"
>
<div
className="seconddivpadding"
>
<p>
<StyledLabelBase
required={true}
>
Keyspace name
<InfoTooltip>
Select an existing keyspace or enter a new keyspace id.
</InfoTooltip>
</StyledLabelBase>
</p>
<Stack
horizontal={true}
verticalAlign="center"
>
<input
aria-label="Create new keyspace"
checked={true}
className="throughputInputRadioBtn"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="throughputInputRadioBtnLabel"
>
Create new
</span>
<input
aria-label="Use existing keyspace"
checked={false}
className="throughputInputRadioBtn"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="throughputInputRadioBtnLabel"
>
Use existing
</span>
</Stack>
<StyledTextFieldBase
aria-required="true"
ariaLabel="Keyspace id"
autoComplete="off"
autoFocus={true}
data-test="addCollection-keyspaceId"
list=""
onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Type a new keyspace id"
size={40}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
value=""
/>
<datalist
id="keyspacesList"
/>
<div
className="databaseProvision"
>
<input
checked={false}
id="keyspaceSharedThroughput"
onChange={[Function]}
tabIndex={0}
title="Provision shared throughput"
type="checkbox"
/>
<span
aria-label="Provision keyspace throughput"
className="databaseProvisionText"
>
Provision keyspace throughput
</span>
<InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the keyspace
</InfoTooltip>
</div>
</div>
<div
className="seconddivpadding"
>
<p>
<StyledLabelBase
required={true}
>
Enter CQL command to create the table.
<a
href="https://aka.ms/cassandra-create-table"
rel="noreferrer"
target="_blank"
>
Learn More
</a>
</StyledLabelBase>
</p>
<div
aria-label="CREATE TABLE "
style={
Object {
"float": "left",
"paddingRight": "3px",
"paddingTop": "3px",
}
}
>
CREATE TABLE
</div>
<StyledTextFieldBase
aria-required="true"
ariaLabel="addCollection-tableId"
autoComplete="off"
className="textfontclr"
onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Enter tableId"
size={20}
style={
Object {
"marginBottom": "5px",
}
}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
value=""
/>
<StyledTextFieldBase
aria-label="Table Schema"
id="editor-area"
multiline={true}
onChange={[Function]}
rows={5}
value="(userid int, name text, email text, PRIMARY KEY (userid))"
/>
</div>
<div>
<ThroughputInput
isDatabase={false}
isSharded={false}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
/>
</div>
</div>
</RightPaneForm>
`;
@@ -1,54 +1,53 @@
jest.mock("../../../Common/dataAccess/deleteCollection");
jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import { mount, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
import { Collection, Database } from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
describe("Delete Collection Confirmation Pane", () => {
describe("Explorer.isLastCollection()", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
describe("useDatabases.isLastCollection()", () => {
beforeAll(() => useDatabases.getState().clearDatabases());
afterEach(() => useDatabases.getState().clearDatabases());
it("should be true if 1 database and 1 collection", () => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
explorer.databases = ko.observableArray<Database>([database]);
expect(explorer.isLastCollection()).toBe(true);
const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
useDatabases.getState().addDatabases([database]);
expect(useDatabases.getState().isLastCollection()).toBe(true);
});
it("should be false if if 1 database and 2 collection", () => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection, {} as Collection]);
explorer.databases = ko.observableArray<Database>([database]);
expect(explorer.isLastCollection()).toBe(false);
const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([
{ id: ko.observable("coll1") } as Collection,
{ id: ko.observable("coll2") } as Collection,
]);
useDatabases.getState().addDatabases([database]);
expect(useDatabases.getState().isLastCollection()).toBe(false);
});
it("should be false if 2 database and 1 collection each", () => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
const database2 = {} as Database;
database2.collections = ko.observableArray<Collection>([{} as Collection]);
explorer.databases = ko.observableArray<Database>([database, database2]);
expect(explorer.isLastCollection()).toBe(false);
const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("coll1") } as Collection]);
const database2 = { id: ko.observable("testDB2") } as Database;
database2.collections = ko.observableArray<Collection>([{ id: ko.observable("coll2") } as Collection]);
useDatabases.getState().addDatabases([database, database2]);
expect(useDatabases.getState().isLastCollection()).toBe(false);
});
it("should be false if 0 databases", () => {
const database = {} as Database;
explorer.databases = ko.observableArray<Database>();
database.collections = ko.observableArray<Collection>();
expect(explorer.isLastCollection()).toBe(false);
expect(useDatabases.getState().isLastCollection()).toBe(false);
});
});
@@ -56,46 +55,39 @@ describe("Delete Collection Confirmation Pane", () => {
it("should return true if last collection and database does not have shared throughput else false", () => {
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
collectionName: "container",
};
const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
props.explorer.isLastCollection = () => true;
props.explorer.isSelectedDatabaseShared = () => true;
wrapper.setProps(props);
const wrapper = shallow(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
props.explorer.isLastCollection = () => false;
props.explorer.isSelectedDatabaseShared = () => false;
wrapper.setProps(props);
const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.nodeKind = "Database";
database.isDatabaseShared = ko.computed(() => false);
useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(database);
wrapper.setProps({ explorer: fakeExplorer });
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
database.isDatabaseShared = ko.computed(() => true);
wrapper.setProps({ explorer: fakeExplorer });
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
});
});
describe("submit()", () => {
let wrapper: ReactWrapper;
const selectedCollectionId = "testCol";
const databaseId = "testDatabase";
const fakeExplorer = {} as Explorer;
fakeExplorer.findSelectedCollection = () => {
return {
id: ko.observable<string>(selectedCollectionId),
databaseId,
rid: "test",
} as Collection;
};
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
fakeExplorer.selectedNode = ko.observable<TreeNode>();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
const database = { id: ko.observable(databaseId) } as Database;
const collection = {
id: ko.observable(selectedCollectionId),
nodeKind: "Collection",
database,
databaseId,
} as Collection;
database.collections = ko.observableArray<Collection>([collection]);
database.isDatabaseShared = ko.computed(() => false);
beforeAll(() => {
updateUserContext({
@@ -113,15 +105,17 @@ describe("Delete Collection Confirmation Pane", () => {
});
beforeEach(() => {
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
collectionName: "container",
};
wrapper = mount(<DeleteCollectionConfirmationPane {...props} />);
useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(collection);
});
afterEach(() => {
useDatabases.getState().clearDatabases();
useSelectedNode.getState().setSelectedNode(undefined);
});
it("should call delete collection", () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
@@ -138,6 +132,7 @@ describe("Delete Collection Confirmation Pane", () => {
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
wrapper
.find("#confirmCollectionId")
@@ -13,7 +13,10 @@ import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface DeleteCollectionConfirmationPaneProps {
explorer: Explorer;
}
@@ -27,13 +30,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean => {
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
};
const shouldRecordFeedback = (): boolean =>
useDatabases.getState().isLastCollection() &&
!useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName;
const onSubmit = async (): Promise<void> => {
const collection = explorer.findSelectedCollection();
const collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
setFormError(errorMessage);
@@ -58,7 +62,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
await deleteCollection(collection.databaseId, collection.id());
setIsExecuting(false);
explorer.selectedNode(collection.database);
useSelectedNode.getState().setSelectedNode(collection.database);
explorer.tabsManager?.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);
@@ -2,16 +2,9 @@
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
<DeleteCollectionConfirmationPane
closePanel={[Function]}
collectionName="container"
explorer={
Object {
"findSelectedCollection": [Function],
"isLastCollection": [Function],
"isSelectedDatabaseShared": [Function],
"refreshAllDatabases": [Function],
"selectedCollectionId": [Function],
"selectedNode": [Function],
}
}
>
@@ -43,7 +36,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small"
>
<span
className="css-102"
className="css-53"
>
Confirm by typing the
container
@@ -347,18 +340,18 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value=""
>
<div
className="ms-TextField root-104"
className="ms-TextField root-55"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-105"
className="ms-TextField-fieldGroup fieldGroup-56"
>
<input
aria-invalid={false}
autoFocus={true}
className="ms-TextField-field field-106"
className="ms-TextField-field field-57"
id="confirmCollectionId"
onBlur={[Function]}
onChange={[Function]}
@@ -373,355 +366,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</TextFieldBase>
</StyledTextFieldBase>
</div>
<div
className="deleteCollectionFeedback"
>
<Text
block={true}
variant="small"
>
<span
className="css-115"
>
Help us improve Azure Cosmos DB!
</span>
</Text>
<Text
block={true}
variant="small"
>
<span
className="css-115"
>
What is the reason why you are deleting this
container
?
</span>
</Text>
<StyledTextFieldBase
id="deleteCollectionFeedbackInput"
multiline={true}
onChange={[Function]}
rows={3}
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
value=""
>
<TextFieldBase
deferredValidationTime={200}
id="deleteCollectionFeedbackInput"
multiline={true}
onChange={[Function]}
resizable={true}
rows={3}
styles={[Function]}
theme={
Object {
"disableGlobalClassNames": false,
"effects": Object {
"elevation16": "0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108)",
"elevation4": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)",
"elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"roundedCorner2": "2px",
"roundedCorner4": "4px",
"roundedCorner6": "6px",
},
"fonts": Object {
"large": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "18px",
"fontWeight": 400,
},
"medium": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "14px",
"fontWeight": 400,
},
"mediumPlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "16px",
"fontWeight": 400,
},
"mega": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "68px",
"fontWeight": 600,
},
"small": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "12px",
"fontWeight": 400,
},
"smallPlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "12px",
"fontWeight": 400,
},
"superLarge": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "42px",
"fontWeight": 600,
},
"tiny": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "10px",
"fontWeight": 400,
},
"xLarge": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "20px",
"fontWeight": 600,
},
"xLargePlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "24px",
"fontWeight": 600,
},
"xSmall": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "10px",
"fontWeight": 400,
},
"xxLarge": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "28px",
"fontWeight": 600,
},
"xxLargePlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "32px",
"fontWeight": 600,
},
},
"isInverted": false,
"palette": Object {
"accent": "#0078d4",
"black": "#000000",
"blackTranslucent40": "rgba(0,0,0,.4)",
"blue": "#0078d4",
"blueDark": "#002050",
"blueLight": "#00bcf2",
"blueMid": "#00188f",
"green": "#107c10",
"greenDark": "#004b1c",
"greenLight": "#bad80a",
"magenta": "#b4009e",
"magentaDark": "#5c005c",
"magentaLight": "#e3008c",
"neutralDark": "#201f1e",
"neutralLight": "#edebe9",
"neutralLighter": "#f3f2f1",
"neutralLighterAlt": "#faf9f8",
"neutralPrimary": "#323130",
"neutralPrimaryAlt": "#3b3a39",
"neutralQuaternary": "#d2d0ce",
"neutralQuaternaryAlt": "#e1dfdd",
"neutralSecondary": "#605e5c",
"neutralSecondaryAlt": "#8a8886",
"neutralTertiary": "#a19f9d",
"neutralTertiaryAlt": "#c8c6c4",
"orange": "#d83b01",
"orangeLight": "#ea4300",
"orangeLighter": "#ff8c00",
"purple": "#5c2d91",
"purpleDark": "#32145a",
"purpleLight": "#b4a0ff",
"red": "#e81123",
"redDark": "#a4262c",
"teal": "#008272",
"tealDark": "#004b50",
"tealLight": "#00b294",
"themeDark": "#005a9e",
"themeDarkAlt": "#106ebe",
"themeDarker": "#004578",
"themeLight": "#c7e0f4",
"themeLighter": "#deecf9",
"themeLighterAlt": "#eff6fc",
"themePrimary": "#0078d4",
"themeSecondary": "#2b88d8",
"themeTertiary": "#71afe5",
"white": "#ffffff",
"whiteTranslucent40": "rgba(255,255,255,.4)",
"yellow": "#ffb900",
"yellowDark": "#d29200",
"yellowLight": "#fff100",
},
"rtl": undefined,
"semanticColors": Object {
"accentButtonBackground": "#0078d4",
"accentButtonText": "#ffffff",
"actionLink": "#323130",
"actionLinkHovered": "#201f1e",
"blockingBackground": "#FDE7E9",
"blockingIcon": "#FDE7E9",
"bodyBackground": "#ffffff",
"bodyBackgroundChecked": "#edebe9",
"bodyBackgroundHovered": "#f3f2f1",
"bodyDivider": "#edebe9",
"bodyFrameBackground": "#ffffff",
"bodyFrameDivider": "#edebe9",
"bodyStandoutBackground": "#faf9f8",
"bodySubtext": "#605e5c",
"bodyText": "#323130",
"bodyTextChecked": "#000000",
"buttonBackground": "#ffffff",
"buttonBackgroundChecked": "#c8c6c4",
"buttonBackgroundCheckedHovered": "#edebe9",
"buttonBackgroundDisabled": "#f3f2f1",
"buttonBackgroundHovered": "#f3f2f1",
"buttonBackgroundPressed": "#edebe9",
"buttonBorder": "#8a8886",
"buttonBorderDisabled": "#f3f2f1",
"buttonText": "#323130",
"buttonTextChecked": "#201f1e",
"buttonTextCheckedHovered": "#000000",
"buttonTextDisabled": "#a19f9d",
"buttonTextHovered": "#201f1e",
"buttonTextPressed": "#201f1e",
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"cardStandoutBackground": "#ffffff",
"defaultStateBackground": "#faf9f8",
"disabledBackground": "#f3f2f1",
"disabledBodySubtext": "#c8c6c4",
"disabledBodyText": "#a19f9d",
"disabledBorder": "#c8c6c4",
"disabledSubtext": "#d2d0ce",
"disabledText": "#a19f9d",
"errorBackground": "#FDE7E9",
"errorIcon": "#A80000",
"errorText": "#a4262c",
"focusBorder": "#605e5c",
"infoBackground": "#f3f2f1",
"infoIcon": "#605e5c",
"inputBackground": "#ffffff",
"inputBackgroundChecked": "#0078d4",
"inputBackgroundCheckedHovered": "#005a9e",
"inputBorder": "#605e5c",
"inputBorderHovered": "#323130",
"inputFocusBorderAlt": "#0078d4",
"inputForegroundChecked": "#ffffff",
"inputIcon": "#0078d4",
"inputIconDisabled": "#a19f9d",
"inputIconHovered": "#005a9e",
"inputPlaceholderBackgroundChecked": "#deecf9",
"inputPlaceholderText": "#605e5c",
"inputText": "#323130",
"inputTextHovered": "#201f1e",
"link": "#0078d4",
"linkHovered": "#004578",
"listBackground": "#ffffff",
"listHeaderBackgroundHovered": "#f3f2f1",
"listHeaderBackgroundPressed": "#edebe9",
"listItemBackgroundChecked": "#edebe9",
"listItemBackgroundCheckedHovered": "#e1dfdd",
"listItemBackgroundHovered": "#f3f2f1",
"listText": "#323130",
"listTextColor": "#323130",
"menuBackground": "#ffffff",
"menuDivider": "#c8c6c4",
"menuHeader": "#0078d4",
"menuIcon": "#0078d4",
"menuItemBackgroundChecked": "#edebe9",
"menuItemBackgroundHovered": "#f3f2f1",
"menuItemBackgroundPressed": "#edebe9",
"menuItemText": "#323130",
"menuItemTextHovered": "#201f1e",
"messageLink": "#005A9E",
"messageLinkHovered": "#004578",
"messageText": "#323130",
"primaryButtonBackground": "#0078d4",
"primaryButtonBackgroundDisabled": "#f3f2f1",
"primaryButtonBackgroundHovered": "#106ebe",
"primaryButtonBackgroundPressed": "#005a9e",
"primaryButtonBorder": "transparent",
"primaryButtonText": "#ffffff",
"primaryButtonTextDisabled": "#d2d0ce",
"primaryButtonTextHovered": "#ffffff",
"primaryButtonTextPressed": "#ffffff",
"severeWarningBackground": "#FED9CC",
"severeWarningIcon": "#D83B01",
"smallInputBorder": "#605e5c",
"successBackground": "#DFF6DD",
"successIcon": "#107C10",
"successText": "#107C10",
"variantBorder": "#edebe9",
"variantBorderHovered": "#a19f9d",
"warningBackground": "#FFF4CE",
"warningHighlight": "#ffb900",
"warningIcon": "#797775",
"warningText": "#323130",
},
"spacing": Object {
"l1": "20px",
"l2": "32px",
"m": "16px",
"s1": "8px",
"s2": "4px",
},
}
}
validateOnLoad={true}
value=""
>
<div
className="ms-TextField ms-TextField--multiline root-104"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-116"
>
<textarea
aria-invalid={false}
className="ms-TextField-field field-117"
id="deleteCollectionFeedbackInput"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onInput={[Function]}
rows={3}
value=""
/>
</div>
</div>
</div>
</TextFieldBase>
</StyledTextFieldBase>
</div>
</div>
</div>
<PanelFooterComponent
@@ -2434,7 +2078,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<button
aria-label="OK"
className="ms-Button ms-Button--primary root-119"
className="ms-Button ms-Button--primary root-66"
data-is-focusable={true}
id="sidePanelOkButton"
onClick={[Function]}
@@ -2446,16 +2090,16 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-120"
className="ms-Button-flexContainer flexContainer-67"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-121"
className="ms-Button-textContainer textContainer-68"
>
<span
className="ms-Button-label label-123"
id="id__6"
key="id__6"
className="ms-Button-label label-70"
id="id__3"
key="id__3"
>
OK
</span>
@@ -1,6 +1,6 @@
jest.mock("../../Common/dataAccess/deleteDatabase");
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import { mount, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
@@ -11,128 +11,104 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
describe("Delete Database Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
const selectedDatabaseId = "testDatabase";
let fakeExplorer: Explorer;
let database: Database;
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database.id = ko.observable<string>("testDatabse");
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
const wrapper = shallow(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
props.explorer.isLastNonEmptyDatabase = () => false;
props.explorer.isLastDatabase = () => false;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
props.explorer.isLastNonEmptyDatabase = () => false;
props.explorer.isLastDatabase = () => true;
props.explorer.isSelectedDatabaseShared = () => false;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "testDatabaseAccountName",
properties: {
cassandraEndpoint: "testEndpoint",
},
id: "testDatabaseAccountId",
} as DatabaseAccount,
apiType: "SQL",
});
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
});
describe("submit()", () => {
const selectedDatabaseId = "testDatabse";
const fakeExplorer = new Explorer();
beforeEach(() => {
fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
fakeExplorer.tabsManager = new TabsManager();
let wrapper: ReactWrapper;
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "testDatabaseAccountName",
properties: {
cassandraEndpoint: "testEndpoint",
},
id: "testDatabaseAccountId",
} as DatabaseAccount,
apiType: "SQL",
});
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
});
beforeEach(() => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
});
it("Should call delete database", () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("button")).toBe(true);
wrapper.find("button").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
wrapper.unmount();
});
it("should record feedback", async () => {
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#deleteDatabaseFeedbackInput")).toBe(true);
const feedbackText = "Test delete Database feedback text";
wrapper
.find("#deleteDatabaseFeedbackInput")
.hostNodes()
.simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
const deleteFeedback = new DeleteFeedback(
"testDatabaseAccountId",
"testDatabaseAccountName",
ApiKind.SQL,
feedbackText
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
wrapper.unmount();
database = {} as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
database.nodeKind = "Database";
useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(database);
});
afterEach(() => {
useDatabases.getState().clearDatabases();
useSelectedNode.getState().setSelectedNode(undefined);
});
it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => {
const wrapper = shallow(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().addDatabases([database]);
wrapper.setProps({ explorer: fakeExplorer });
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
useDatabases.getState().clearDatabases();
});
it("Should call delete database", () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("button")).toBe(true);
wrapper.find("button").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
wrapper.unmount();
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#deleteDatabaseFeedbackInput")).toBe(true);
const feedbackText = "Test delete Database feedback text";
wrapper
.find("#deleteDatabaseFeedbackInput")
.hostNodes()
.simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
const deleteFeedback = new DeleteFeedback(
"testDatabaseAccountId",
"testDatabaseAccountName",
ApiKind.SQL,
feedbackText
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
wrapper.unmount();
});
});
@@ -13,24 +13,26 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer;
selectedDatabase: Database;
}
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
explorer,
selectedDatabase,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const selectedDatabase: Database = useSelectedNode.getState().findSelectedDatabase();
const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
@@ -52,7 +54,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
closeSidePanel();
explorer.refreshAllDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
explorer.selectedNode(undefined);
useSelectedNode.getState().setSelectedNode(undefined);
selectedDatabase
.collections()
.forEach((collection: Collection) =>
@@ -70,7 +72,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
startKey
);
if (shouldRecordFeedback()) {
if (isLastNonEmptyDatabase()) {
const deleteFeedback = new DeleteFeedback(
userContext?.databaseAccount.id,
userContext?.databaseAccount.name,
@@ -100,10 +102,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}
};
const shouldRecordFeedback = (): boolean => {
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
};
const props: RightPaneFormProps = {
formError,
isExecuting: isLoading,
@@ -134,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}}
/>
</div>
{shouldRecordFeedback() && (
{isLastNonEmptyDatabase() && (
<div className="deleteDatabaseFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!
@@ -19,17 +19,11 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
@@ -44,38 +38,16 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -19,7 +19,8 @@ export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
const buttonLabel = "Ok";
const submit = () => {
const submit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
closeSidePanel();
};
@@ -1,17 +1,10 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { LoadQueryPane } from "./LoadQueryPane";
describe("Load Query Pane", () => {
it("should render Default properly", () => {
const fakeExplorer = {} as Explorer;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
};
const wrapper = shallow(<LoadQueryPane {...props} />);
const wrapper = shallow(<LoadQueryPane />);
expect(wrapper).toMatchSnapshot();
});
});
@@ -7,15 +7,10 @@ import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
interface LoadQueryPaneProps {
explorer: Explorer;
}
export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({ explorer }: LoadQueryPaneProps): JSX.Element => {
export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
@@ -59,21 +54,20 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({ explorer
};
const loadQueryFromFile = async (file: File): Promise<void> => {
const selectedCollection: Collection = explorer?.findSelectedCollection();
if (!selectedCollection) {
logError("No collection was selected", "LoadQueryPane.loadQueryFromFile");
} else if (userContext.apiType === "Mongo") {
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
} else {
selectedCollection.onNewQueryClick(selectedCollection, undefined);
}
const selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection();
const reader = new FileReader();
let fileData: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reader.onload = (evt: any): void => {
const fileData: string = evt.target.result;
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
queryTab.initialEditorContent(fileData);
queryTab.sqlQueryEditorContent(fileData);
fileData = evt.target.result;
if (!selectedCollection) {
logError("No collection was selected", "LoadQueryPane.loadQueryFromFile");
} else if (userContext.apiType === "Mongo") {
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
} else {
selectedCollection.onNewQueryClick(selectedCollection, undefined, fileData);
}
};
reader.onerror = (): void => {
+20
View File
@@ -0,0 +1,20 @@
import { ITextFieldStyles } from "@fluentui/react";
interface TextFieldStylesProps {
fontSize: number | string;
width: number | string;
}
export const getTextFieldStyles = (params?: TextFieldStylesProps): Partial<ITextFieldStyles> => ({
field: {
fontSize: params?.fontSize || 12,
selectors: {
"::placeholder": {
fontSize: params?.fontSize || 12,
},
},
},
root: {
width: params?.width || 300,
},
});
@@ -1,32 +1,38 @@
import { shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { SavedQueries } from "../../../Common/Constants";
import { Collection, Database } from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { SaveQueryPane } from "./SaveQueryPane";
describe("Save Query Pane", () => {
const fakeExplorer = {} as Explorer;
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
};
const wrapper = shallow(<SaveQueryPane {...props} />);
it("should return true if can save Queries else false", () => {
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
wrapper.setProps(props);
expect(wrapper.exists("#saveQueryInput")).toBe(true);
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => false);
wrapper.setProps(props);
expect(wrapper.exists("#saveQueryInput")).toBe(false);
});
it("should render Default properly", () => {
const wrapper = shallow(<SaveQueryPane {...props} />);
expect(wrapper.exists("#saveQueryInput")).toBe(false);
expect(wrapper).toMatchSnapshot();
});
it("should return true if can save Queries else false", () => {
useDatabases.getState().addDatabases([
{
id: ko.observable(SavedQueries.DatabaseName),
collections: ko.observableArray([
{
id: ko.observable(SavedQueries.CollectionName),
} as Collection,
]),
} as Database,
]);
const wrapper = shallow(<SaveQueryPane {...props} />);
expect(wrapper.exists("#saveQueryInput")).toBe(true);
});
});
@@ -9,7 +9,8 @@ import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab";
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
import { useDatabases } from "../../useDatabases";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
interface SaveQueryPaneProps {
@@ -24,17 +25,18 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
const title = "Save Query";
const { canSaveQueries } = explorer;
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
const submit = async (): Promise<void> => {
setFormError("");
if (!canSaveQueries()) {
if (!isSaveQueryEnabled()) {
setFormError("Cannot save query");
logConsoleError("Failed to save query: account not setup to save queries");
}
const queryTab = explorer && (explorer.tabsManager.activeTab() as QueryTab);
const query: string = queryTab && queryTab.sqlQueryEditorContent();
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab);
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
if (!queryName || queryName.length === 0) {
setFormError("No query name specified");
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
@@ -128,16 +130,16 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
onSubmit: () => {
canSaveQueries() ? submit() : setupQueries();
isSaveQueryEnabled() ? submit() : setupQueries();
},
};
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelMainContent">
{!canSaveQueries() ? (
{!isSaveQueryEnabled() ? (
<Text variant="small">{setupSaveQueriesText}</Text>
) : (
<TextField
@@ -7,6 +7,7 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
@@ -56,8 +57,10 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
try {
setLoadingTrue();
await explorer.notebookWorkspaceManager.createNotebookWorkspaceAsync(
userContext.databaseAccount && userContext.databaseAccount.id,
await createOrUpdate(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks
@@ -9,17 +9,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
@@ -34,38 +28,16 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -1,13 +1,10 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { UploadItemsPane } from "./UploadItemsPane";
const props = {
explorer: new Explorer(),
};
describe("Upload Items Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<UploadItemsPane {...props} />);
const wrapper = shallow(<UploadItemsPane />);
expect(wrapper).toMatchSnapshot();
});
});
@@ -3,15 +3,11 @@ import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface UploadItemsPaneProps {
explorer: Explorer;
}
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explorer }: UploadItemsPaneProps) => {
export const UploadItemsPane: FunctionComponent = () => {
const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
const [formError, setFormError] = useState<string>("");
@@ -25,7 +21,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
return;
}
const selectedCollection = explorer.findSelectedCollection();
const selectedCollection = useSelectedNode.getState().findSelectedCollection();
setIsExecuting(true);
selectedCollection
@@ -1,86 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Delete Database Confirmation Pane submit() Should call delete database 1`] = `
exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<DeleteDatabaseConfirmationPanel
closePanel={[Function]}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLastCollection": [Function],
"isLastNonEmptyDatabase": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isSelectedDatabaseShared": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
Object {
"refreshAllDatabases": [Function],
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
openNotificationConsole={[Function]}
selectedDatabase={
Object {
"collections": [Function],
"id": [Function],
}
}
>
<RightPaneForm
formError=""
@@ -103,7 +33,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
verticalAlign="center"
>
<div
className="ms-Stack panelInfoErrorContainer css-102"
className="ms-Stack panelInfoErrorContainer css-53"
>
<StyledIconBase
aria-label="warning"
@@ -392,7 +322,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
>
<i
aria-label="warning"
className="panelWarningIcon root-104"
className="panelWarningIcon root-55"
data-icon-name="WarningSolid"
role="img"
>
@@ -411,7 +341,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
>
<span
aria-label="message"
className="panelWarningErrorMessage css-105"
className="panelWarningErrorMessage css-56"
>
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
</span>
@@ -435,7 +365,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-105"
className="css-56"
>
Confirm by typing the database id
</span>
@@ -735,18 +665,18 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
validateOnLoad={true}
>
<div
className="ms-TextField root-107"
className="ms-TextField root-58"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-108"
className="ms-TextField-fieldGroup fieldGroup-59"
>
<input
aria-invalid={false}
autoFocus={true}
className="ms-TextField-field field-109"
className="ms-TextField-field field-60"
id="confirmDatabaseId"
onBlur={[Function]}
onChange={[Function]}
@@ -769,7 +699,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-126"
className="css-69"
>
Help us improve Azure Cosmos DB!
</span>
@@ -779,7 +709,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-126"
className="css-69"
>
What is the reason why you are deleting this database?
</span>
@@ -1081,17 +1011,17 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
validateOnLoad={true}
>
<div
className="ms-TextField ms-TextField--multiline root-107"
className="ms-TextField ms-TextField--multiline root-58"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-127"
className="ms-TextField-fieldGroup fieldGroup-70"
>
<textarea
aria-invalid={false}
className="ms-TextField-field field-128"
className="ms-TextField-field field-71"
id="deleteDatabaseFeedbackInput"
onBlur={[Function]}
onChange={[Function]}
@@ -2817,7 +2747,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
>
<button
aria-label="OK"
className="ms-Button ms-Button--primary root-118"
className="ms-Button ms-Button--primary root-73"
data-is-focusable={true}
id="sidePanelOkButton"
onClick={[Function]}
@@ -2829,16 +2759,16 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-119"
className="ms-Button-flexContainer flexContainer-74"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-120"
className="ms-Button-textContainer textContainer-75"
>
<span
className="ms-Button-label label-122"
id="id__3"
key="id__3"
className="ms-Button-label label-77"
id="id__6"
key="id__6"
>
OK
</span>
@@ -7,7 +7,6 @@ jest.mock("../Explorer");
const createExplorer = () => {
const mock = new Explorer();
mock.selectedNode = ko.observable();
mock.isNotebookEnabled = ko.observable(false);
mock.tabsManager = new TabsManager();
return mock as jest.Mocked<Explorer>;
+11 -15
View File
@@ -22,6 +22,8 @@ import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLaunc
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
export interface SplashScreenItem {
iconSrc: string;
@@ -59,7 +61,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() {
this.subscriptions.push(
this.container.selectedNode.subscribe(() => this.setState({})),
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
);
}
@@ -227,12 +229,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return items;
}
if (!this.container.isDatabaseNodeOrNoneSelected()) {
if (!useSelectedNode.getState().isDatabaseNodeOrNoneSelected()) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
},
title: "New SQL Query",
@@ -242,7 +244,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
},
title: "New Query",
@@ -265,20 +267,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "New Stored Procedure",
description: null,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
});
}
/* Scale & Settings */
let isShared = false;
if (this.container.isDatabaseNodeSelected()) {
isShared = this.container.findSelectedDatabase().isDatabaseShared();
} else if (this.container.isNodeKindSelected("Collection")) {
const database: ViewModels.Database = this.container.findSelectedCollection().getDatabase();
isShared = database && database.isDatabaseShared();
}
const isShared = useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
const label = isShared ? "Settings" : "Scale & Settings";
items.push({
@@ -286,7 +282,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: label,
description: null,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onSettingsClick();
},
});
@@ -308,8 +304,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: collectionId,
description: "Data",
onClick: () => {
const collection = this.container.findCollection(databaseId, collectionId);
collection && collection.openTab();
const collection = useDatabases.getState().findCollection(databaseId, collectionId);
collection?.openTab();
},
};
}
@@ -792,7 +792,7 @@ export default class QueryBuilderViewModel {
return null;
}
public checkIfClauseChanged(clause: QueryClauseViewModel): void {
this._queryViewModel.checkIfBuilderChanged(clause);
public checkIfClauseChanged(): void {
this._queryViewModel.checkIfBuilderChanged();
}
}
@@ -89,7 +89,7 @@ export default class QueryClauseViewModel {
);
this.and_or.subscribe((value) => {
this._queryBuilderViewModel.checkIfClauseChanged(this);
this._queryBuilderViewModel.checkIfClauseChanged();
});
this.field.subscribe((value) => {
this.changeField();
@@ -103,13 +103,13 @@ export default class QueryClauseViewModel {
// }
});
this.customTimeValue.subscribe((value) => {
this._queryBuilderViewModel.checkIfClauseChanged(this);
this._queryBuilderViewModel.checkIfClauseChanged();
});
this.value.subscribe((value) => {
this._queryBuilderViewModel.checkIfClauseChanged(this);
this._queryBuilderViewModel.checkIfClauseChanged();
});
this.operator.subscribe((value) => {
this._queryBuilderViewModel.checkIfClauseChanged(this);
this._queryBuilderViewModel.checkIfClauseChanged();
});
this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => {
this._queryBuilderViewModel.updateCanGroupClauses();
@@ -184,7 +184,7 @@ export default class QueryClauseViewModel {
this.type(QueryBuilderConstants.TableType.String);
}
}
this._queryBuilderViewModel.checkIfClauseChanged(this);
this._queryBuilderViewModel.checkIfClauseChanged();
}
private resetFromTimestamp(): void {
@@ -216,7 +216,7 @@ export default class QueryClauseViewModel {
this.timeValue("");
this.customTimeValue("");
}
this._queryBuilderViewModel.checkIfClauseChanged(this);
this._queryBuilderViewModel.checkIfClauseChanged();
}
// private customTimestampDialog(): Promise<any> {
@@ -1,16 +1,18 @@
import * as ko from "knockout";
import React from "react";
import * as _ from "underscore";
import { KeyCodes } from "../../../Common/Constants";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import QueryBuilderViewModel from "./QueryBuilderViewModel";
import QueryClauseViewModel from "./QueryClauseViewModel";
export default class QueryViewModel {
public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
public readonly topValueLimitMessage: string = "Please input a number between 0 and 1000.";
public queryBuilderViewModel = ko.observable<QueryBuilderViewModel>();
public isHelperActive = ko.observable<boolean>(true);
public isEditorActive = ko.observable<boolean>(false);
@@ -49,7 +51,7 @@ export default class QueryViewModel {
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
return userContext.apiType !== "Cassandra";
});
let initialOptions = this._tableEntityListViewModel.headers;
const initialOptions = this._tableEntityListViewModel.headers;
this.columnOptions = ko.observableArray<string>(initialOptions);
this.focusTopResult = ko.observable<boolean>(false);
this.focusExpandIcon = ko.observable<boolean>(false);
@@ -63,12 +65,12 @@ export default class QueryViewModel {
this.topValue() !== this.unchangedSaveTop()
);
this.queryBuilderViewModel().clauseArray.subscribe((value) => {
this.queryBuilderViewModel().clauseArray.subscribe(() => {
this.setFilter();
});
this.isExceedingLimit = ko.computed<boolean>(() => {
var currentTopValue: number = this.topValue();
const currentTopValue: number = this.topValue();
return currentTopValue < 0 || currentTopValue > 1000;
});
@@ -111,7 +113,7 @@ export default class QueryViewModel {
DataTableUtilities.forceRecalculateTableSize(); // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size.
};
public ontoggleAdvancedOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
public ontoggleAdvancedOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.toggleAdvancedOptions();
event.stopPropagation();
@@ -125,31 +127,29 @@ export default class QueryViewModel {
};
private setFilter = (): string => {
var queryString = this.isEditorActive()
const queryString = this.isEditorActive()
? this.queryText()
: userContext.apiType === "Cassandra"
? this.queryBuilderViewModel().getCqlFilterFromClauses()
: this.queryBuilderViewModel().getODataFilterFromClauses();
var filter = queryString;
const filter = queryString;
this.queryText(filter);
return this.queryText();
};
private setSqlFilter = (): string => {
var filter = this.queryBuilderViewModel().getSqlFilterFromClauses();
return filter;
return this.queryBuilderViewModel().getSqlFilterFromClauses();
};
private setCqlFilter = (): string => {
var filter = this.queryBuilderViewModel().getCqlFilterFromClauses();
return filter;
return this.queryBuilderViewModel().getCqlFilterFromClauses();
};
public isHelperEnabled = ko
.computed<boolean>(() => {
return (
this.queryText() === this.unchangedText() ||
this.queryText() === null ||
this.queryText() === undefined ||
this.queryText() === "" ||
this.isHelperActive()
);
@@ -159,13 +159,13 @@ export default class QueryViewModel {
});
public runQuery = (): DataTables.DataTable => {
var filter = this.setFilter();
let filter = this.setFilter();
if (filter && userContext.apiType !== "Cassandra") {
filter = filter.replace(/"/g, "'");
}
var top = this.topValue();
var selectOptions = this._getSelectedResults();
var select = selectOptions;
const top = this.topValue();
const selectOptions = this._getSelectedResults();
const select = selectOptions;
this._tableEntityListViewModel.tableQuery.filter = filter;
this._tableEntityListViewModel.tableQuery.top = top;
this._tableEntityListViewModel.tableQuery.select = select;
@@ -177,16 +177,16 @@ export default class QueryViewModel {
};
public clearQuery = (): DataTables.DataTable => {
this.queryText(null);
this.topValue(null);
this.selectText(null);
this.queryText();
this.topValue();
this.selectText();
this.selectMessage("");
// clears the queryBuilder and adds a new blank clause
this.queryBuilderViewModel().queryClauses.removeAll();
this.queryBuilderViewModel().addNewClause();
this._tableEntityListViewModel.tableQuery.filter = null;
this._tableEntityListViewModel.tableQuery.top = null;
this._tableEntityListViewModel.tableQuery.select = null;
this._tableEntityListViewModel.tableQuery.filter = undefined;
this._tableEntityListViewModel.tableQuery.top = undefined;
this._tableEntityListViewModel.tableQuery.select = undefined;
this._tableEntityListViewModel.oDataQuery("");
this._tableEntityListViewModel.sqlQuery("SELECT * FROM c");
this._tableEntityListViewModel.cqlQuery(
@@ -197,12 +197,11 @@ export default class QueryViewModel {
return this._tableEntityListViewModel.reloadTable(false);
};
public selectQueryOptions(): Promise<any> {
this.queryTablesTab.container.openTableSelectQueryPanel(this);
return null;
public selectQueryOptions() {
useSidePanel.getState().openSidePanel("Select Column", <TableQuerySelectPanel queryViewModel={this} />);
}
public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
public onselectQueryOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.selectQueryOptions();
event.stopPropagation();
@@ -212,7 +211,7 @@ export default class QueryViewModel {
};
public getSelectMessage(): void {
if (_.isEmpty(this.selectText()) || this.selectText() === null) {
if (_.isEmpty(this.selectText()) || this.selectText() === undefined) {
this.selectMessage("");
} else {
this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`);
@@ -220,7 +219,7 @@ export default class QueryViewModel {
}
public isSelected = ko.computed<boolean>(() => {
return !(_.isEmpty(this.selectText()) || this.selectText() === null);
return !(_.isEmpty(this.selectText()) || this.selectText() === undefined);
});
private setCheckToSave(): void {
@@ -230,7 +229,7 @@ export default class QueryViewModel {
this.isSaveEnabled(false);
}
public checkIfBuilderChanged(clause: QueryClauseViewModel): void {
public checkIfBuilderChanged(): void {
this.setFilter();
}
}
-6
View File
@@ -15,7 +15,6 @@ describe("Documents tab", () => {
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
hashLocation: "",
});
expect(documentsTab.buildQuery("")).toContain("select");
@@ -90,7 +89,6 @@ describe("Documents tab", () => {
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
hashLocation: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
@@ -104,7 +102,6 @@ describe("Documents tab", () => {
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
hashLocation: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
@@ -118,7 +115,6 @@ describe("Documents tab", () => {
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
hashLocation: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
@@ -135,7 +131,6 @@ describe("Documents tab", () => {
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
hashLocation: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
@@ -149,7 +144,6 @@ describe("Documents tab", () => {
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
hashLocation: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
+3 -2
View File
@@ -28,6 +28,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import DocumentId from "../Tree/DocumentId";
import { useSelectedNode } from "../useSelectedNode";
import template from "./DocumentsTab.html";
import TabsBase from "./TabsBase";
@@ -911,13 +912,13 @@ export default class DocumentsTab extends TabsBase {
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
};
}
}
+1 -1
View File
@@ -281,7 +281,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
}
/** Renders a Javascript object to be displayed inside Monaco Editor */
protected renderObjectForEditor(value: any, replacer: any, space: string | number): string {
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
return MongoUtility.tojson(value, null, false);
}
-29
View File
@@ -1,29 +0,0 @@
import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q";
import MongoUtility from "../../Common/MongoUtility";
import QueryTab from "./QueryTab";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { queryIterator } from "../../Common/MongoProxyClient";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
export default class MongoQueryTab extends QueryTab {
public collection: ViewModels.Collection;
constructor(options: ViewModels.QueryTabOptions) {
options.queryText = ""; // override sql query editor content for now so we only display mongo related help items
super(options);
this.isPreferredApiMongoDB = true;
this.monacoSettings = new ViewModels.MonacoEditorSettings("plaintext", false);
}
/** Renders a Javascript object to be displayed inside Monaco Editor */
protected renderObjectForEditor(value: any, replacer: any, space: string | number): string {
return MongoUtility.tojson(value, null, false);
}
protected _initIterator(): Q.Promise<MinimalQueryIterator> {
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
this._iterator = queryIterator(this.collection.databaseId, this.collection, this.sqlStatementToExecute());
return Q(this._iterator);
}
}
@@ -0,0 +1,47 @@
import React from "react";
import MongoUtility from "../../../Common/MongoUtility";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import { NewQueryTab } from "../QueryTab/QueryTab";
import QueryTabComponent, { IQueryTabComponentProps, ITabAccessor } from "../QueryTab/QueryTabComponent";
export interface IMongoQueryTabProps {
container: Explorer;
viewModelcollection?: ViewModels.Collection;
}
export class NewMongoQueryTab extends NewQueryTab {
public collection: ViewModels.Collection;
public iMongoQueryTabComponentProps: IQueryTabComponentProps;
public queryText: string;
constructor(options: ViewModels.QueryTabOptions, private mongoQueryTabProps: IMongoQueryTabProps) {
super(options, mongoQueryTabProps);
this.queryText = "";
this.iMongoQueryTabComponentProps = {
collection: options.collection,
isExecutionError: this.isExecutionError(),
tabId: this.tabId,
tabsBaseInstance: this,
queryText: this.queryText,
partitionKey: this.partitionKey,
container: this.mongoQueryTabProps.container,
onTabAccessor: (instance: ITabAccessor): void => {
this.iTabAccessor = instance;
},
isPreferredApiMongoDB: true,
monacoEditorSetting: "plaintext",
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
};
}
/** Renders a Javascript object to be displayed inside Monaco Editor */
//eslint-disable-next-line
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
return MongoUtility.tojson(value, undefined, false);
}
public render(): JSX.Element {
return <QueryTabComponent {...this.iMongoQueryTabComponentProps} />;
}
}
-15
View File
@@ -1,15 +0,0 @@
<iframe
name="explorer"
class="iframe"
style="width: 100%; height: 100%; border: 0px; padding: 0px; margin: 0px; overflow: hidden"
data-bind="
attr: {
src: url,
id: tabId
},
event:{
load: setContentFocus(event)
}"
title="Mongo Shell"
role="tabpanel"
></iframe>
@@ -0,0 +1,39 @@
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { TabOptions } from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import TabsBase from "../TabsBase";
import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent";
export interface IMongoShellTabProps {
container: Explorer;
}
export class NewMongoShellTab extends TabsBase {
public queryText: string;
public currentQuery: string;
public partitionKey: DataModels.PartitionKey;
public iMongoShellTabComponentProps: IMongoShellTabComponentProps;
public iMongoShellTabAccessor: IMongoShellTabAccessor;
constructor(options: TabOptions, private props: IMongoShellTabProps) {
super(options);
this.iMongoShellTabComponentProps = {
collection: this.collection,
tabsBaseInstance: this,
container: this.props.container,
onMongoShellTabAccessor: (instance: IMongoShellTabAccessor) => {
this.iMongoShellTabAccessor = instance;
},
};
}
public render(): JSX.Element {
return <MongoShellTabComponent {...this.iMongoShellTabComponentProps} />;
}
public onTabClick(): void {
this.manager?.activateTab(this);
this.iMongoShellTabAccessor.onTabClickEvent();
}
}
@@ -1,67 +1,101 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import { configContext, Platform } from "../../ConfigContext";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { isInvalidParentFrameOrigin, isReadyMessage } from "../../Utils/MessageValidation";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import template from "./MongoShellTab.html";
import TabsBase from "./TabsBase";
import React, { Component } from "react";
import * as Constants from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/MessageValidation";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import TabsBase from "../TabsBase";
export default class MongoShellTab extends TabsBase {
public readonly html = template;
public url: ko.Computed<string>;
private _container: Explorer;
//eslint-disable-next-line
class MessageType {
static IframeReady = "iframeready";
static Notification = "notification";
static Log = "log";
}
//eslint-disable-next-line
class LogType {
static Information = "information";
static Warning = "warning";
static Verbose = "verbose";
static InProgress = "inprogress";
static StartTrace = "start";
static SuccessTrace = "success";
static FailureTrace = "failure";
}
export interface IMongoShellTabAccessor {
onTabClickEvent: () => void;
}
export interface IMongoShellTabComponentStates {
url: string;
}
export interface IMongoShellTabComponentProps {
collection: ViewModels.CollectionBase;
tabsBaseInstance: TabsBase;
container: Explorer;
onMongoShellTabAccessor: (instance: IMongoShellTabAccessor) => void;
}
export default class MongoShellTabComponent extends Component<
IMongoShellTabComponentProps,
IMongoShellTabComponentStates
> {
private _runtimeEndpoint: string;
private _logTraces: Map<string, number>;
constructor(options: ViewModels.TabOptions) {
super(options);
constructor(props: IMongoShellTabComponentProps) {
super(props);
this._logTraces = new Map();
this._container = options.collection.container;
this.url = ko.computed<string>(() => {
const { databaseAccount: account } = userContext;
const resourceId = account?.id;
const accountName = account?.name;
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
let baseUrl = "/content/mongoshell/dist/";
if (userContext.portalEnv === "localhost") {
baseUrl = "/content/mongoshell/";
}
this.state = {
url: this.getURL(),
};
return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
props.onMongoShellTabAccessor({
onTabClickEvent: this.onTabClick.bind(this),
});
window.addEventListener("message", this.handleMessage.bind(this), false);
}
public setContentFocus(event: any): any {
// TODO: Work around cross origin security issue in Hosted Data Explorer by using Shell <-> Data Explorer messaging (253527)
// if(event.type === "load" && window.dataExplorerPlatform != PlatformType.Hosted) {
// let activeShell = event.target.contentWindow && event.target.contentWindow.mongo && event.target.contentWindow.mongo.shells && event.target.contentWindow.mongo.shells[0];
// activeShell && setTimeout(function(){
// activeShell.focus();
// },2000);
// }
public getURL(): string {
const { databaseAccount: account } = userContext;
const resourceId = account?.id;
const accountName = account?.name;
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
let baseUrl = "/content/mongoshell/dist/";
if (userContext.portalEnv === "localhost") {
baseUrl = "/content/mongoshell/";
}
return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
}
//eslint-disable-next-line
public setContentFocus(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {}
public onTabClick(): void {
super.onTabClick();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
this.props.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
public handleMessage(event: MessageEvent) {
public handleMessage(event: MessageEvent): void {
if (isInvalidParentFrameOrigin(event)) {
return;
}
const shellIframe: HTMLIFrameElement = <HTMLIFrameElement>document.getElementById(this.tabId);
const shellIframe: HTMLIFrameElement = document.getElementById(
this.props.tabsBaseInstance.tabId
) as HTMLIFrameElement;
if (!shellIframe) {
return;
@@ -73,9 +107,9 @@ export default class MongoShellTab extends TabsBase {
return;
}
if (event.data.eventType == MessageType.IframeReady) {
if (event.data.eventType === MessageType.IframeReady) {
this.handleReadyMessage(event, shellIframe);
} else if (event.data.eventType == MessageType.Notification) {
} else if (event.data.eventType === MessageType.Notification) {
this.handleNotificationMessage(event, shellIframe);
} else {
this.handleLogMessage(event, shellIframe);
@@ -98,8 +132,8 @@ export default class MongoShellTab extends TabsBase {
documentEndpoint.length -
(Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length)
) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.collection.databaseId;
const collectionId = this.collection.id();
const databaseId = this.props.collection.databaseId;
const collectionId = this.props.collection.id();
const apiEndpoint = configContext.BACKEND_ENDPOINT;
const encryptedAuthToken: string = userContext.accessToken;
@@ -121,6 +155,7 @@ export default class MongoShellTab extends TabsBase {
);
}
//eslint-disable-next-line
private handleLogMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) {
if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") {
return;
@@ -144,6 +179,7 @@ export default class MongoShellTab extends TabsBase {
TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Mark, dataToLog);
break;
case LogType.StartTrace:
//eslint-disable-next-line
const telemetryTraceId: number = TelemetryProcessor.traceStart(Action.MongoShell, dataToLog);
this._logTraces.set(shellTraceId, telemetryTraceId);
break;
@@ -168,6 +204,7 @@ export default class MongoShellTab extends TabsBase {
}
}
//eslint-disable-next-line
private handleNotificationMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) {
if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") {
return;
@@ -188,20 +225,19 @@ export default class MongoShellTab extends TabsBase {
return logConsoleProgress(dataToLog);
}
}
}
class MessageType {
static IframeReady: string = "iframeready";
static Notification: string = "notification";
static Log: string = "log";
}
class LogType {
static Information: string = "information";
static Warning: string = "warning";
static Verbose: string = "verbose";
static InProgress: string = "inprogress";
static StartTrace: string = "start";
static SuccessTrace: string = "success";
static FailureTrace: string = "failure";
render(): JSX.Element {
return (
<iframe
name="explorer"
className="iframe"
style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }}
src={this.state.url}
id={this.props.tabsBaseInstance.tabId}
onLoad={(event) => this.setContentFocus(event)}
title="Mongo Shell"
role="tabpanel"
></iframe>
);
}
}
-335
View File
@@ -1,335 +0,0 @@
<div class="tab-pane" data-bind="attr:{id: tabId}" role="tabpanel">
<div class="tabPaneContentContainer">
<div class="mongoQueryHelper" data-bind="visible: isPreferredApiMongoDB && sqlQueryEditorContent().length === 0">
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the
documents.
</div>
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: maybeSubQuery">
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/info_color.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery"
>Please see Cosmos sub query documentation for further information</a
>
</span>
</div>
</div>
<div class="queryEditorWithSplitter" data-bind="attr: { id: queryEditorId }">
<editor
class="queryEditor"
data-bind="css: { mongoQueryEditor: isPreferredApiMongoDB }"
params="{
content: initialEditorContent,
contentType: monacoSettings.language,
isReadOnly: monacoSettings.readOnly,
lineNumbers: 'on',
ariaLabel: 'Editing Query',
updatedContent: sqlQueryEditorContent,
selectedContent: selectedContent
}"
></editor>
<!-- Splitter - Start -->
<div class="splitter ui-resizable-handle ui-resizable-s" data-bind="attr: { id: splitterId }">
<img class="queryEditorHorizontalSplitter" src="/HorizontalSplitter.svg" alt="Splitter" />
</div>
</div>
<!-- Splitter - End -->
<!-- Script for results metadata that is common to all APIs -->
<script type="text/html" id="result-metadata-template">
<span>
<span data-bind="text: showingDocumentsDisplayText"></span>
</span>
<span class="queryResultDivider" data-bind="visible: fetchNextPageButton.enabled"> | </span>
<span class="queryResultNextEnable" data-bind="visible: fetchNextPageButton.enabled">
<a data-bind="click: onFetchNextPageClick">
<span>Load more</span>
<img class="queryResultnextImg" src="/Query-Editor-Next.svg" alt="Fetch next page" />
</a>
</span>
</script>
<!-- Query Errors Tab - Start-->
<div class="active queryErrorsHeaderContainer" data-bind="visible: !!error()">
<span class="queryErrors" data-toggle="tab" data-bind="attr: { href: '#queryerrors' + tabId }">Errors</span>
</div>
<!-- Query Errors Tab - End -->
<!-- Query Results & Errors Content Container - Start-->
<div class="queryResultErrorContentContainer">
<div
class="queryEditorWatermark"
data-bind="visible: allResultsMetadata().length === 0 && !error() && !queryResults() && !isExecuting()"
>
<p><img src="/RunQuery.png" alt="Execute Query Watermark" /></p>
<p class="queryEditorWatermarkText">Execute a query to see the results</p>
</div>
<div
class="queryResultsErrorsContent"
data-bind="visible: allResultsMetadata().length > 0 || !!error() || queryResults()"
>
<div class="togglesWithMetadata" data-bind="visible: !error()">
<div
class="toggles"
aria-label="Successful execution"
id="execute-query-toggles"
data-bind="event: { keydown: onToggleKeyDown }"
tabindex="0"
>
<div class="tab">
<input type="radio" class="radio" value="result" />
<span
class="toggleSwitch"
role="button"
tabindex="0"
data-bind="click: toggleResult, css:{ selectedToggle: isResultToggled(), unselectedToggle: !isResultToggled() }"
aria-label="Results"
>Results</span
>
</div>
<div class="tab">
<input type="radio" class="radio" value="logs" />
<span
class="toggleSwitch"
role="button"
tabindex="0"
data-bind="click: toggleMetrics, css:{ selectedToggle: isMetricsToggled(), unselectedToggle: !isMetricsToggled() }"
aria-label="Query stats"
>Query Stats</span
>
</div>
</div>
<div
class="result-metadata"
data-bind="template: { name: 'result-metadata-template' }, visible: isResultToggled()"
></div>
</div>
<json-editor
params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }"
data-bind="visible: queryResults() && queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
>
</json-editor>
<div
class="queryMetricsSummaryContainer"
data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && !error()"
>
<table class="queryMetricsSummary">
<caption>
Query Statistics
</caption>
<thead class="queryMetricsSummaryHead">
<tr class="queryMetricsSummaryHeader queryMetricsSummaryTuple">
<th title="METRIC" scope="col">METRIC</th>
<th title="VALUE" scope="col">VALUE</th>
</tr>
</thead>
<tbody class="queryMetricsSummaryBody" data-bind="with: aggregatedQueryMetrics">
<tr class="queryMetricsSummaryTuple">
<td title="Request Charge">Request Charge</td>
<td>
<span
data-bind="text: $parent.requestChargeDisplayText, attr: { title: $parent.requestChargeDisplayText }"
></span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple">
<td title="Showing Results">Showing Results</td>
<td>
<span
data-bind="text: $parent.showingDocumentsDisplayText, attr: { title: $parent.showingDocumentsDisplayText }"
></span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Retrieved document count">Retrieved document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of retrieved documents</span>
</span>
</td>
<td><span data-bind="text: retrievedDocumentCount, attr: { title: retrievedDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Retrieved document size">Retrieved document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of retrieved documents in bytes</span>
</span>
</td>
<td>
<span data-bind="text: retrievedDocumentSize, attr: { title: retrievedDocumentSize }"></span>
<span>bytes</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Output document count">Output document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Number of output documents</span>
</span>
</td>
<td><span data-bind="text: outputDocumentCount, attr: { title: outputDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Output document size">Output document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of output documents in bytes</span>
</span>
</td>
<td>
<span data-bind="text: outputDocumentSize, attr: { title: outputDocumentSize }"></span>
<span>bytes</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Index hit document count">Index hit document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of documents matched by the filter</span>
</span>
</td>
<td><span data-bind="text: indexHitDocumentCount, attr: { title: indexHitDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Index lookup time">Index lookup time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in physical index layer</span>
</span>
</td>
<td>
<span data-bind="text: indexLookupTime, attr: { title: indexLookupTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Document load time">Document load time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in loading documents</span>
</span>
</td>
<td>
<span data-bind="text: documentLoadTime, attr: { title: documentLoadTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Query engine execution time">Query engine execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText queryEngineExeTimeInfo"
>Time spent by the query engine to execute the query expression (excludes other execution times
like load documents or write results)</span
>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.queryEngineExecutionTime, attr: { title: runtimeExecutionTimes.queryEngineExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="System function execution time">System function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing system (built-in) functions</span>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.systemFunctionExecutionTime, attr: { title: runtimeExecutionTimes.systemFunctionExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="User defined function execution time">User defined function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing user-defined functions</span>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.userDefinedFunctionExecutionTime, attr: { title: runtimeExecutionTimes.userDefinedFunctionExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Document write time">Document write time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent to write query result set to response buffer</span>
</span>
</td>
<td>
<span data-bind="text: documentWriteTime, attr: { title: documentWriteTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.roundTrips() != null">
<td title="Round Trips">Round Trips</td>
<td><span data-bind="text: $parent.roundTrips, attr: { title: $parent.roundTrips }"></span></td>
</tr>
<!-- TODO: Report activity id for mongo queries -->
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.activityId() != null">
<td title="Activity id">Activity id</td>
<td></td>
<td><span data-bind="text: $parent.activityId, attr: { title: $parent.activityId }"></span></td>
</tr>
</tbody>
</table>
<div class="downloadMetricsLinkContainer" data-bind="visible: $parent.isQueryMetricsEnabled">
<a
id="downloadMetricsLink"
role="button"
tabindex="0"
data-bind="event: { click: onDownloadQueryMetricsCsvClick, keypress: onDownloadQueryMetricsCsvKeyPress }"
>
<img class="downloadCsvImg" src="/DownloadQuery.svg" alt="download query metrics csv" />
<span>Per-partition query metrics (CSV)</span>
</a>
</div>
</div>
<!-- Query Errors Content - Start-->
<div
class="tab-pane active"
data-bind="
id: {
href: 'queryerrors' + tabId
},
visible: !!error()"
>
<div class="errorContent">
<span class="errorMessage" data-bind="text: error"></span>
<span class="errorDetailsLink">
<a
data-bind="click: $parent.onErrorDetailsClick, event: { keypress: $parent.onErrorDetailsKeyPress }"
id="error-display"
tabindex="0"
aria-label="Error details link"
>More details</a
>
</span>
</div>
</div>
<!-- Query Errors Content - End-->
</div>
</div>
<!-- Results & Errors Content Container - End-->
</div>
</div>
-311
View File
@@ -1,311 +0,0 @@
@import "../../../less/Common/Constants";
@import "../../../less/Common/TabCommon";
@MongoQueryEditorHeight: 50px;
@ResultsTextFontWeight: 600;
@ToggleHeight: 30px;
@ToggleWidth: 250px;
@QueryEngineExeInfo: 305px;
.tab-pane {
.tabContentContainer();
.tabPaneContentContainer {
.tabContentContainer();
.mongoQueryHelper {
margin:@DefaultSpace 0px 0px 44px;
position: absolute;
top: 115px; //this is to avoid the jump of query editor
}
.queryEditorWithSplitter {
.flex-display();
.flex-direction();
flex-shrink: 0;
height: 100%;
width: 100%;
margin-left: @SmallSpace;
.queryEditor {
.flex-display();
height: 100%;
width: 100%;
margin-top: @SmallSpace;
.jsonEditor {
border: none;
margin-top: @SmallSpace;
}
}
.queryEditor.mongoQueryEditor {
margin-top: 32px;
overflow: hidden;
}
.queryEditorHorizontalSplitter {
margin: auto;
display: block;
}
}
.queryErrorsHeaderContainer {
padding: 24px @LargeSpace 0px @MediumSpace;
.queryErrors {
font-size: @mediumFontSize;
list-style-type: none;
color: @BaseDark;
font-weight: bold;
margin-left: 24px;
}
}
.queryResultErrorContentContainer {
.flex-display();
.flex-direction();
font-size: @DefaultFontSize;
padding: @DefaultSpace;
height: 100%;
width: 100%;
overflow: hidden;
.queryEditorWatermark {
text-align: center;
margin: auto;
height: 25vh; // this is to align the water mark in center of the layout.
p {
margin-bottom: @LargeSpace;
color: @BaseHigh;
}
.queryEditorWatermarkText {
color: @BaseHigh;
font-size: @DefaultFontSize;
font-family: @DataExplorerFont;
}
}
.queryResultsErrorsContent {
height: 100%;
margin-left: @MediumSpace;
.flex-display();
.flex-direction();
.togglesWithMetadata {
margin-top: @MediumSpace;
.toggles {
height: @ToggleHeight;
width: @ToggleWidth;
margin-left: @MediumSpace;
&:focus {
.focus();
}
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
}
.result-metadata {
padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace;
.queryResultDivider {
margin-left: @SmallSpace;
margin-right: @SmallSpace;
}
.queryResultNextEnable {
color: @AccentMediumHigh;
font-size: @mediumFontSize;
cursor: pointer;
img {
height: @ImgHeight;
width: @ImgWidth;
margin-left: @SmallSpace;
}
}
.queryResultNextDisable {
color: @BaseMediumHigh;
opacity: 0.5;
font-size: @mediumFontSize;
img {
height: @ImgHeight;
width: @ImgWidth;
margin-left: @SmallSpace;
}
}
}
.tab-pane.active {
height: 100%;
width: 100%;
}
.errorContent {
.flex-display();
width: 60%;
white-space: nowrap;
font-size: @mediumFontSize;
padding: 0px @MediumSpace 0px @MediumSpace;
.errorMessage {
padding: @SmallSpace;
overflow: hidden;
text-overflow: ellipsis;
}
}
.errorDetailsLink {
cursor: pointer;
padding: @SmallSpace;
}
.queryMetricsSummaryContainer {
.flex-display();
.flex-direction();
overflow: hidden;
.queryMetricsSummary {
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
table-layout: fixed;
display: block;
height: auto;
overflow-y: auto;
overflow-x: hidden;
caption {
width: 100px;
}
.queryMetricsSummaryHead {
.flex-display();
}
.queryMetricsSummaryHeader.queryMetricsSummaryTuple {
font-size: 10px;
}
.queryMetricsSummaryBody {
.flex-display();
.flex-direction();
}
.queryMetricsSummaryTuple {
border-bottom: 1px solid @BaseMedium;
height: 32px;
font-size: 12px;
width: 100%;
.flex-display();
th, td {
padding: @DefaultSpace;
&:nth-child(1) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 0 50%;
}
&:nth-child(3) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 0 50%;
}
.queryMetricInfoTooltip {
.infoTooltip();
&:hover .queryMetricTooltipText {
.tooltipVisible();
}
&:focus .queryMetricTooltipText {
.tooltipVisible();
}
.queryMetricTooltipText {
top: -50px;
width: auto;
white-space: nowrap;
left: 6px;
visibility: hidden;
background-color: @BaseHigh;
color: @BaseLight;
position: absolute;
z-index: 1;
padding: @MediumSpace;
&::after {
border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px;
bottom: -14px;
.tooltipTextAfter();
}
}
.queryEngineExeTimeInfo {
width: @QueryEngineExeInfo;
top: -85px;
white-space: pre-wrap;
}
}
}
}
}
.downloadMetricsLinkContainer {
margin: 24px 0px 24px @MediumSpace;
#downloadMetricsLink {
color: @BaseHigh;
padding: @DefaultSpace;
font-size: @mediumFontSize;
border: @ButtonBorderWidth solid @BaseLight;
cursor: pointer;
&:hover {
.hover();
}
&:active {
border: @ButtonBorderWidth dashed @AccentMedium;
.active();
}
}
}
}
json-editor {
.flex-display();
.flex-direction();
overflow: hidden;
height: 100%;
width: 100%;
padding: @SmallSpace 0px @SmallSpace @MediumSpace;
}
}
}
}
}
-96
View File
@@ -1,96 +0,0 @@
import * as ko from "knockout";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import QueryTab from "./QueryTab";
describe("Query Tab", () => {
function getNewQueryTabForContainer(container: Explorer): QueryTab {
const database = {
container: container,
id: ko.observable<string>("test"),
isDatabaseShared: () => false,
} as ViewModels.Database;
const collection = {
container: container,
databaseId: "test",
id: ko.observable<string>("test"),
} as ViewModels.Collection;
return new QueryTab({
tabKind: ViewModels.CollectionTabKind.Query,
collection: collection,
database: database,
title: "",
tabPath: "",
hashLocation: "",
});
}
describe("shouldSetSystemPartitionKeyContainerPartitionKeyValueUndefined", () => {
const collection = {
id: ko.observable<string>("withoutsystempk"),
partitionKey: {
systemKey: true,
},
} as ViewModels.Collection;
it("no container with system pk, should not set partition key option", () => {
const iteratorOptions = QueryTab.getIteratorOptions(collection);
expect(iteratorOptions.initialHeaders).toBeUndefined();
});
});
describe("isQueryMetricsEnabled()", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("should be true for accounts using SQL API", () => {
updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(true);
});
it("should be false for accounts using other APIs", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(false);
});
});
describe("Save Queries command button", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("should be visible when using a supported API", () => {
updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(true);
});
it("should not be visible when using an unsupported API", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(false);
});
});
});
-594
View File
@@ -1,594 +0,0 @@
import * as ko from "knockout";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
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 ViewModels from "../../Contracts/ViewModels";
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import template from "./QueryTab.html";
import TabsBase from "./TabsBase";
enum ToggleState {
Result,
QueryMetrics,
}
export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate {
public readonly html = template;
public queryEditorId: string;
public executeQueryButton: ViewModels.Button;
public fetchNextPageButton: ViewModels.Button;
public saveQueryButton: ViewModels.Button;
public initialEditorContent: ko.Observable<string>;
public maybeSubQuery: ko.Computed<boolean>;
public sqlQueryEditorContent: ko.Observable<string>;
public selectedContent: ko.Observable<string>;
public sqlStatementToExecute: ko.Observable<string>;
public queryResults: ko.Observable<string>;
public error: ko.Observable<string>;
public statusMessge: ko.Observable<string>;
public statusIcon: ko.Observable<string>;
public allResultsMetadata: ko.ObservableArray<ViewModels.QueryResultsMetadata>;
public showingDocumentsDisplayText: ko.Observable<string>;
public requestChargeDisplayText: ko.Observable<string>;
public isTemplateReady: ko.Observable<boolean>;
public splitterId: string;
public splitter: Splitter;
public isPreferredApiMongoDB: boolean;
public queryMetrics: ko.Observable<Map<string, DataModels.QueryMetrics>>;
public aggregatedQueryMetrics: ko.Observable<DataModels.QueryMetrics>;
public activityId: ko.Observable<string>;
public roundTrips: ko.Observable<number>;
public toggleState: ko.Observable<ToggleState>;
public isQueryMetricsEnabled: ko.Computed<boolean>;
protected monacoSettings: ViewModels.MonacoEditorSettings;
private _executeQueryButtonTitle: ko.Observable<string>;
protected _iterator: MinimalQueryIterator;
private _isSaveQueriesEnabled: ko.Computed<boolean>;
private _resourceTokenPartitionKey: string;
_partitionKey: DataModels.PartitionKey;
constructor(options: ViewModels.QueryTabOptions) {
super(options);
this.queryEditorId = `queryeditor${this.tabId}`;
this.showingDocumentsDisplayText = ko.observable<string>();
this.requestChargeDisplayText = ko.observable<string>();
const defaultQueryText = options.queryText != void 0 ? options.queryText : "SELECT * FROM c";
this.initialEditorContent = ko.observable<string>(defaultQueryText);
this.sqlQueryEditorContent = ko.observable<string>(defaultQueryText);
this._executeQueryButtonTitle = ko.observable<string>("Execute Query");
this.selectedContent = ko.observable<string>();
this.selectedContent.subscribe((selectedContent: string) => {
if (!selectedContent.trim()) {
this._executeQueryButtonTitle("Execute Query");
} else {
this._executeQueryButtonTitle("Execute Selection");
}
});
this.sqlStatementToExecute = ko.observable<string>("");
this.queryResults = ko.observable<string>("");
this.statusMessge = ko.observable<string>();
this.statusIcon = ko.observable<string>();
this.allResultsMetadata = ko.observableArray<ViewModels.QueryResultsMetadata>([]);
this.error = ko.observable<string>();
this._partitionKey = options.partitionKey;
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.splitterId = this.tabId + "_splitter";
this.isPreferredApiMongoDB = false;
this.aggregatedQueryMetrics = ko.observable<DataModels.QueryMetrics>();
this._resetAggregateQueryMetrics();
this.queryMetrics = ko.observable<Map<string, DataModels.QueryMetrics>>(new Map());
this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)));
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
return userContext.apiType === "SQL" || false;
});
this.activityId = ko.observable<string>();
this.roundTrips = ko.observable<number>();
this.toggleState = ko.observable<ToggleState>(ToggleState.Result);
this.monacoSettings = new ViewModels.MonacoEditorSettings("sql", false);
this.executeQueryButton = {
enabled: ko.computed<boolean>(() => {
return !!this.sqlQueryEditorContent() && this.sqlQueryEditorContent().length > 0;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this._isSaveQueriesEnabled = ko.computed<boolean>(() => {
const container = this.collection && this.collection.container;
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
});
this.maybeSubQuery = ko.computed<boolean>(function () {
const sql = this.sqlQueryEditorContent();
return sql && /.*\(.*SELECT.*\)/i.test(sql);
}, this);
this.saveQueryButton = {
enabled: this._isSaveQueriesEnabled,
visible: this._isSaveQueriesEnabled,
};
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady) {
const splitterBounds: SplitterBounds = {
min: Constants.Queries.QueryEditorMinHeightRatio * window.innerHeight,
max: $("#" + this.tabId).height() - Constants.Queries.QueryEditorMaxHeightRatio * window.innerHeight,
};
this.splitter = new Splitter({
splitterId: this.splitterId,
leftId: this.queryEditorId,
bounds: splitterBounds,
direction: SplitterDirection.Horizontal,
});
}
});
this.fetchNextPageButton = {
enabled: ko.computed<boolean>(() => {
const allResultsMetadata = this.allResultsMetadata() || [];
const numberOfResultsMetadata = allResultsMetadata.length;
if (numberOfResultsMetadata === 0) {
return false;
}
if (allResultsMetadata[numberOfResultsMetadata - 1].hasMoreResults) {
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this._buildCommandBarOptions();
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
}
public onExecuteQueryClick = async (): Promise<void> => {
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
this.sqlStatementToExecute(sqlStatement);
this.allResultsMetadata([]);
this.queryResults("");
this._iterator = undefined;
await this._executeQueryDocumentsPage(0);
};
public onSaveQueryClick = (): void => {
this.collection && this.collection.container && this.collection.container.openSaveQueryPanel();
};
public onSavedQueriesClick = (): void => {
this.collection && this.collection.container && this.collection.container.openBrowseQueriesPanel();
};
public async onFetchNextPageClick(): Promise<void> {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
}
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
useNotificationConsole.getState().expandConsole();
return false;
};
public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onErrorDetailsClick(src, null);
return false;
}
return true;
};
public toggleResult(): void {
this.toggleState(ToggleState.Result);
this.queryResults.valueHasMutated(); // needed to refresh the json-editor component
}
public toggleMetrics(): void {
this.toggleState(ToggleState.QueryMetrics);
}
public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.LeftArrow) {
this.toggleResult();
event.stopPropagation();
return false;
} else if (event.keyCode === Constants.KeyCodes.RightArrow) {
this.toggleMetrics();
event.stopPropagation();
return false;
}
return true;
};
public togglesOnFocus(): void {
const focusElement = document.getElementById("execute-query-toggles");
focusElement && focusElement.focus();
}
public isResultToggled(): boolean {
return this.toggleState() === ToggleState.Result;
}
public isMetricsToggled(): boolean {
return this.toggleState() === ToggleState.QueryMetrics;
}
public onDownloadQueryMetricsCsvClick = (source: any, event: MouseEvent): boolean => {
this._downloadQueryMetricsCsvData();
return false;
};
public onDownloadQueryMetricsCsvKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || Constants.KeyCodes.Enter) {
this._downloadQueryMetricsCsvData();
return false;
}
return true;
};
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
this.error("");
this.roundTrips(undefined);
if (this._iterator === undefined) {
this._initIterator();
}
await this._queryDocumentsPage(firstItemIndex);
}
// TODO: Position and enable spinner when request is in progress
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
this.isExecutionError(false);
this._resetAggregateQueryMetrics();
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
const queryDocuments = async (firstItemIndex: number) =>
await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
this.isExecuting(true);
try {
const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent(
firstItemIndex,
queryDocuments
);
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex,
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
// we let users query for the next page because the SDK sometimes specifies there are more elements
// even though there aren't any so we should not update the prior query results.
return;
}
const documents: any[] = queryResults.documents;
const results = this.renderObjectForEditor(documents, null, 4);
const resultsDisplay: string =
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
this.showingDocumentsDisplayText(resultsDisplay);
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
this.queryResults(results);
TelemetryProcessor.traceSuccess(
Action.ExecuteQuery,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
} catch (error) {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
this.error(errorMessage);
TelemetryProcessor.traceFailure(
Action.ExecuteQuery,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
document.getElementById("error-display").focus();
} finally {
this.isExecuting(false);
this.togglesOnFocus();
}
}
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
if (!metricsMap) {
return;
}
Object.keys(metricsMap).forEach((key: string) => {
this.queryMetrics().set(key, metricsMap[key]);
});
this.queryMetrics.valueHasMutated();
}
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
if (!metricsMap) {
return null;
}
const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics();
metricsMap.forEach((queryMetrics) => {
if (queryMetrics) {
aggregatedMetrics.documentLoadTime =
queryMetrics.documentLoadTime &&
this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.documentLoadTime);
aggregatedMetrics.documentWriteTime =
queryMetrics.documentWriteTime &&
this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.documentWriteTime);
aggregatedMetrics.indexHitDocumentCount =
queryMetrics.indexHitDocumentCount &&
this._normalize(queryMetrics.indexHitDocumentCount) +
this._normalize(aggregatedMetrics.indexHitDocumentCount);
aggregatedMetrics.outputDocumentCount =
queryMetrics.outputDocumentCount &&
this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount);
aggregatedMetrics.outputDocumentSize =
queryMetrics.outputDocumentSize &&
this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize);
aggregatedMetrics.indexLookupTime =
queryMetrics.indexLookupTime &&
this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.indexLookupTime);
aggregatedMetrics.retrievedDocumentCount =
queryMetrics.retrievedDocumentCount &&
this._normalize(queryMetrics.retrievedDocumentCount) +
this._normalize(aggregatedMetrics.retrievedDocumentCount);
aggregatedMetrics.retrievedDocumentSize =
queryMetrics.retrievedDocumentSize &&
this._normalize(queryMetrics.retrievedDocumentSize) +
this._normalize(aggregatedMetrics.retrievedDocumentSize);
aggregatedMetrics.vmExecutionTime =
queryMetrics.vmExecutionTime &&
this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.vmExecutionTime);
aggregatedMetrics.totalQueryExecutionTime =
queryMetrics.totalQueryExecutionTime &&
this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.totalQueryExecutionTime);
aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime =
aggregatedMetrics.runtimeExecutionTimes &&
this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime);
aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime =
aggregatedMetrics.runtimeExecutionTimes &&
this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime);
aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime =
aggregatedMetrics.runtimeExecutionTimes &&
this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime);
}
});
return aggregatedMetrics;
}
public _downloadQueryMetricsCsvData(): void {
const csvData: string = this._generateQueryMetricsCsvData();
if (!csvData) {
return;
}
if (navigator.msSaveBlob) {
// for IE and Edge
navigator.msSaveBlob(
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
"PerPartitionQueryMetrics.csv"
);
} else {
const downloadLink: HTMLAnchorElement = document.createElement("a");
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
downloadLink.target = "_self";
downloadLink.download = "QueryMetricsPerPartition.csv";
// for some reason, FF displays the download prompt only when
// the link is added to the dom so we add and remove it
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
}
}
protected _initIterator(): void {
const options: any = QueryTab.getIteratorOptions(this.collection);
if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey;
}
this._iterator = queryDocuments(
this.collection.databaseId,
this.collection.id(),
this.sqlStatementToExecute(),
options
);
}
public static getIteratorOptions(container: ViewModels.CollectionBase): any {
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
return options;
}
private _normalize(value: number): number {
if (!value) {
return 0;
}
return value;
}
private _resetAggregateQueryMetrics(): void {
this.aggregatedQueryMetrics({
clientSideMetrics: {},
documentLoadTime: undefined,
documentWriteTime: undefined,
indexHitDocumentCount: undefined,
outputDocumentCount: undefined,
outputDocumentSize: undefined,
indexLookupTime: undefined,
retrievedDocumentCount: undefined,
retrievedDocumentSize: undefined,
vmExecutionTime: undefined,
queryPreparationTimes: undefined,
runtimeExecutionTimes: {
queryEngineExecutionTime: undefined,
systemFunctionExecutionTime: undefined,
userDefinedFunctionExecutionTime: undefined,
},
totalQueryExecutionTime: undefined,
});
}
private _generateQueryMetricsCsvData(): string {
if (!this.queryMetrics()) {
return null;
}
const queryMetrics = this.queryMetrics();
let csvData: string = "";
const columnHeaders: string =
[
"Partition key range id",
"Retrieved document count",
"Retrieved document size (in bytes)",
"Output document count",
"Output document size (in bytes)",
"Index hit document count",
"Index lookup time (ms)",
"Document load time (ms)",
"Query engine execution time (ms)",
"System function execution time (ms)",
"User defined function execution time (ms)",
"Document write time (ms)",
].join(",") + "\n";
csvData = csvData + columnHeaders;
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
const partitionKeyRangeData: string =
[
partitionKeyRangeId,
queryMetric.retrievedDocumentCount,
queryMetric.retrievedDocumentSize,
queryMetric.outputDocumentCount,
queryMetric.outputDocumentSize,
queryMetric.indexHitDocumentCount,
queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(),
queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(),
queryMetric.runtimeExecutionTimes &&
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime &&
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(),
queryMetric.runtimeExecutionTimes &&
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime &&
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(),
queryMetric.runtimeExecutionTimes &&
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime &&
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(),
queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(),
].join(",") + "\n";
csvData = csvData + partitionKeyRangeData;
});
return csvData;
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (this.executeQueryButton.visible()) {
const label = this._executeQueryButtonTitle();
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: this.onExecuteQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.executeQueryButton.enabled(),
});
}
if (this.saveQueryButton.visible()) {
const label = "Save Query";
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveQueryButton.enabled(),
});
}
return buttons;
}
private _buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([this.executeQueryButton.visible, this.executeQueryButton.enabled, this._executeQueryButtonTitle])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
}
+57
View File
@@ -0,0 +1,57 @@
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase";
import QueryTabComponent from "./QueryTabComponent";
export interface IQueryTabProps {
container: Explorer;
}
export class NewQueryTab extends TabsBase {
public queryText: string;
public currentQuery: string;
public partitionKey: DataModels.PartitionKey;
public iQueryTabComponentProps: IQueryTabComponentProps;
public iTabAccessor: ITabAccessor;
constructor(options: QueryTabOptions, private props: IQueryTabProps) {
super(options);
this.partitionKey = options.partitionKey;
this.iQueryTabComponentProps = {
collection: this.collection,
isExecutionError: this.isExecutionError(),
tabId: this.tabId,
tabsBaseInstance: this,
queryText: options.queryText,
partitionKey: this.partitionKey,
container: this.props.container,
onTabAccessor: (instance: ITabAccessor): void => {
this.iTabAccessor = instance;
},
isPreferredApiMongoDB: false,
};
}
public render(): JSX.Element {
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
}
public onTabClick(): void {
this.manager?.activateTab(this);
this.iTabAccessor.onTabClickEvent();
}
public onCloseTabButtonClick(): void {
this.manager?.closeTab(this);
if (this.iTabAccessor) {
this.iTabAccessor.onCloseClickEvent(true);
}
}
public getContainer(): Explorer {
return this.props.container;
}
}
@@ -0,0 +1,285 @@
@import "../../../../less/Common/Constants.less";
@import "../../../../less/Common/TabCommon.less";
@MongoQueryEditorHeight: 50px;
@ResultsTextFontWeight: 600;
@ToggleHeight: 30px;
@ToggleWidth: 250px;
@QueryEngineExeInfo: 305px;
.tab-pane {
.tabContentContainer();
.tabPaneContentContainer {
position: relative;
.tabContentContainer();
.mongoQueryHelper {
margin: @DefaultSpace 0px 0px 44px;
}
.splitter-layout {
.layout-pane-primary {
overflow: hidden !important;
.queryEditor {
.flex-display();
height: 100%;
width: 100%;
margin-top: @SmallSpace;
.jsonEditor {
border: none;
margin-top: @SmallSpace;
}
}
}
.queryEditor.mongoQueryEditor {
margin-top: 32px;
overflow: hidden;
}
.queryEditorHorizontalSplitter {
margin: auto;
display: block;
}
}
.queryErrorsHeaderContainer {
padding: 24px @LargeSpace 0px @MediumSpace;
.queryErrors {
font-size: @mediumFontSize;
list-style-type: none;
color: @BaseDark;
font-weight: bold;
margin-left: 24px;
}
}
.queryResultErrorContentContainer {
.flex-display();
.flex-direction();
font-size: @DefaultFontSize;
padding: @DefaultSpace;
height: 100%;
width: 100%;
overflow: hidden;
.queryEditorWatermark {
text-align: center;
margin: auto;
height: 25vh; // this is to align the water mark in center of the layout.
p {
margin-bottom: @LargeSpace;
color: @BaseHigh;
}
.queryEditorWatermarkText {
color: @BaseHigh;
font-size: @DefaultFontSize;
font-family: @DataExplorerFont;
}
}
.queryResultsErrorsContent {
height: 100%;
margin-left: @MediumSpace;
.flex-display();
.flex-direction();
div[role="tabpanel"] {
height: 100%;
div:nth-child(1) {
height: 100%;
}
}
.result-metadata {
padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace;
height: auto !important;
.queryResultDivider {
margin-left: @SmallSpace;
margin-right: @SmallSpace;
}
.queryResultNextEnable {
color: @AccentMediumHigh;
font-size: @mediumFontSize;
cursor: pointer;
img {
height: @ImgHeight;
width: @ImgWidth;
margin-left: @SmallSpace;
}
}
.queryResultNextDisable {
color: @BaseMediumHigh;
opacity: 0.5;
font-size: @mediumFontSize;
img {
height: @ImgHeight;
width: @ImgWidth;
margin-left: @SmallSpace;
}
}
}
.tab-pane.active {
height: 100%;
width: 100%;
}
.errorContent {
.flex-display();
width: 60%;
white-space: nowrap;
font-size: @mediumFontSize;
padding: 0px @MediumSpace 0px @MediumSpace;
.errorMessage {
padding: @SmallSpace;
overflow: hidden;
text-overflow: ellipsis;
}
}
.errorDetailsLink {
cursor: pointer;
padding: @SmallSpace;
}
.queryMetricsSummaryContainer {
.flex-display();
.flex-direction();
overflow: hidden;
position: relative;
height: 100%;
.queryMetricsSummary {
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
table-layout: fixed;
display: block;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
caption {
width: 100px;
}
.queryMetricsSummaryHead {
.flex-display();
}
.queryMetricsSummaryHeader.queryMetricsSummaryTuple {
font-size: 10px;
}
.queryMetricsSummaryBody {
.flex-display();
.flex-direction();
}
.queryMetricsSummaryTuple {
border-bottom: 1px solid @BaseMedium;
height: 32px;
font-size: 12px;
width: 100%;
.flex-display();
th,
td {
padding: @DefaultSpace;
&:nth-child(1) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 0 50%;
}
&:nth-child(3) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 0 50%;
}
.queryMetricInfoTooltip {
.infoTooltip();
&:hover .queryMetricTooltipText {
.tooltipVisible();
}
&:focus .queryMetricTooltipText {
.tooltipVisible();
}
.queryMetricTooltipText {
top: -50px;
width: auto;
white-space: nowrap;
left: 6px;
visibility: hidden;
background-color: @BaseHigh;
color: @BaseLight;
position: absolute;
z-index: 1;
padding: @MediumSpace;
&::after {
border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px;
bottom: -14px;
.tooltipTextAfter();
}
}
.queryEngineExeTimeInfo {
width: @QueryEngineExeInfo;
top: -85px;
white-space: pre-wrap;
}
}
}
}
}
.downloadMetricsLinkContainer {
margin: 24px 0px 50px @MediumSpace;
position: sticky;
#downloadMetricsLink {
color: @BaseHigh;
padding: @DefaultSpace;
font-size: @mediumFontSize;
border: @ButtonBorderWidth solid @BaseLight;
cursor: pointer;
&:hover {
.hover();
}
&:active {
border: @ButtonBorderWidth dashed @AccentMedium;
.active();
}
}
}
}
json-editor {
.flex-display();
.flex-direction();
overflow: hidden;
height: 100%;
width: 100%;
padding: @SmallSpace 0px @SmallSpace @MediumSpace;
}
}
}
}
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More