mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-10 22:47:37 +01:00
Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate/QueryTablesTab
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -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} /> : <></>;
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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: () => {
|
||||
|
||||
+6
-8
@@ -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,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);
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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">* </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">* </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">* </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>
|
||||
|
||||
-163
@@ -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>
|
||||
`;
|
||||
+55
-60
@@ -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")
|
||||
|
||||
+9
-5
@@ -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
|
||||
);
|
||||
|
||||
+10
-366
@@ -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 => {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
+29
-30
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+98
-62
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user