Merge branch 'master'

This commit is contained in:
sunilyadav840 2021-07-12 01:26:43 +05:30
commit 42b7d8fe09
146 changed files with 6543 additions and 6209 deletions

View File

@ -44,7 +44,6 @@ src/Definitions/png.d.ts
src/Definitions/svg.d.ts
src/Explorer/ComponentRegisterer.test.ts
src/Explorer/ComponentRegisterer.ts
src/Explorer/ContextMenuButtonFactory.ts
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
src/Explorer/Controls/DynamicList/DynamicList.test.ts
@ -106,17 +105,10 @@ src/Explorer/Notebook/NotebookContentClient.ts
src/Explorer/Notebook/NotebookContentItem.ts
src/Explorer/Notebook/NotebookUtil.ts
src/Explorer/OpenActionsStubs.ts
src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
src/Explorer/Tables/DataTable/DataTableBuilder.ts
@ -133,7 +125,6 @@ src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryViewModel.ts
src/Explorer/Tables/TableDataClient.ts
src/Explorer/Tables/TableEntityProcessor.ts
src/Explorer/Tables/Utilities.ts
@ -143,14 +134,8 @@ src/Explorer/Tabs/DocumentsTab.test.ts
src/Explorer/Tabs/DocumentsTab.ts
src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/MongoQueryTab.ts
src/Explorer/Tabs/MongoShellTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts
src/Explorer/Tabs/QueryTablesTab.ts
src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/StoredProcedureTab.ts
src/Explorer/Tabs/TabComponents.ts
src/Explorer/Tabs/TabsBase.ts
src/Explorer/Tabs/TriggerTab.ts
@ -159,7 +144,6 @@ src/Explorer/Tree/AccessibleVerticalList.ts
src/Explorer/Tree/Collection.test.ts
src/Explorer/Tree/Collection.ts
src/Explorer/Tree/ConflictId.ts
src/Explorer/Tree/Database.ts
src/Explorer/Tree/DocumentId.ts
src/Explorer/Tree/ObjectId.ts
src/Explorer/Tree/ResourceTokenCollection.ts
@ -174,38 +158,12 @@ src/GitHub/GitHubConnector.ts
src/GitHub/GitHubContentProvider.test.ts
src/GitHub/GitHubContentProvider.ts
src/GitHub/GitHubOAuthService.ts
src/HostedExplorer.ts
src/Index.ts
src/Juno/JunoClient.test.ts
src/Juno/JunoClient.ts
src/Main.ts
src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts
src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts
src/Platform/Emulator/DataAccessUtility.ts
src/Platform/Emulator/ExplorerFactory.ts
src/Platform/Emulator/Main.ts
src/Platform/Emulator/NotificationsClient.ts
src/Platform/Hosted/ArmResourceUtils.ts
src/Platform/Hosted/Authorization.ts
src/Platform/Hosted/DataAccessUtility.ts
src/Platform/Hosted/ExplorerFactory.ts
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
src/Platform/Hosted/Main.ts
src/Platform/Hosted/Maint.test.ts
src/Platform/Hosted/NotificationsClient.ts
src/Platform/Portal/DataAccessUtility.ts
src/Platform/Portal/ExplorerFactory.ts
src/Platform/Portal/Main.ts
src/Platform/Portal/NotificationsClient.ts
src/PlatformType.ts
src/ReactDevTools.ts
src/ResourceProvider/IResourceProviderClient.test.ts
src/ResourceProvider/IResourceProviderClient.ts
src/ResourceProvider/ResourceProviderClient.ts
src/ResourceProvider/ResourceProviderClientFactory.ts
src/RouteHandlers/RouteHandler.ts
src/RouteHandlers/TabRouteHandler.test.ts
src/RouteHandlers/TabRouteHandler.ts
src/Shared/Constants.ts
src/Shared/DefaultExperienceUtility.test.ts
src/Shared/DefaultExperienceUtility.ts
@ -248,15 +206,7 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx
src/NotebookViewer/NotebookViewer.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx
src/Explorer/Controls/ResizeSensorReactComponent/ResizeSensorComponent.tsx
src/Explorer/Controls/Spark/ClusterSettingsComponent.tsx
src/Explorer/Controls/Spark/ClusterSettingsComponentAdapter.tsx
src/Explorer/Controls/Tabs/TabComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
@ -304,8 +254,5 @@ src/Explorer/Tabs/NotebookViewerTab.tsx
src/Explorer/Tabs/TerminalTab.tsx
src/Explorer/Tree/ResourceTreeAdapter.tsx
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
src/GalleryViewer/Cards/GalleryCardComponent.tsx
src/GalleryViewer/GalleryViewer.tsx
src/GalleryViewer/GalleryViewerComponent.tsx
__mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx

View File

@ -724,45 +724,24 @@ execute-sproc-params-pane {
.results-container,
.errors-container {
padding: @MediumSpace 0px 0px @MediumSpace;
height: 100%;
.flex-display();
.flex-direction();
overflow: hidden;
.toggles {
height: @ToggleHeight;
width: @ToggleWidth;
margin-left: @MediumSpace;
&:focus {
.focus();
}
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
.enterInputParameters {
padding: @LargeSpace @MediumSpace;
}
div[role="tabpanel"] {
height: 100%;
padding-bottom: 50px;
}
}
.errors-container {
padding-left: (2 * @MediumSpace);
padding: @MediumSpace 0px 0px @MediumSpace;
.errors-header {
font-weight: 700;
font-size: @DefaultFontSize;
@ -3161,3 +3140,10 @@ settings-pane {
padding-left: 10px;
padding-right: 0px;
}
.spinner {
width: 100%;
position: absolute;
z-index: 1;
background: white;
height: 100%;
}

View File

@ -201,3 +201,11 @@
.migration:disabled {
background-color: #ccc;
}
.trigger-field {
width: 40%;
margin-top: 10px
}
.trigger-form {
padding: 10px 30px 10px 30px;
}

118
package-lock.json generated
View File

@ -3709,14 +3709,84 @@
}
},
"@nteract/editor": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.2.tgz",
"integrity": "sha512-Wtj0kJUSoBZsWUh82JGt6miqYS0jt0k+3SD3cnW9socayxp2KB0Qbqhh2NtrF9ysxVHWnQT8iUarJjpGIdNyng==",
"version": "10.1.12",
"resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.12.tgz",
"integrity": "sha512-bsUrCctukjWdpKNWQOQmhfxMCQ/SBVIO6+RkazI4y4dVeeP3KMP8nxfhzIbzTMNSkyynps/deZFjpDWqRhG+Dg==",
"requires": {
"@nteract/messaging": "^7.0.10",
"@nteract/outputs": "^3.0.9",
"codemirror": "5.57.0",
"@nteract/messaging": "^7.0.19",
"@nteract/outputs": "^3.0.11",
"codemirror": "5.61.1",
"rxjs": "^6.3.3"
},
"dependencies": {
"@nteract/commutable": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@nteract/commutable/-/commutable-7.4.5.tgz",
"integrity": "sha512-RYqyMvkFt/04GQ9T+hGYgr9/LEy0dAYJ2QKn930TFX004KjfBT6Tt8VSLFyHWkXqPwyJ0jKMCJwqLcGOI/atqg==",
"requires": {
"immutable": "^4.0.0-rc.12",
"uuid": "^8.0.0"
}
},
"@nteract/messaging": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/@nteract/messaging/-/messaging-7.0.19.tgz",
"integrity": "sha512-gRPMxJr741/BshrfCcPSbm5iVyRU2TKmAv9jeQzk0MZEGy+Y1A0REO+eptkt4Ma0OXlvDxON6JEDauk8+2xt4w==",
"requires": {
"@nteract/types": "^7.1.9",
"@types/uuid": "^8.0.0",
"lodash.clonedeep": "^4.5.0",
"rxjs": "^6.6.0",
"uuid": "^8.0.0"
}
},
"@nteract/outputs": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@nteract/outputs/-/outputs-3.0.11.tgz",
"integrity": "sha512-LeT9ViBf+fTPSubZ9dMe7128kg0rl1jIG54V0n2GiU5RuYnUz21FU0IOaLMPUfFMO1VyVEOW5jDc3PAQx5/Kwg==",
"requires": {
"@nteract/markdown": "^4.5.2",
"@nteract/mathjax": "^4.0.11",
"ansi-to-react": "^6.0.5",
"react-json-tree": "^0.12.1"
}
},
"@nteract/types": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@nteract/types/-/types-7.1.9.tgz",
"integrity": "sha512-a7lGMWdjfz2QGlZbAiFHifU9Nhk9ntwg/iKUTMIMRPY1Wfs5UreHSMt+vZ8OY5HGjxicfHozBatGDKXeKXFHMQ==",
"requires": {
"@nteract/commutable": "^7.4.5",
"immutable": "^4.0.0-rc.12",
"rxjs": "^6.6.0",
"uuid": "^8.0.0"
}
},
"react-base16-styling": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.7.0.tgz",
"integrity": "sha512-lTa/VSFdU6BOAj+FryOe7OTZ0OBP8GXPOnCS0QnZi7G3zhssWgIgwl0eUL77onXx/WqKPFndB3ZeC77QC/l4Dw==",
"requires": {
"base16": "^1.0.0",
"lodash.curry": "^4.1.1",
"lodash.flow": "^3.5.0",
"pure-color": "^1.3.0"
}
},
"react-json-tree": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.12.1.tgz",
"integrity": "sha512-j6fkRY7ha9XMv1HPVakRCsvyFwHGR5AZuwO8naBBeZXnZbbLor5tpcUxS/8XD01+D1v7ZN5p+7LU+9V1uyASiQ==",
"requires": {
"prop-types": "^15.7.2",
"react-base16-styling": "^0.7.0"
}
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}
}
},
"@nteract/epics": {
@ -8067,9 +8137,9 @@
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"codemirror": {
"version": "5.57.0",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz",
"integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg=="
"version": "5.61.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
"integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
},
"collapse-white-space": {
"version": "1.0.6",
@ -17699,12 +17769,6 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -18508,9 +18572,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.20",
@ -18737,9 +18801,9 @@
"integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg=="
},
"marked": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.3.tgz",
"integrity": "sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.6.tgz",
"integrity": "sha512-S2mYj0FzTQa0dLddssqwRVW4EOJOVJ355Xm2Vcbm+LU7GQRGWvwbO5K87OaPSOux2AwTSgtPPaXmc8sDPrhn2A==",
"dev": true
},
"martinez-polygon-clipping": {
@ -24381,12 +24445,6 @@
"universalify": "^2.0.0"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@ -24402,9 +24460,9 @@
"dev": true
},
"typescript": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
"dev": true
},
"typestyle": {

View File

@ -22,7 +22,7 @@
"@nteract/data-explorer": "8.0.3",
"@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.2",
"@nteract/editor": "10.1.12",
"@nteract/fixtures": "2.3.0",
"@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0",
@ -174,7 +174,7 @@
"tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0",
"typedoc": "0.20.36",
"typescript": "4.2.4",
"typescript": "4.3.4",
"url-loader": "1.1.1",
"wait-on": "4.0.2",
"webpack": "4.46.0",

View File

@ -4,6 +4,7 @@ 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 { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
@ -176,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) {

View File

@ -120,6 +120,14 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
const armAPIVersion = params.get("armAPIVersion") || "";
updateConfigContext({ armAPIVersion });
}
if (params.has("armEndpoint")) {
const ARM_ENDPOINT = params.get("armEndpoint") || "";
updateConfigContext({ ARM_ENDPOINT });
}
if (params.has("aadEndpoint")) {
const AAD_ENDPOINT = params.get("aadEndpoint") || "";
updateConfigContext({ AAD_ENDPOINT });
}
if (params.has("platform")) {
const platform = params.get("platform");
switch (platform) {

View File

@ -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;

View File

@ -1,172 +0,0 @@
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import AddUdfIcon from "../../images/AddUdf.svg";
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
export interface CollectionContextMenuButtonParams {
databaseId: string;
collectionId: string;
}
export interface DatabaseContextMenuButtonParams {
databaseId: string;
}
/**
* New resource tree (in ReactJS)
*/
export class ResourceTreeContextMenuButtonFactory {
public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(databaseId),
label: `New ${getCollectionName()}`,
},
];
if (userContext.apiType !== "Tables") {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () => container.openDeleteDatabaseConfirmationPane(),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
}
return items;
}
public static createCollectionContextMenuButton(
container: Explorer,
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
label: "New SQL Query",
});
}
if (userContext.apiType === "Mongo") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),
label: "New Query",
});
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
},
label: "New Trigger",
});
}
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () => container.openDeleteCollectionConfirmationPane(),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
return items;
}
public static createStoreProcedureContextMenuItems(
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: "Delete Store Procedure",
},
];
}
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: "Delete Trigger",
},
];
}
public static createUserDefinedFunctionContextMenuItems(
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: "Delete User Defined Function",
},
];
}
}

View File

@ -0,0 +1,189 @@
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 { useNotebook } from "./Notebook/useNotebook";
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 refreshDatabases={() => container.refreshAllDatabases()} />
),
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 (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: useNotebook.getState().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 refreshDatabases={() => container.refreshAllDatabases()} />
),
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",
},
];
};

View File

@ -1,6 +1,11 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react";
import { loadMonaco, monaco } from "../../LazyMonaco";
// import "./EditorReact.less";
interface EditorReactStates {
showEditor: boolean;
}
export interface EditorReactProps {
language: string;
content: string;
@ -13,13 +18,16 @@ export interface EditorReactProps {
editorKey?: string;
}
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 {
@ -42,7 +50,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) {
@ -83,6 +96,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 {

View File

@ -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()));
}
}

View File

@ -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
});

View File

@ -30,7 +30,7 @@ import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
const title: string = "Open Saved Queries";
const title = "Open Saved Queries";
export interface QueriesGridComponentProps {
queriesClient: QueriesClient;
@ -196,9 +196,9 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
{
key: "Action",
name: "Action",
fieldName: null,
fieldName: undefined,
minWidth: 70,
onRender: (query: Query, index: number, column: IColumn) => {
onRender: (query: Query) => {
const buttonProps: IButtonProps = {
iconProps: {
iconName: "More",
@ -214,19 +214,15 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
{
key: "Open",
text: "Open query",
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, menuItem: any) => {
onClick: () => {
this.props.onQuerySelect(query);
},
},
{
key: "Delete",
text: "Delete query",
onClick: async (
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
menuItem: any
) => {
onClick: async () => {
if (window.confirm("Are you sure you want to delete this query?")) {
const container = window.dataExplorer;
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,

View File

@ -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,

View File

@ -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} /> : <></>;
}
}

View File

@ -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,

View File

@ -30,21 +30,8 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -52,26 +39,15 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [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],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],
@ -125,21 +101,8 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -147,26 +110,15 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [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],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],

View File

@ -58,7 +58,7 @@ export class TabComponent extends React.Component<TabComponentProps> {
as="span"
className={className}
role="presentation"
onActivated={(e) => this.setActiveTab(index)}
onActivated={() => this.setActiveTab(index)}
aria-label={`Select tab: ${tab.title}`}
>
{tab.title}

View File

@ -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" : ""}`}

View File

@ -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 "

View File

@ -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);
});

View File

@ -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;
}

View File

@ -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);

View File

@ -2,6 +2,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil {
@ -17,7 +18,7 @@ export class DataSamplesUtil {
const databaseName = generator.getDatabaseId();
const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, this.container.databases())) {
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg);

View File

@ -1,43 +0,0 @@
jest.mock("./../Common/dataAccess/deleteDatabase");
jest.mock("./../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase";
import * as ViewModels from "./../Contracts/ViewModels";
import Explorer from "./Explorer";
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: Explorer;
beforeAll(() => {
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
});
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if only 1 database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
const database = {} as ViewModels.Database;
const database2 = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
const database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,12 @@
*/
import * as React from "react";
import { NeighborVertexBasicInfo, EditedEdges, GraphNewEdgeData, PossibleVertex } from "./GraphExplorer";
import * as GraphUtil from "./GraphUtil";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import DeleteIcon from "../../../../images/delete.svg";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import DeleteIcon from "../../../../images/delete.svg";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import { EditedEdges, GraphNewEdgeData, NeighborVertexBasicInfo, PossibleVertex } from "./GraphExplorer";
import * as GraphUtil from "./GraphUtil";
export interface EditorNeighborsComponentProps {
isSource: boolean;
@ -83,11 +83,11 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
}
private removeCurrentNeighborEdge(index: number): void {
let sources = this.props.editedNeighbors.currentNeighbors;
let id = sources[index].edgeId;
const sources = this.props.editedNeighbors.currentNeighbors;
const id = sources[index].edgeId;
sources.splice(index, 1);
let droppedIds = this.props.editedNeighbors.droppedIds;
const droppedIds = this.props.editedNeighbors.droppedIds;
droppedIds.push(id);
this.onUpdateEdges();
}
@ -215,7 +215,7 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
</td>
<td className="actionCol">
<span className="rightPaneTrashIcon rightPaneBtns">
<img src={DeleteIcon} alt="Delete" onClick={(e) => this.removeAddedEdgeToNeighbor(index)} />
<img src={DeleteIcon} alt="Delete" onClick={() => this.removeAddedEdgeToNeighbor(index)} />
</span>
</td>
</tr>

View File

@ -8,9 +8,10 @@ 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 { useTabs } from "../../../hooks/useTabs";
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 +30,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);
@ -53,8 +54,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker", container.memoryUsageInfo));
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
}
return (

View File

@ -1,17 +1,24 @@
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 { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
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;
@ -22,15 +29,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
});
it("Account is not serverless - button should be visible", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@ -45,7 +47,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@ -55,6 +57,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -66,44 +69,39 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
});
afterEach(() => {
updateUserContext({
portalEnv: "prod",
});
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Notebooks is already enabled - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
it("Account is running on one of the national clouds - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
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();
});
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
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);
@ -111,10 +109,7 @@ 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);
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);
@ -126,6 +121,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Mongo Shell button", () => {
const openMongoShellBtnLabel = "Open Mongo Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -136,34 +132,32 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isShellEnabled = ko.observable(true);
});
afterAll(() => {
updateUserContext({
apiType: "SQL",
});
useNotebook.getState().setIsShellEnabled(false);
});
beforeEach(() => {
updateUserContext({
apiType: "Mongo",
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
useNotebook.getState().setIsShellEnabled(true);
});
mockExplorer.isShellEnabled = ko.observable(true);
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Mongo Api not available - button should be hidden", () => {
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@ -173,29 +167,29 @@ describe("CommandBarComponentButtonFactory tests", () => {
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();
});
it("Notebooks is not enabled and is available - button should be hidden", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
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 enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(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);
@ -203,10 +197,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(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);
@ -214,11 +208,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isShellEnabled = ko.observable(false);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
useNotebook.getState().setIsShellEnabled(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@ -226,6 +220,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Cassandra Shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -236,9 +231,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
});
beforeEach(() => {
@ -249,8 +241,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
});
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Cassandra Api not available - button should be hidden", () => {
@ -262,7 +257,7 @@ 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();
});
@ -272,29 +267,29 @@ describe("CommandBarComponentButtonFactory tests", () => {
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();
});
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
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 enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(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);
@ -302,10 +297,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(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);
@ -316,6 +311,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;
@ -327,35 +323,28 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
});
beforeEach(() => {
mockExplorer.isNotebookEnabled = ko.observable(false);
});
afterEach(() => {
jest.resetAllMocks();
useNotebook.getState().setIsNotebookEnabled(false);
});
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeDefined();
});
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(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
);
@ -363,7 +352,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();
@ -376,10 +365,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection);
useDatabases.setState({ resourceTokenCollection: mockCollection });
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
updateUserContext({
authType: AuthType.ResourceToken,
@ -392,7 +384,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);

View File

@ -28,15 +28,21 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { OpenFullScreen } from "../../OpenFullScreen";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { useDatabases } from "../../useDatabases";
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);
@ -58,7 +64,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createDivider());
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton);
@ -71,7 +77,9 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) ||
(userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
buttons.push(createDivider());
@ -87,18 +95,18 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
}
}
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);
}
@ -108,16 +116,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);
}
}
@ -125,17 +133,20 @@ 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") {
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
if (container.isShellEnabled()) {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
@ -144,7 +155,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);
}
@ -166,7 +177,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,
@ -178,7 +192,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: !container.isHostedDataExplorerEnabled(),
disabled: !showOpenFullScreen,
className: "OpenFullScreen",
};
buttons.push(fullScreenButton);
@ -257,7 +271,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: container.isSynapseLinkUpdating(),
disabled: useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
@ -276,20 +290,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";
@ -297,23 +311,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";
@ -321,13 +336,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);
}
@ -338,13 +353,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);
}
@ -355,13 +370,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);
}
@ -408,12 +423,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,
@ -436,9 +451,9 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
commandButtonLabel: label,
hasPopup: false,
disabled: !container.isNotebooksEnabledForAccount(),
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
ariaLabel: label,
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
};
}
@ -462,12 +477,13 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
container.openSetupNotebooksPanel(title, description);
@ -488,12 +504,13 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else {
container.openSetupNotebooksPanel(title, description);
@ -534,19 +551,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 resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
const isResourceTokenCollectionNodeSelected: boolean =
resourceTokenCollection?.id() === selectedNodeState.selectedNode?.id();
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().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];

View File

@ -6,16 +6,14 @@ import {
IDropdownOption,
IDropdownStyles,
} from "@fluentui/react";
import { Observable } from "knockout";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { StyleConstants } from "../../../Common/Constants";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
import { MemoryTracker } from "./MemoryTrackerComponent";
/**
* Convert our NavbarButtonConfig to UI Fabric buttons
@ -185,12 +183,9 @@ export const createDivider = (key: string): ICommandBarItemProps => {
};
};
export const createMemoryTracker = (
key: string,
memoryUsageInfo: Observable<MemoryUsageInfo>
): ICommandBarItemProps => {
export const createMemoryTracker = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />,
onRender: () => <MemoryTracker />,
};
};

View File

@ -1,27 +1,9 @@
import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { Observable, Subscription } from "knockout";
import * as React from "react";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
import { useNotebook } from "../../Notebook/useNotebook";
interface MemoryTrackerProps {
memoryUsageInfo: Observable<MemoryUsageInfo>;
}
export class MemoryTrackerComponent extends React.Component<MemoryTrackerProps> {
private memoryUsageInfoSubscription: Subscription;
public componentDidMount(): void {
this.memoryUsageInfoSubscription = this.props.memoryUsageInfo.subscribe(() => {
this.forceUpdate();
});
}
public componentWillUnmount(): void {
this.memoryUsageInfoSubscription && this.memoryUsageInfoSubscription.dispose();
}
public render(): JSX.Element {
const memoryUsageInfo: MemoryUsageInfo = this.props.memoryUsageInfo();
export const MemoryTracker: React.FC = (): JSX.Element => {
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
if (!memoryUsageInfo) {
return (
<Stack className="memoryTrackerContainer" horizontal>
@ -44,5 +26,4 @@ export class MemoryTrackerComponent extends React.Component<MemoryTrackerProps>
/>
</Stack>
);
}
}
};

View File

@ -21,7 +21,7 @@ import {
makeStateRecord,
makeTransformsRecord,
} from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { configOption, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable";
@ -242,22 +242,27 @@ export class NotebookClientV2 {
);
// Additional configuration
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "monaco"));
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror"));
this.store.dispatch(
configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs)
);
createConfigCollection({
key: "monaco",
});
this.store.dispatch(configOption("codeMirror.lineNumbers").action(true));
const readOnlyConfigOption = configOption("codeMirror.readOnly");
const readOnlyValue = params.isReadOnly ? "nocursor" : undefined;
if (!readOnlyConfigOption) {
defineConfigOption({
label: "Show Line numbers",
key: "monaco.lineNumbers",
label: "Read-only",
key: "codeMirror.readOnly",
values: [
{ label: "Yes", value: true },
{ label: "No", value: false },
{ label: "Read-Only", value: "nocursor" },
{ label: "Not read-only", value: undefined },
],
defaultValue: true,
defaultValue: readOnlyValue,
});
} else {
this.store.dispatch(readOnlyConfigOption.action(readOnlyValue));
}
}
/**

View File

@ -34,6 +34,7 @@ import {
import { webSocket } from "rxjs/webSocket";
import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants";
import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
@ -776,7 +777,9 @@ const closeUnsupportedMimetypesEpic = (
if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.tabsManager.closeTabsByComparator(
useTabs
.getState()
.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
@ -804,7 +807,9 @@ const closeContentFailedToFetchEpic = (
if (explorer) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.tabsManager.closeTabsByComparator(
useTabs
.getState()
.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `Failed to load file: ${filepath}.`;

View File

@ -8,25 +8,26 @@ import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean;
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
private onConnectionLost: () => void,
private onMemoryUsageInfoUpdate: (update: DataModels.MemoryUsageInfo) => void
) {
if (notebookServerInfo() && notebookServerInfo().notebookServerEndpoint) {
constructor(private onConnectionLost: () => void) {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else {
const subscription = notebookServerInfo.subscribe((newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo && newServerInfo.notebookServerEndpoint) {
const unsub = useNotebook.subscribe(
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
subscription.dispose();
});
unsub();
},
(state) => state.notebookServerInfo
);
}
}
@ -36,13 +37,14 @@ export class NotebookContainerClient {
private scheduleHeartbeat(delayMs: number): void {
setTimeout(() => {
this.getMemoryUsage()
.then((memoryUsageInfo) => this.onMemoryUsageInfoUpdate(memoryUsageInfo))
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
}, delayMs);
}
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
return Promise.reject(error);
@ -98,7 +100,8 @@ export class NotebookContainerClient {
}
private async _resetWorkspace(): Promise<void> {
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
return Promise.reject(error);
@ -117,15 +120,11 @@ export class NotebookContainerClient {
}
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
let authToken: string,
notebookServerEndpoint = this.notebookServerInfo().notebookServerEndpoint,
token = this.notebookServerInfo().authToken;
if (token) {
authToken = `Token ${token}`;
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
return {
notebookServerEndpoint,
notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
authToken,
};
}

View File

@ -1,18 +1,14 @@
import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import * as DataModels from "../../Contracts/DataModels";
import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContentClient {
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
public notebookBasePath: ko.Observable<string>,
private contentProvider: IContentProvider
) {}
constructor(private contentProvider: IContentProvider) {}
/**
* This updates the item and points all the children's parent to this item
@ -271,9 +267,10 @@ export class NotebookContentClient {
}
private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return {
endpoint: this.notebookServerInfo().notebookServerEndpoint,
token: this.notebookServerInfo().authToken,
endpoint: notebookServerInfo.notebookServerEndpoint,
token: notebookServerInfo.authToken,
crossDomain: true,
};
}

View File

@ -4,13 +4,11 @@
import { ImmutableNotebook } from "@nteract/commutable";
import type { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react";
import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
@ -37,7 +35,6 @@ export type { NotebookPaneContent };
export interface NotebookManagerOptions {
container: Explorer;
notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void;
refreshNotebookList: () => void;
@ -81,17 +78,11 @@ export default class NotebookManager {
contents.JupyterContentProvider
);
this.notebookClient = new NotebookContainerClient(
this.params.container.notebookServerInfo,
() => this.params.container.initNotebooks(userContext?.databaseAccount),
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
this.notebookClient = new NotebookContainerClient(() =>
this.params.container.initNotebooks(userContext?.databaseAccount)
);
this.notebookContentClient = new NotebookContentClient(
this.params.container.notebookServerInfo,
this.params.notebookBasePath,
this.notebookContentProvider
);
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token);

View File

@ -10,18 +10,30 @@
}
.CodeMirror-scroll {
background-color: #f5f5f5;
overflow: hidden !important;
}
.CodeMirror-lines {
cursor: default;
}
.CodeMirror {
height: inherit;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #f5f5f5;
}
.nteract-cell:hover {
border: 1px solid #0078d4;
border-left: 3px solid #0078d4;
.CodeMirror-scroll {
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #ffffff;
}

View File

@ -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>

View File

@ -36,10 +36,20 @@
}
}
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
.CodeMirror-scroll {
overflow: hidden !important;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #f5f5f5;
}
.CodeMirror {
height: inherit;
}
.nteract-cell:hover {
border: 1px solid @HoverColor;
border-left: 3px solid @HoverColor;
@ -58,13 +68,16 @@
}
// White background when hovered or selected
.nteract-cell:hover, .nteract-cell-container.selected .nteract-cell {
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
.nteract-cell:hover,
.nteract-cell-container.selected .nteract-cell {
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #ffffff;
}
.CodeMirror-linenumber {
color: #015CDA;
color: #015cda;
}
.nteract-cell-outputs {
@ -101,10 +114,9 @@
}
}
// Undo tree.less
.expanded::before {
content: '';
content: "";
}
.monaco-editor .monaco-list .main {

View File

@ -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} />,
}}

View File

@ -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: {

View File

@ -0,0 +1,106 @@
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
interface NotebookState {
isNotebookEnabled: boolean;
isNotebooksEnabledForAccount: boolean;
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
isSynapseLinkUpdating: boolean;
memoryUsageInfo: DataModels.MemoryUsageInfo;
isShellEnabled: boolean;
notebookBasePath: string;
isInitializingNotebooks: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void;
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
}
export const useNotebook: UseStore<NotebookState> = create((set) => ({
isNotebookEnabled: false,
isNotebooksEnabledForAccount: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
password: undefined,
endpoints: [],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
set({ isNotebooksEnabledForAccount: false });
return;
}
const firstWriteLocation =
databaseAccount?.properties?.writeLocations &&
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
body: JSON.stringify({
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
}),
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.contentType]: "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to fetch disallowed locations");
}
const disallowedLocations: string[] = await response.json();
if (!disallowedLocations) {
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: true });
return;
}
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: false });
}
},
}));

View File

@ -31,6 +31,7 @@ import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
@ -125,6 +126,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
render(): JSX.Element {
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
{this.state.errorMessage && (
@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!this.state.errorMessage && this.isFreeTierAccount() && (
<PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, this.props.explorer.isFirstResourceCreated(), true)}
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
@ -240,9 +243,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private getDatabaseOptions(): IDropdownOption[] {
return this.props.explorer?.databases()?.map((database) => ({
return useDatabases.getState().databases?.map((database) => ({
key: database.id(),
text: database.id(),
}));
@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
const selectedDatabase = this.props.explorer
.databases()
?.find((database) => database.id() === this.state.selectedDatabaseId);
const selectedDatabase = useDatabases
.getState()
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
return !!selectedDatabase?.offer();
}

View File

@ -16,6 +16,7 @@ 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";
@ -172,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}
@ -225,7 +231,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
{!isServerlessAccount() && databaseCreateNewShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}

View File

@ -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} />);

View File

@ -4,6 +4,7 @@ import { logError } from "../../../Common/Logger";
import { Query } from "../../../Contracts/DataModels";
import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
@ -12,7 +13,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 +26,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 +34,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 = useTabs.getState().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 +48,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 (

View File

@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = `
closePanel={[Function]}
explorer={
Object {
"canSaveQueries": [Function],
"queriesClient": Object {
"getQueries": [Function],
},

View File

@ -12,6 +12,7 @@ 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";
@ -236,7 +237,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
styles={{ root: { width: 300 }, title: { fontSize: 12 }, dropdownItem: { fontSize: 12 } }}
placeholder="Choose existing keyspace id"
defaultSelectedKey={existingKeyspaceId}
options={container?.databases()?.map((keyspace) => ({
options={useDatabases.getState().databases?.map((keyspace) => ({
key: keyspace.id(),
text: keyspace.id(),
data: {
@ -253,7 +254,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
showFreeTierExceedThroughputTooltip={
isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()
}
isDatabase
isSharded
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
@ -324,7 +327,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
)}
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}

View File

@ -9,6 +9,7 @@ import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
@ -101,7 +102,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: container.getNotebookBasePath(),
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
break;

View File

@ -1,101 +1,87 @@
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);
});
});
describe("shouldRecordFeedback()", () => {
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 refreshDatabases={() => undefined} />);
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({});
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
database.isDatabaseShared = ko.computed(() => true);
wrapper.setProps({});
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),
const database = { id: ko.observable(databaseId) } as Database;
const collection = {
id: ko.observable(selectedCollectionId),
nodeKind: "Collection",
database,
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;
database.collections = ko.observableArray<Collection>([collection]);
database.isDatabaseShared = ko.computed(() => false);
beforeAll(() => {
updateUserContext({
@ -113,15 +99,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 refreshDatabases={() => undefined} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
@ -138,6 +126,7 @@ describe("Delete Collection Confirmation Pane", () => {
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
wrapper
.find("#confirmCollectionId")

View File

@ -6,20 +6,23 @@ import DeleteFeedback from "../../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
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;
refreshDatabases: () => Promise<void>;
}
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
explorer,
refreshDatabases,
}: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
@ -27,13 +30,13 @@ 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() && !useDatabases.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,11 +61,13 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
await deleteCollection(collection.databaseId, collection.id());
setIsExecuting(false);
explorer.selectedNode(collection.database);
explorer.tabsManager?.closeTabsByComparator(
useSelectedNode.getState().setSelectedNode(collection.database);
useTabs
.getState()
.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);
explorer.refreshAllDatabases();
refreshDatabases();
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);

View File

@ -2,18 +2,7 @@
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],
}
}
refreshDatabases={[Function]}
>
<RightPaneForm
formError=""
@ -373,355 +362,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-66"
>
Help us improve Azure Cosmos DB!
</span>
</Text>
<Text
block={true}
variant="small"
>
<span
className="css-66"
>
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-55"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-67"
>
<textarea
aria-invalid={false}
className="ms-TextField-field field-68"
id="deleteCollectionFeedbackInput"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onInput={[Function]}
rows={3}
value=""
/>
</div>
</div>
</div>
</TextFieldBase>
</StyledTextFieldBase>
</div>
</div>
</div>
<PanelFooterComponent
@ -2434,7 +2074,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<button
aria-label="OK"
className="ms-Button ms-Button--primary root-70"
className="ms-Button ms-Button--primary root-66"
data-is-focusable={true}
id="sidePanelOkButton"
onClick={[Function]}
@ -2446,16 +2086,16 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-71"
className="ms-Button-flexContainer flexContainer-67"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-72"
className="ms-Button-textContainer textContainer-68"
>
<span
className="ms-Button-label label-74"
id="id__6"
key="id__6"
className="ms-Button-label label-70"
id="id__3"
key="id__3"
>
OK
</span>

View File

@ -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";
@ -10,54 +10,14 @@ 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 { 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 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);
});
});
describe("submit()", () => {
const selectedDatabaseId = "testDatabse";
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
let wrapper: ReactWrapper;
beforeAll(() => {
updateUserContext({
databaseAccount: {
@ -74,23 +34,32 @@ describe("Delete Database Confirmation Pane", () => {
});
beforeEach(() => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database = {} as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
database.nodeKind = "Database";
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(database);
});
wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
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 refreshDatabases={() => undefined} />);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().addDatabases([database]);
wrapper.setProps({});
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
useDatabases.getState().clearDatabases();
});
it("Should call delete database", () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
@ -105,6 +74,7 @@ describe("Delete Database Confirmation Pane", () => {
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
@ -135,4 +105,3 @@ describe("Delete Database Confirmation Pane", () => {
wrapper.unmount();
});
});
});

View File

@ -7,30 +7,32 @@ import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection, Database } from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
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;
refreshDatabases: () => Promise<void>;
}
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
explorer,
selectedDatabase,
refreshDatabases,
}: 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 = useDatabases.getState().findSelectedDatabase();
const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
@ -50,14 +52,17 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
try {
await deleteDatabase(selectedDatabase.id());
closeSidePanel();
explorer.refreshAllDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
explorer.selectedNode(undefined);
refreshDatabases();
useTabs.getState().closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
useSelectedNode.getState().setSelectedNode(undefined);
selectedDatabase
.collections()
.forEach((collection: Collection) =>
explorer.tabsManager.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
useTabs
.getState()
.closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
)
);
TelemetryProcessor.traceSuccess(
@ -70,7 +75,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
startKey
);
if (shouldRecordFeedback()) {
if (isLastNonEmptyDatabase()) {
const deleteFeedback = new DeleteFeedback(
userContext?.databaseAccount.id,
userContext?.databaseAccount.name,
@ -100,10 +105,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}
};
const shouldRecordFeedback = (): boolean => {
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
};
const props: RightPaneFormProps = {
formError,
isExecuting: isLoading,
@ -134,7 +135,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}}
/>
</div>
{shouldRecordFeedback() && (
{isLastNonEmptyDatabase() && (
<div className="deleteDatabaseFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!

View File

@ -19,21 +19,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -41,26 +28,15 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [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],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
},
"getRepo": [Function],
"pinRepo": [Function],

View File

@ -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();
});
});

View File

@ -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();
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 => {
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);
selectedCollection.onNewQueryClick(selectedCollection, undefined, fileData);
}
const reader = new FileReader();
// 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);
};
reader.onerror = (): void => {

View File

@ -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);
});
});

View File

@ -5,11 +5,13 @@ import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Query } from "../../../Contracts/DataModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
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 +26,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 = useTabs.getState().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 +131,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

View File

@ -63,7 +63,7 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
userContext.databaseAccount.name,
"default"
);
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks
explorer.refreshExplorer();
closeSidePanel();

View File

@ -1,15 +1,14 @@
import { TextField } from "@fluentui/react";
import React, { FormEvent, FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface StringInputPanelProps {
explorer: Explorer;
closePanel: () => void;
errorMessage: string;
inProgressMessage: string;
@ -23,7 +22,6 @@ export interface StringInputPanelProps {
}
export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
explorer: container,
closePanel,
errorMessage,
inProgressMessage,
@ -55,7 +53,9 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
logConsoleInfo(`${successMessage}: ${stringInput}`);
const originalPath = notebookFile.path;
const notebookTabs = container.tabsManager.getTabs(
const notebookTabs = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);

View File

@ -9,21 +9,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@ -31,26 +18,15 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [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],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
inProgressMessage="Creating directory "

View File

@ -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();
});
});

View File

@ -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

View File

@ -1,67 +1,8 @@
// 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],
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLastCollection": [Function],
"isLastNonEmptyDatabase": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isSelectedDatabaseShared": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshAllDatabases": [Function],
"refreshNotebookList": [Function],
"resourceTokenCollection": [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],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
openNotificationConsole={[Function]}
selectedDatabase={
Object {
"collections": [Function],
"id": [Function],
}
}
refreshDatabases={[Function]}
>
<RightPaneForm
formError=""
@ -750,7 +691,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-77"
className="css-69"
>
Help us improve Azure Cosmos DB!
</span>
@ -760,7 +701,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small"
>
<span
className="css-77"
className="css-69"
>
What is the reason why you are deleting this database?
</span>
@ -1068,11 +1009,11 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-78"
className="ms-TextField-fieldGroup fieldGroup-70"
>
<textarea
aria-invalid={false}
className="ms-TextField-field field-79"
className="ms-TextField-field field-71"
id="deleteDatabaseFeedbackInput"
onBlur={[Function]}
onChange={[Function]}
@ -2798,7 +2739,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
>
<button
aria-label="OK"
className="ms-Button ms-Button--primary root-69"
className="ms-Button ms-Button--primary root-73"
data-is-focusable={true}
id="sidePanelOkButton"
onClick={[Function]}
@ -2810,16 +2751,16 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-70"
className="ms-Button-flexContainer flexContainer-74"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-71"
className="ms-Button-textContainer textContainer-75"
>
<span
className="ms-Button-label label-73"
id="id__3"
key="id__3"
className="ms-Button-label label-77"
id="id__6"
key="id__6"
>
OK
</span>

View File

@ -1,15 +1,10 @@
import * as ko from "knockout";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { SplashScreen } from "./SplashScreen";
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>;
};

View File

@ -22,6 +22,9 @@ import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLaunc
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
export interface SplashScreenItem {
iconSrc: string;
@ -59,8 +62,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() {
this.subscriptions.push(
this.container.selectedNode.subscribe(() => this.setState({})),
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
{
dispose: useNotebook.subscribe(
() => this.setState({}),
(state) => state.isNotebookEnabled
),
},
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }
);
}
@ -208,7 +216,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
});
}
if (this.container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
heroes.push({
iconSrc: NewNotebookIcon,
title: "New Notebook",
@ -227,12 +235,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 +250,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 +273,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 = useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const label = isShared ? "Settings" : "Scale & Settings";
items.push({
@ -286,7 +288,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 +310,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();
},
};
}

View File

@ -1,4 +1,4 @@
export var TableType = {
export const TableType = {
String: "String",
Boolean: "Boolean",
Binary: "Binary",
@ -9,7 +9,7 @@ export var TableType = {
Int64: "Int64",
};
export var CassandraType = {
export const CassandraType = {
Ascii: "Ascii",
Bigint: "Bigint",
Blob: "Blob",
@ -27,12 +27,12 @@ export var CassandraType = {
Tinyint: "Tinyint",
};
export var ClauseRule = {
export const ClauseRule = {
And: "And",
Or: "Or",
};
export var Operator = {
export const Operator = {
EqualTo: "==",
GreaterThan: ">",
GreaterThanOrEqualTo: ">=",
@ -42,7 +42,7 @@ export var Operator = {
Equal: "=",
};
export var ODataOperator = {
export const ODataOperator = {
EqualTo: "eq",
GreaterThan: "gt",
GreaterThanOrEqualTo: "ge",
@ -51,7 +51,7 @@ export var ODataOperator = {
NotEqualTo: "ne",
};
export var timeOptions = {
export const timeOptions = {
lastHour: "Last hour",
last24Hours: "Last 24 hours",
last7Days: "Last 7 days",
@ -62,7 +62,7 @@ export var timeOptions = {
custom: "Custom...",
};
export var htmlSelectors = {
export const htmlSelectors = {
dataTableSelector: "#storageTable",
dataTableAllRowsSelector: "#storageTable tbody tr",
dataTableHeadRowSelector: ".dataTable thead tr",
@ -84,9 +84,9 @@ export var htmlSelectors = {
selectAllDropdownSelector: "#select-all-dropdown",
};
export var defaultHeader = " ";
export const defaultHeader = " ";
export var EntityKeyNames = {
export const EntityKeyNames = {
PartitionKey: "PartitionKey",
RowKey: "RowKey",
Timestamp: "Timestamp",
@ -94,7 +94,7 @@ export var EntityKeyNames = {
Etag: "etag",
};
export var htmlAttributeNames = {
export const htmlAttributeNames = {
dataTableNameAttr: "name_attr",
dataTableContentTypeAttr: "contentType_attr",
dataTableSnapshotAttr: "snapshot_attr",
@ -103,14 +103,14 @@ export var htmlAttributeNames = {
dataTableHeaderIndex: "data-column-index",
};
export var cssColors = {
export const cssColors = {
commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */,
};
export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
export var transparentColor = "transparent";
export const clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
export const transparentColor = "transparent";
export var keyCodes = {
export const keyCodes = {
RightClick: 3,
Enter: 13,
Esc: 27,
@ -163,7 +163,7 @@ export var keyCodes = {
Dash: 189,
};
export var InputType = {
export const InputType = {
Text: "text",
// Chrome doesn't support datetime, instead, datetime-local is supported.
DateTime: "datetime-local",

View File

@ -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();
}
}

View File

@ -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> {

View File

@ -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();
}
}

View File

@ -0,0 +1,152 @@
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 DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
describe("Documents tab", () => {
describe("buildQuery", () => {
it("should generate the right select query for SQL API", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.buildQuery("")).toContain("select");
});
});
describe("showPartitionKey", () => {
const explorer = new Explorer();
const mongoExplorer = new Explorer();
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
container: explorer,
});
const collectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: explorer,
});
const collectionWithNonSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: false,
},
container: explorer,
});
const mongoCollectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: mongoExplorer,
});
it("should be false for null or undefined collection", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be false for null or undefined partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithoutPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-Mongo accounts with system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
it("should be false for Mongo accounts with system partitionKey", () => {
updateUserContext({
apiType: "Mongo",
});
const documentsTab = new DocumentsTab({
collection: mongoCollectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithNonSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
});
});

View File

@ -0,0 +1,922 @@
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import DiscardIcon from "../../../images/discard.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
import * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import DocumentId from "../Tree/DocumentId";
import { useSelectedNode } from "../useSelectedNode";
import TabsBase from "./TabsBase";
export default class DocumentsTab extends TabsBase {
public selectedDocumentId: ko.Observable<DocumentId>;
public selectedDocumentContent: ViewModels.Editable<string>;
public initialDocumentContent: ko.Observable<string>;
public documentContentsGridId: string;
public documentContentsContainerId: string;
public filterContent: ko.Observable<string>;
public appliedFilter: ko.Observable<string>;
public lastFilterContents: ko.ObservableArray<string>;
public isFilterExpanded: ko.Observable<boolean>;
public isFilterCreated: ko.Observable<boolean>;
public applyFilterButton: ViewModels.Button;
public isEditorDirty: ko.Computed<boolean>;
public editorState: ko.Observable<ViewModels.DocumentExplorerState>;
public newDocumentButton: ViewModels.Button;
public saveNewDocumentButton: ViewModels.Button;
public saveExisitingDocumentButton: ViewModels.Button;
public discardNewDocumentChangesButton: ViewModels.Button;
public discardExisitingDocumentChangesButton: ViewModels.Button;
public deleteExisitingDocumentButton: ViewModels.Button;
public displayedError: ko.Observable<string>;
public accessibleDocumentList: AccessibleVerticalList;
public dataContentsGridScrollHeight: ko.Observable<string>;
public isPreferredApiMongoDB: boolean;
public shouldShowEditor: ko.Computed<boolean>;
public splitter: Splitter;
public showPartitionKey: boolean;
public idHeader: string;
// TODO need to refactor
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public documentIds: ko.ObservableArray<DocumentId>;
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
private _resourceTokenPartitionKey: string;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
this.documentContentsGridId = `documentContentsGrid${this.tabId}`;
this.documentContentsContainerId = `documentContentsContainer${this.tabId}`;
this.editorState = ko.observable<ViewModels.DocumentExplorerState>(
ViewModels.DocumentExplorerState.noDocumentSelected
);
this.selectedDocumentId = ko.observable<DocumentId>();
this.selectedDocumentContent = editable.observable<string>("");
this.initialDocumentContent = ko.observable<string>("");
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.documentIds = options.documentIds;
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: null;
this.isFilterExpanded = ko.observable<boolean>(false);
this.isFilterCreated = ko.observable<boolean>(true);
this.filterContent = ko.observable<string>("");
this.appliedFilter = ko.observable<string>("");
this.displayedError = ko.observable<string>("");
this.lastFilterContents = ko.observableArray<string>([
'WHERE c.id = "foo"',
"ORDER BY c._ts DESC",
'WHERE c.id = "foo" ORDER BY c._ts DESC',
]);
this.dataContentsGridScrollHeight = ko.observable<string>(null);
// initialize splitter only after template has been loaded so dom elements are accessible
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady) {
const tabContainer: HTMLElement = document.getElementById("content");
const splitterBounds: SplitterBounds = {
min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth,
max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth,
};
this.splitter = new Splitter({
splitterId: "h_splitter2",
leftId: this.documentContentsContainerId,
bounds: splitterBounds,
direction: SplitterDirection.Vertical,
});
}
});
this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds());
this.accessibleDocumentList.setOnSelect(
(selectedDocument: DocumentId) => selectedDocument && selectedDocument.click()
);
this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) =>
this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId)
);
this.documentIds.subscribe((newDocuments: DocumentId[]) => {
this.accessibleDocumentList.updateItemList(newDocuments);
if (newDocuments.length > 0) {
this.dataContentsGridScrollHeight(
newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
);
} else {
this.dataContentsGridScrollHeight(
DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
);
}
});
this.isEditorDirty = ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return false;
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
return true;
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return (
this.selectedDocumentContent.getEditableOriginalValue() !==
this.selectedDocumentContent.getEditableCurrentValue()
);
default:
return false;
}
});
this.newDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.saveNewDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
};
this.discardNewDocumentChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
};
this.saveExisitingDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.discardExisitingDocumentChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.deleteExisitingDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.applyFilterButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.buildCommandBarOptions();
this.shouldShowEditor = ko.computed<boolean>(() => {
const documentHasContent: boolean =
this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0;
const isNewDocument: boolean =
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid;
return documentHasContent || isNewDocument;
});
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.showPartitionKey = this._shouldShowPartitionKey();
}
private _shouldShowPartitionKey(): boolean {
if (!this.collection) {
return false;
}
if (!this.collection.partitionKey) {
return false;
}
if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) {
return false;
}
return true;
}
public onShowFilterClick(): Q.Promise<any> {
this.isFilterCreated(true);
this.isFilterExpanded(true);
$(".filterDocExpanded").addClass("active");
$("#content").addClass("active");
$(".querydropdown").focus();
return Q();
}
public onHideFilterClick(): Q.Promise<any> {
this.isFilterExpanded(false);
$(".filterDocExpanded").removeClass("active");
$("#content").removeClass("active");
$(".queryButton").focus();
return Q();
}
public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.onHideFilterClick();
event.stopPropagation();
return false;
}
return true;
};
public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid
this.documentIds([]);
try {
// reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage();
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus();
} catch (error) {
window.alert(getErrorMessage(error));
}
}
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.refreshDocumentsGrid();
event.stopPropagation();
return false;
}
return true;
};
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
}
public onNewDocumentClick = (): Q.Promise<any> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
return Q();
}
this.selectedDocumentId(null);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
return Q();
};
public onSaveNewDocumentClick = (): Promise<any> => {
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
const document = JSON.parse(this.selectedDocumentContent());
this.isExecuting(true);
return createDocument(this.collection, document)
.then(
(savedDocument: any) => {
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.initialDocumentContent(value);
const partitionKeyValueArray = extractPartitionKey(
savedDocument,
this.partitionKey as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
let id = new DocumentId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
this.selectedDocumentId(id);
this.documentIds(ids);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
TelemetryProcessor.traceSuccess(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onRevertNewDocumentClick = (): Q.Promise<any> => {
this.initialDocumentContent("");
this.selectedDocumentContent("");
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
return Q();
};
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent());
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
selectedDocumentId.partitionKeyValue = partitionKeyValue;
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
this.isExecuting(true);
return updateDocument(this.collection, selectedDocumentId, documentContent)
.then(
(updatedDocument: any) => {
const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.initialDocumentContent(value);
this.documentIds().forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) {
documentId.id(updatedDocument.id);
}
});
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
TelemetryProcessor.traceSuccess(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onRevertExisitingDocumentClick = (): Q.Promise<any> => {
this.selectedDocumentContent.setBaseline(this.initialDocumentContent());
this.initialDocumentContent.valueHasMutated();
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
};
public onDeleteExisitingDocumentClick = async (): Promise<void> => {
const selectedDocumentId = this.selectedDocumentId();
const msg = !this.isPreferredApiMongoDB
? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?";
if (window.confirm(msg)) {
await this._deleteDocument(selectedDocumentId);
}
};
public onValidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
) {
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid);
return Q();
}
public onInvalidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
) {
this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid);
return Q();
}
if (
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits ||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid
) {
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid);
return Q();
}
return Q();
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
public async onActivate(): Promise<void> {
super.onActivate();
if (!this._documentsIterator) {
try {
this._documentsIterator = this.createIterator();
await this.loadNextPage();
} catch (error) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
}
}
private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg);
};
protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection, documentId);
}
private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
this.isExecuting(true);
return this.__deleteDocument(selectedDocumentId)
.then(
() => {
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
this.selectedDocumentContent("");
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
TelemetryProcessor.traceSuccess(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
console.error(error);
TelemetryProcessor.traceFailure(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
}
public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter);
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey;
}
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
}
public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId);
const content = await readDocument(this.collection, documentId);
this.initDocumentEditor(documentId, content);
}
public loadNextPage(): Q.Promise<any> {
this.isExecuting(true);
this.isExecutionError(false);
return this._loadNextPageInternal()
.then(
(documentsIdsResponse = []) => {
const currentDocuments = this.documentIds();
const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid);
const nextDocumentIds = documentsIdsResponse
// filter documents already loaded in observable
.filter((d: any) => {
return currentDocumentsRids.indexOf(d._rid) < 0;
})
// map raw response to view model
.map((rawDocument: any) => {
const partitionKeyValue = rawDocument._partitionKeyValue;
return new DocumentId(this, rawDocument, partitionKeyValue);
});
const merged = currentDocuments.concat(nextDocumentIds);
this.documentIds(merged);
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
logConsoleError(errorMessage);
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
)
.finally(() => this.isExecuting(false));
}
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
if (event.key === " " || event.key === "Enter") {
const focusElement = document.getElementById(this.documentContentsGridId);
this.loadNextPage();
focusElement && focusElement.focus();
event.stopPropagation();
event.preventDefault();
}
};
protected _loadNextPageInternal(): Q.Promise<DataModels.DocumentId[]> {
return Q(this._documentsIterator.fetchNext().then((response) => response.resources));
}
protected _onEditorContentChange(newContent: string) {
try {
let parsed: any = JSON.parse(newContent);
this.onValidDocumentEdit();
} catch (e) {
this.onInvalidDocumentEdit();
}
}
public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise<any> {
if (documentId) {
const content: string = this.renderObjectForEditor(documentContent, null, 4);
this.selectedDocumentContent.setBaseline(content);
this.initialDocumentContent(content);
const newState = documentId
? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits
: ViewModels.DocumentExplorerState.newDocumentValid;
this.editorState(newState);
}
return Q();
}
public buildQuery(filter: string): string {
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey);
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
if (this.newDocumentButton.visible()) {
buttons.push({
iconSrc: NewDocumentIcon,
iconAlt: label,
onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.newDocumentButton.enabled(),
});
}
if (this.saveNewDocumentButton.visible()) {
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveNewDocumentButton.enabled(),
});
}
if (this.discardNewDocumentChangesButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardNewDocumentChangesButton.enabled(),
});
}
if (this.saveExisitingDocumentButton.visible()) {
const label = "Update";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveExisitingDocumentButton.enabled(),
});
}
if (this.discardExisitingDocumentChangesButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardExisitingDocumentChangesButton.enabled(),
});
}
if (this.deleteExisitingDocumentButton.visible()) {
const label = "Delete";
buttons.push({
iconSrc: DeleteDocumentIcon,
iconAlt: label,
onCommandClick: this.onDeleteExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.deleteExisitingDocumentButton.enabled(),
});
}
if (!this.isPreferredApiMongoDB) {
buttons.push(DocumentsTab._createUploadButton(this.collection.container));
}
return buttons;
}
protected buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([
this.newDocumentButton.visible,
this.newDocumentButton.enabled,
this.saveNewDocumentButton.visible,
this.saveNewDocumentButton.enabled,
this.discardNewDocumentChangesButton.visible,
this.discardNewDocumentChangesButton.enabled,
this.saveExisitingDocumentButton.visible,
this.saveExisitingDocumentButton.enabled,
this.discardExisitingDocumentChangesButton.visible,
this.discardExisitingDocumentChangesButton.enabled,
this.deleteExisitingDocumentButton.visible,
this.deleteExisitingDocumentButton.enabled,
])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
private _getPartitionKeyPropertyHeader(): string {
return (
(this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0]) ||
null
);
}
public static _createUploadButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload Item";
return {
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
};
}
}

View File

@ -41,6 +41,7 @@ import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId";
import { useSelectedNode } from "../useSelectedNode";
import DocumentsTab from "./DocumentsTab";
import {
formatDocumentContent,
@ -450,13 +451,13 @@ export default class DocumentsTabContent extends React.Component<DocumentsTab, I
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = collection.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && collection.container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: collection.container.isDatabaseNodeOrNoneSelected(),
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
});
}
return buttons;

View File

@ -1,29 +0,0 @@
import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q";
import MongoUtility from "../../Common/MongoUtility";
import QueryTab from "./QueryTab";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { queryIterator } from "../../Common/MongoProxyClient";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
export default class MongoQueryTab extends QueryTab {
public collection: ViewModels.Collection;
constructor(options: ViewModels.QueryTabOptions) {
options.queryText = ""; // override sql query editor content for now so we only display mongo related help items
super(options);
this.isPreferredApiMongoDB = true;
this.monacoSettings = new ViewModels.MonacoEditorSettings("plaintext", false);
}
/** Renders a Javascript object to be displayed inside Monaco Editor */
protected renderObjectForEditor(value: any, replacer: any, space: string | number): string {
return MongoUtility.tojson(value, null, false);
}
protected _initIterator(): Q.Promise<MinimalQueryIterator> {
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
this._iterator = queryIterator(this.collection.databaseId, this.collection, this.sqlStatementToExecute());
return Q(this._iterator);
}
}

View File

@ -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} />;
}
}

View File

@ -1,15 +0,0 @@
<iframe
name="explorer"
class="iframe"
style="width: 100%; height: 100%; border: 0px; padding: 0px; margin: 0px; overflow: hidden"
data-bind="
attr: {
src: url,
id: tabId
},
event:{
load: setContentFocus(event)
}"
title="Mongo Shell"
role="tabpanel"
></iframe>

View File

@ -0,0 +1,40 @@
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { TabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
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 {
useTabs.getState().activateTab(this);
this.iMongoShellTabAccessor.onTabClickEvent();
}
}

View File

@ -1,28 +1,71 @@
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>(() => {
this.state = {
url: this.getURL(),
};
props.onMongoShellTabAccessor({
onTabClickEvent: this.onTabClick.bind(this),
});
window.addEventListener("message", this.handleMessage.bind(this), false);
}
public getURL(): string {
const { databaseAccount: account } = userContext;
const resourceId = account?.id;
const accountName = account?.name;
@ -36,32 +79,23 @@ export default class MongoShellTab extends TabsBase {
}
return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
});
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);
// }
}
//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";
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>
);
}
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";
}

View File

@ -6,6 +6,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import Explorer from "../Explorer";
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { useNotebook } from "../Notebook/useNotebook";
import TabsBase from "./TabsBase";
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
@ -28,7 +29,7 @@ export default class NotebookTabBase extends TabsBase {
if (!NotebookTabBase.clientManager) {
NotebookTabBase.clientManager = new NotebookClientV2({
connectionInfo: this.container.notebookServerInfo(),
connectionInfo: useNotebook.getState().notebookServerInfo,
databaseAccountName: userContext?.databaseAccount?.name,
defaultExperience: userContext.apiType,
contentProvider: this.container.notebookManager?.notebookContentProvider,

View File

@ -23,6 +23,7 @@ import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { useNotebook } from "../Notebook/useNotebook";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
export interface NotebookTabOptions extends NotebookTabBaseOptions {
@ -39,10 +40,13 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.container = options.container;
this.notebookPath = ko.observable(options.notebookContentItem.path);
this.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received."));
useNotebook.subscribe(
() => logConsoleInfo("New notebook server info received."),
(state) => state.notebookServerInfo
);
this.notebookComponentAdapter = new NotebookComponentAdapter({
contentItem: options.notebookContentItem,
notebooksBasePath: this.container.getNotebookBasePath(),
notebooksBasePath: useNotebook.getState().notebookBasePath,
notebookClient: NotebookTabBase.clientManager,
onUpdateKernelInfo: this.onKernelUpdate,
});
@ -359,8 +363,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
};
private async configureServiceEndpoints(kernelName: string) {
const notebookConnectionInfo = this.container && this.container.notebookServerInfo();
const sparkClusterConnectionInfo = this.container && this.container.sparkClusterConnectionInfo();
const notebookConnectionInfo = useNotebook.getState().notebookServerInfo;
const sparkClusterConnectionInfo = useNotebook.getState().sparkClusterConnectionInfo;
await NotebookConfigurationUtils.configureServiceEndpoints(
this.notebookPath(),
notebookConnectionInfo,

View File

@ -1,335 +0,0 @@
<div class="tab-pane" data-bind="attr:{id: tabId}" role="tabpanel">
<div class="tabPaneContentContainer">
<div class="mongoQueryHelper" data-bind="visible: isPreferredApiMongoDB && sqlQueryEditorContent().length === 0">
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the
documents.
</div>
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: maybeSubQuery">
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/info_color.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery"
>Please see Cosmos sub query documentation for further information</a
>
</span>
</div>
</div>
<div class="queryEditorWithSplitter" data-bind="attr: { id: queryEditorId }">
<editor
class="queryEditor"
data-bind="css: { mongoQueryEditor: isPreferredApiMongoDB }"
params="{
content: initialEditorContent,
contentType: monacoSettings.language,
isReadOnly: monacoSettings.readOnly,
lineNumbers: 'on',
ariaLabel: 'Editing Query',
updatedContent: sqlQueryEditorContent,
selectedContent: selectedContent
}"
></editor>
<!-- Splitter - Start -->
<div class="splitter ui-resizable-handle ui-resizable-s" data-bind="attr: { id: splitterId }">
<img class="queryEditorHorizontalSplitter" src="/HorizontalSplitter.svg" alt="Splitter" />
</div>
</div>
<!-- Splitter - End -->
<!-- Script for results metadata that is common to all APIs -->
<script type="text/html" id="result-metadata-template">
<span>
<span data-bind="text: showingDocumentsDisplayText"></span>
</span>
<span class="queryResultDivider" data-bind="visible: fetchNextPageButton.enabled"> | </span>
<span class="queryResultNextEnable" data-bind="visible: fetchNextPageButton.enabled">
<a data-bind="click: onFetchNextPageClick">
<span>Load more</span>
<img class="queryResultnextImg" src="/Query-Editor-Next.svg" alt="Fetch next page" />
</a>
</span>
</script>
<!-- Query Errors Tab - Start-->
<div class="active queryErrorsHeaderContainer" data-bind="visible: !!error()">
<span class="queryErrors" data-toggle="tab" data-bind="attr: { href: '#queryerrors' + tabId }">Errors</span>
</div>
<!-- Query Errors Tab - End -->
<!-- Query Results & Errors Content Container - Start-->
<div class="queryResultErrorContentContainer">
<div
class="queryEditorWatermark"
data-bind="visible: allResultsMetadata().length === 0 && !error() && !queryResults() && !isExecuting()"
>
<p><img src="/RunQuery.png" alt="Execute Query Watermark" /></p>
<p class="queryEditorWatermarkText">Execute a query to see the results</p>
</div>
<div
class="queryResultsErrorsContent"
data-bind="visible: allResultsMetadata().length > 0 || !!error() || queryResults()"
>
<div class="togglesWithMetadata" data-bind="visible: !error()">
<div
class="toggles"
aria-label="Successful execution"
id="execute-query-toggles"
data-bind="event: { keydown: onToggleKeyDown }"
tabindex="0"
>
<div class="tab">
<input type="radio" class="radio" value="result" />
<span
class="toggleSwitch"
role="button"
tabindex="0"
data-bind="click: toggleResult, css:{ selectedToggle: isResultToggled(), unselectedToggle: !isResultToggled() }"
aria-label="Results"
>Results</span
>
</div>
<div class="tab">
<input type="radio" class="radio" value="logs" />
<span
class="toggleSwitch"
role="button"
tabindex="0"
data-bind="click: toggleMetrics, css:{ selectedToggle: isMetricsToggled(), unselectedToggle: !isMetricsToggled() }"
aria-label="Query stats"
>Query Stats</span
>
</div>
</div>
<div
class="result-metadata"
data-bind="template: { name: 'result-metadata-template' }, visible: isResultToggled()"
></div>
</div>
<json-editor
params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }"
data-bind="visible: queryResults() && queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
>
</json-editor>
<div
class="queryMetricsSummaryContainer"
data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && !error()"
>
<table class="queryMetricsSummary">
<caption>
Query Statistics
</caption>
<thead class="queryMetricsSummaryHead">
<tr class="queryMetricsSummaryHeader queryMetricsSummaryTuple">
<th title="METRIC" scope="col">METRIC</th>
<th title="VALUE" scope="col">VALUE</th>
</tr>
</thead>
<tbody class="queryMetricsSummaryBody" data-bind="with: aggregatedQueryMetrics">
<tr class="queryMetricsSummaryTuple">
<td title="Request Charge">Request Charge</td>
<td>
<span
data-bind="text: $parent.requestChargeDisplayText, attr: { title: $parent.requestChargeDisplayText }"
></span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple">
<td title="Showing Results">Showing Results</td>
<td>
<span
data-bind="text: $parent.showingDocumentsDisplayText, attr: { title: $parent.showingDocumentsDisplayText }"
></span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Retrieved document count">Retrieved document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of retrieved documents</span>
</span>
</td>
<td><span data-bind="text: retrievedDocumentCount, attr: { title: retrievedDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Retrieved document size">Retrieved document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of retrieved documents in bytes</span>
</span>
</td>
<td>
<span data-bind="text: retrievedDocumentSize, attr: { title: retrievedDocumentSize }"></span>
<span>bytes</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Output document count">Output document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Number of output documents</span>
</span>
</td>
<td><span data-bind="text: outputDocumentCount, attr: { title: outputDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Output document size">Output document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of output documents in bytes</span>
</span>
</td>
<td>
<span data-bind="text: outputDocumentSize, attr: { title: outputDocumentSize }"></span>
<span>bytes</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Index hit document count">Index hit document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of documents matched by the filter</span>
</span>
</td>
<td><span data-bind="text: indexHitDocumentCount, attr: { title: indexHitDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Index lookup time">Index lookup time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in physical index layer</span>
</span>
</td>
<td>
<span data-bind="text: indexLookupTime, attr: { title: indexLookupTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Document load time">Document load time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in loading documents</span>
</span>
</td>
<td>
<span data-bind="text: documentLoadTime, attr: { title: documentLoadTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Query engine execution time">Query engine execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText queryEngineExeTimeInfo"
>Time spent by the query engine to execute the query expression (excludes other execution times
like load documents or write results)</span
>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.queryEngineExecutionTime, attr: { title: runtimeExecutionTimes.queryEngineExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="System function execution time">System function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing system (built-in) functions</span>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.systemFunctionExecutionTime, attr: { title: runtimeExecutionTimes.systemFunctionExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="User defined function execution time">User defined function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing user-defined functions</span>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.userDefinedFunctionExecutionTime, attr: { title: runtimeExecutionTimes.userDefinedFunctionExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Document write time">Document write time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent to write query result set to response buffer</span>
</span>
</td>
<td>
<span data-bind="text: documentWriteTime, attr: { title: documentWriteTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.roundTrips() != null">
<td title="Round Trips">Round Trips</td>
<td><span data-bind="text: $parent.roundTrips, attr: { title: $parent.roundTrips }"></span></td>
</tr>
<!-- TODO: Report activity id for mongo queries -->
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.activityId() != null">
<td title="Activity id">Activity id</td>
<td></td>
<td><span data-bind="text: $parent.activityId, attr: { title: $parent.activityId }"></span></td>
</tr>
</tbody>
</table>
<div class="downloadMetricsLinkContainer" data-bind="visible: $parent.isQueryMetricsEnabled">
<a
id="downloadMetricsLink"
role="button"
tabindex="0"
data-bind="event: { click: onDownloadQueryMetricsCsvClick, keypress: onDownloadQueryMetricsCsvKeyPress }"
>
<img class="downloadCsvImg" src="/DownloadQuery.svg" alt="download query metrics csv" />
<span>Per-partition query metrics (CSV)</span>
</a>
</div>
</div>
<!-- Query Errors Content - Start-->
<div
class="tab-pane active"
data-bind="
id: {
href: 'queryerrors' + tabId
},
visible: !!error()"
>
<div class="errorContent">
<span class="errorMessage" data-bind="text: error"></span>
<span class="errorDetailsLink">
<a
data-bind="click: $parent.onErrorDetailsClick, event: { keypress: $parent.onErrorDetailsKeyPress }"
id="error-display"
tabindex="0"
aria-label="Error details link"
>More details</a
>
</span>
</div>
</div>
<!-- Query Errors Content - End-->
</div>
</div>
<!-- Results & Errors Content Container - End-->
</div>
</div>

View File

@ -1,311 +0,0 @@
@import "../../../less/Common/Constants";
@import "../../../less/Common/TabCommon";
@MongoQueryEditorHeight: 50px;
@ResultsTextFontWeight: 600;
@ToggleHeight: 30px;
@ToggleWidth: 250px;
@QueryEngineExeInfo: 305px;
.tab-pane {
.tabContentContainer();
.tabPaneContentContainer {
.tabContentContainer();
.mongoQueryHelper {
margin:@DefaultSpace 0px 0px 44px;
position: absolute;
top: 115px; //this is to avoid the jump of query editor
}
.queryEditorWithSplitter {
.flex-display();
.flex-direction();
flex-shrink: 0;
height: 100%;
width: 100%;
margin-left: @SmallSpace;
.queryEditor {
.flex-display();
height: 100%;
width: 100%;
margin-top: @SmallSpace;
.jsonEditor {
border: none;
margin-top: @SmallSpace;
}
}
.queryEditor.mongoQueryEditor {
margin-top: 32px;
overflow: hidden;
}
.queryEditorHorizontalSplitter {
margin: auto;
display: block;
}
}
.queryErrorsHeaderContainer {
padding: 24px @LargeSpace 0px @MediumSpace;
.queryErrors {
font-size: @mediumFontSize;
list-style-type: none;
color: @BaseDark;
font-weight: bold;
margin-left: 24px;
}
}
.queryResultErrorContentContainer {
.flex-display();
.flex-direction();
font-size: @DefaultFontSize;
padding: @DefaultSpace;
height: 100%;
width: 100%;
overflow: hidden;
.queryEditorWatermark {
text-align: center;
margin: auto;
height: 25vh; // this is to align the water mark in center of the layout.
p {
margin-bottom: @LargeSpace;
color: @BaseHigh;
}
.queryEditorWatermarkText {
color: @BaseHigh;
font-size: @DefaultFontSize;
font-family: @DataExplorerFont;
}
}
.queryResultsErrorsContent {
height: 100%;
margin-left: @MediumSpace;
.flex-display();
.flex-direction();
.togglesWithMetadata {
margin-top: @MediumSpace;
.toggles {
height: @ToggleHeight;
width: @ToggleWidth;
margin-left: @MediumSpace;
&:focus {
.focus();
}
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
}
.result-metadata {
padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace;
.queryResultDivider {
margin-left: @SmallSpace;
margin-right: @SmallSpace;
}
.queryResultNextEnable {
color: @AccentMediumHigh;
font-size: @mediumFontSize;
cursor: pointer;
img {
height: @ImgHeight;
width: @ImgWidth;
margin-left: @SmallSpace;
}
}
.queryResultNextDisable {
color: @BaseMediumHigh;
opacity: 0.5;
font-size: @mediumFontSize;
img {
height: @ImgHeight;
width: @ImgWidth;
margin-left: @SmallSpace;
}
}
}
.tab-pane.active {
height: 100%;
width: 100%;
}
.errorContent {
.flex-display();
width: 60%;
white-space: nowrap;
font-size: @mediumFontSize;
padding: 0px @MediumSpace 0px @MediumSpace;
.errorMessage {
padding: @SmallSpace;
overflow: hidden;
text-overflow: ellipsis;
}
}
.errorDetailsLink {
cursor: pointer;
padding: @SmallSpace;
}
.queryMetricsSummaryContainer {
.flex-display();
.flex-direction();
overflow: hidden;
.queryMetricsSummary {
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
table-layout: fixed;
display: block;
height: auto;
overflow-y: auto;
overflow-x: hidden;
caption {
width: 100px;
}
.queryMetricsSummaryHead {
.flex-display();
}
.queryMetricsSummaryHeader.queryMetricsSummaryTuple {
font-size: 10px;
}
.queryMetricsSummaryBody {
.flex-display();
.flex-direction();
}
.queryMetricsSummaryTuple {
border-bottom: 1px solid @BaseMedium;
height: 32px;
font-size: 12px;
width: 100%;
.flex-display();
th, td {
padding: @DefaultSpace;
&:nth-child(1) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 0 50%;
}
&:nth-child(3) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 0 50%;
}
.queryMetricInfoTooltip {
.infoTooltip();
&:hover .queryMetricTooltipText {
.tooltipVisible();
}
&:focus .queryMetricTooltipText {
.tooltipVisible();
}
.queryMetricTooltipText {
top: -50px;
width: auto;
white-space: nowrap;
left: 6px;
visibility: hidden;
background-color: @BaseHigh;
color: @BaseLight;
position: absolute;
z-index: 1;
padding: @MediumSpace;
&::after {
border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px;
bottom: -14px;
.tooltipTextAfter();
}
}
.queryEngineExeTimeInfo {
width: @QueryEngineExeInfo;
top: -85px;
white-space: pre-wrap;
}
}
}
}
}
.downloadMetricsLinkContainer {
margin: 24px 0px 24px @MediumSpace;
#downloadMetricsLink {
color: @BaseHigh;
padding: @DefaultSpace;
font-size: @mediumFontSize;
border: @ButtonBorderWidth solid @BaseLight;
cursor: pointer;
&:hover {
.hover();
}
&:active {
border: @ButtonBorderWidth dashed @AccentMedium;
.active();
}
}
}
}
json-editor {
.flex-display();
.flex-direction();
overflow: hidden;
height: 100%;
width: 100%;
padding: @SmallSpace 0px @SmallSpace @MediumSpace;
}
}
}
}
}

View File

@ -1,96 +0,0 @@
import * as ko from "knockout";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import QueryTab from "./QueryTab";
describe("Query Tab", () => {
function getNewQueryTabForContainer(container: Explorer): QueryTab {
const database = {
container: container,
id: ko.observable<string>("test"),
isDatabaseShared: () => false,
} as ViewModels.Database;
const collection = {
container: container,
databaseId: "test",
id: ko.observable<string>("test"),
} as ViewModels.Collection;
return new QueryTab({
tabKind: ViewModels.CollectionTabKind.Query,
collection: collection,
database: database,
title: "",
tabPath: "",
hashLocation: "",
});
}
describe("shouldSetSystemPartitionKeyContainerPartitionKeyValueUndefined", () => {
const collection = {
id: ko.observable<string>("withoutsystempk"),
partitionKey: {
systemKey: true,
},
} as ViewModels.Collection;
it("no container with system pk, should not set partition key option", () => {
const iteratorOptions = QueryTab.getIteratorOptions(collection);
expect(iteratorOptions.initialHeaders).toBeUndefined();
});
});
describe("isQueryMetricsEnabled()", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("should be true for accounts using SQL API", () => {
updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(true);
});
it("should be false for accounts using other APIs", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(false);
});
});
describe("Save Queries command button", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("should be visible when using a supported API", () => {
updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(true);
});
it("should not be visible when using an unsupported API", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(false);
});
});
});

View File

@ -1,594 +0,0 @@
import * as ko from "knockout";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import template from "./QueryTab.html";
import TabsBase from "./TabsBase";
enum ToggleState {
Result,
QueryMetrics,
}
export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate {
public readonly html = template;
public queryEditorId: string;
public executeQueryButton: ViewModels.Button;
public fetchNextPageButton: ViewModels.Button;
public saveQueryButton: ViewModels.Button;
public initialEditorContent: ko.Observable<string>;
public maybeSubQuery: ko.Computed<boolean>;
public sqlQueryEditorContent: ko.Observable<string>;
public selectedContent: ko.Observable<string>;
public sqlStatementToExecute: ko.Observable<string>;
public queryResults: ko.Observable<string>;
public error: ko.Observable<string>;
public statusMessge: ko.Observable<string>;
public statusIcon: ko.Observable<string>;
public allResultsMetadata: ko.ObservableArray<ViewModels.QueryResultsMetadata>;
public showingDocumentsDisplayText: ko.Observable<string>;
public requestChargeDisplayText: ko.Observable<string>;
public isTemplateReady: ko.Observable<boolean>;
public splitterId: string;
public splitter: Splitter;
public isPreferredApiMongoDB: boolean;
public queryMetrics: ko.Observable<Map<string, DataModels.QueryMetrics>>;
public aggregatedQueryMetrics: ko.Observable<DataModels.QueryMetrics>;
public activityId: ko.Observable<string>;
public roundTrips: ko.Observable<number>;
public toggleState: ko.Observable<ToggleState>;
public isQueryMetricsEnabled: ko.Computed<boolean>;
protected monacoSettings: ViewModels.MonacoEditorSettings;
private _executeQueryButtonTitle: ko.Observable<string>;
protected _iterator: MinimalQueryIterator;
private _isSaveQueriesEnabled: ko.Computed<boolean>;
private _resourceTokenPartitionKey: string;
_partitionKey: DataModels.PartitionKey;
constructor(options: ViewModels.QueryTabOptions) {
super(options);
this.queryEditorId = `queryeditor${this.tabId}`;
this.showingDocumentsDisplayText = ko.observable<string>();
this.requestChargeDisplayText = ko.observable<string>();
const defaultQueryText = options.queryText != void 0 ? options.queryText : "SELECT * FROM c";
this.initialEditorContent = ko.observable<string>(defaultQueryText);
this.sqlQueryEditorContent = ko.observable<string>(defaultQueryText);
this._executeQueryButtonTitle = ko.observable<string>("Execute Query");
this.selectedContent = ko.observable<string>();
this.selectedContent.subscribe((selectedContent: string) => {
if (!selectedContent.trim()) {
this._executeQueryButtonTitle("Execute Query");
} else {
this._executeQueryButtonTitle("Execute Selection");
}
});
this.sqlStatementToExecute = ko.observable<string>("");
this.queryResults = ko.observable<string>("");
this.statusMessge = ko.observable<string>();
this.statusIcon = ko.observable<string>();
this.allResultsMetadata = ko.observableArray<ViewModels.QueryResultsMetadata>([]);
this.error = ko.observable<string>();
this._partitionKey = options.partitionKey;
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.splitterId = this.tabId + "_splitter";
this.isPreferredApiMongoDB = false;
this.aggregatedQueryMetrics = ko.observable<DataModels.QueryMetrics>();
this._resetAggregateQueryMetrics();
this.queryMetrics = ko.observable<Map<string, DataModels.QueryMetrics>>(new Map());
this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)));
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
return userContext.apiType === "SQL" || false;
});
this.activityId = ko.observable<string>();
this.roundTrips = ko.observable<number>();
this.toggleState = ko.observable<ToggleState>(ToggleState.Result);
this.monacoSettings = new ViewModels.MonacoEditorSettings("sql", false);
this.executeQueryButton = {
enabled: ko.computed<boolean>(() => {
return !!this.sqlQueryEditorContent() && this.sqlQueryEditorContent().length > 0;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this._isSaveQueriesEnabled = ko.computed<boolean>(() => {
const container = this.collection && this.collection.container;
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
});
this.maybeSubQuery = ko.computed<boolean>(function () {
const sql = this.sqlQueryEditorContent();
return sql && /.*\(.*SELECT.*\)/i.test(sql);
}, this);
this.saveQueryButton = {
enabled: this._isSaveQueriesEnabled,
visible: this._isSaveQueriesEnabled,
};
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady) {
const splitterBounds: SplitterBounds = {
min: Constants.Queries.QueryEditorMinHeightRatio * window.innerHeight,
max: $("#" + this.tabId).height() - Constants.Queries.QueryEditorMaxHeightRatio * window.innerHeight,
};
this.splitter = new Splitter({
splitterId: this.splitterId,
leftId: this.queryEditorId,
bounds: splitterBounds,
direction: SplitterDirection.Horizontal,
});
}
});
this.fetchNextPageButton = {
enabled: ko.computed<boolean>(() => {
const allResultsMetadata = this.allResultsMetadata() || [];
const numberOfResultsMetadata = allResultsMetadata.length;
if (numberOfResultsMetadata === 0) {
return false;
}
if (allResultsMetadata[numberOfResultsMetadata - 1].hasMoreResults) {
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this._buildCommandBarOptions();
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
}
public onExecuteQueryClick = async (): Promise<void> => {
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
this.sqlStatementToExecute(sqlStatement);
this.allResultsMetadata([]);
this.queryResults("");
this._iterator = undefined;
await this._executeQueryDocumentsPage(0);
};
public onSaveQueryClick = (): void => {
this.collection && this.collection.container && this.collection.container.openSaveQueryPanel();
};
public onSavedQueriesClick = (): void => {
this.collection && this.collection.container && this.collection.container.openBrowseQueriesPanel();
};
public async onFetchNextPageClick(): Promise<void> {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
}
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
useNotificationConsole.getState().expandConsole();
return false;
};
public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.onErrorDetailsClick(src, null);
return false;
}
return true;
};
public toggleResult(): void {
this.toggleState(ToggleState.Result);
this.queryResults.valueHasMutated(); // needed to refresh the json-editor component
}
public toggleMetrics(): void {
this.toggleState(ToggleState.QueryMetrics);
}
public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.LeftArrow) {
this.toggleResult();
event.stopPropagation();
return false;
} else if (event.keyCode === Constants.KeyCodes.RightArrow) {
this.toggleMetrics();
event.stopPropagation();
return false;
}
return true;
};
public togglesOnFocus(): void {
const focusElement = document.getElementById("execute-query-toggles");
focusElement && focusElement.focus();
}
public isResultToggled(): boolean {
return this.toggleState() === ToggleState.Result;
}
public isMetricsToggled(): boolean {
return this.toggleState() === ToggleState.QueryMetrics;
}
public onDownloadQueryMetricsCsvClick = (source: any, event: MouseEvent): boolean => {
this._downloadQueryMetricsCsvData();
return false;
};
public onDownloadQueryMetricsCsvKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || Constants.KeyCodes.Enter) {
this._downloadQueryMetricsCsvData();
return false;
}
return true;
};
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
this.error("");
this.roundTrips(undefined);
if (this._iterator === undefined) {
this._initIterator();
}
await this._queryDocumentsPage(firstItemIndex);
}
// TODO: Position and enable spinner when request is in progress
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
this.isExecutionError(false);
this._resetAggregateQueryMetrics();
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
const queryDocuments = async (firstItemIndex: number) =>
await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
this.isExecuting(true);
try {
const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent(
firstItemIndex,
queryDocuments
);
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex,
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
// we let users query for the next page because the SDK sometimes specifies there are more elements
// even though there aren't any so we should not update the prior query results.
return;
}
const documents: any[] = queryResults.documents;
const results = this.renderObjectForEditor(documents, null, 4);
const resultsDisplay: string =
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
this.showingDocumentsDisplayText(resultsDisplay);
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
this.queryResults(results);
TelemetryProcessor.traceSuccess(
Action.ExecuteQuery,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
} catch (error) {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
this.error(errorMessage);
TelemetryProcessor.traceFailure(
Action.ExecuteQuery,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
document.getElementById("error-display").focus();
} finally {
this.isExecuting(false);
this.togglesOnFocus();
}
}
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
if (!metricsMap) {
return;
}
Object.keys(metricsMap).forEach((key: string) => {
this.queryMetrics().set(key, metricsMap[key]);
});
this.queryMetrics.valueHasMutated();
}
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
if (!metricsMap) {
return null;
}
const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics();
metricsMap.forEach((queryMetrics) => {
if (queryMetrics) {
aggregatedMetrics.documentLoadTime =
queryMetrics.documentLoadTime &&
this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.documentLoadTime);
aggregatedMetrics.documentWriteTime =
queryMetrics.documentWriteTime &&
this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.documentWriteTime);
aggregatedMetrics.indexHitDocumentCount =
queryMetrics.indexHitDocumentCount &&
this._normalize(queryMetrics.indexHitDocumentCount) +
this._normalize(aggregatedMetrics.indexHitDocumentCount);
aggregatedMetrics.outputDocumentCount =
queryMetrics.outputDocumentCount &&
this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount);
aggregatedMetrics.outputDocumentSize =
queryMetrics.outputDocumentSize &&
this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize);
aggregatedMetrics.indexLookupTime =
queryMetrics.indexLookupTime &&
this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.indexLookupTime);
aggregatedMetrics.retrievedDocumentCount =
queryMetrics.retrievedDocumentCount &&
this._normalize(queryMetrics.retrievedDocumentCount) +
this._normalize(aggregatedMetrics.retrievedDocumentCount);
aggregatedMetrics.retrievedDocumentSize =
queryMetrics.retrievedDocumentSize &&
this._normalize(queryMetrics.retrievedDocumentSize) +
this._normalize(aggregatedMetrics.retrievedDocumentSize);
aggregatedMetrics.vmExecutionTime =
queryMetrics.vmExecutionTime &&
this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.vmExecutionTime);
aggregatedMetrics.totalQueryExecutionTime =
queryMetrics.totalQueryExecutionTime &&
this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.totalQueryExecutionTime);
aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime =
aggregatedMetrics.runtimeExecutionTimes &&
this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime);
aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime =
aggregatedMetrics.runtimeExecutionTimes &&
this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime);
aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime =
aggregatedMetrics.runtimeExecutionTimes &&
this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) +
this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime);
}
});
return aggregatedMetrics;
}
public _downloadQueryMetricsCsvData(): void {
const csvData: string = this._generateQueryMetricsCsvData();
if (!csvData) {
return;
}
if (navigator.msSaveBlob) {
// for IE and Edge
navigator.msSaveBlob(
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
"PerPartitionQueryMetrics.csv"
);
} else {
const downloadLink: HTMLAnchorElement = document.createElement("a");
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
downloadLink.target = "_self";
downloadLink.download = "QueryMetricsPerPartition.csv";
// for some reason, FF displays the download prompt only when
// the link is added to the dom so we add and remove it
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
}
}
protected _initIterator(): void {
const options: any = QueryTab.getIteratorOptions(this.collection);
if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey;
}
this._iterator = queryDocuments(
this.collection.databaseId,
this.collection.id(),
this.sqlStatementToExecute(),
options
);
}
public static getIteratorOptions(container: ViewModels.CollectionBase): any {
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
return options;
}
private _normalize(value: number): number {
if (!value) {
return 0;
}
return value;
}
private _resetAggregateQueryMetrics(): void {
this.aggregatedQueryMetrics({
clientSideMetrics: {},
documentLoadTime: undefined,
documentWriteTime: undefined,
indexHitDocumentCount: undefined,
outputDocumentCount: undefined,
outputDocumentSize: undefined,
indexLookupTime: undefined,
retrievedDocumentCount: undefined,
retrievedDocumentSize: undefined,
vmExecutionTime: undefined,
queryPreparationTimes: undefined,
runtimeExecutionTimes: {
queryEngineExecutionTime: undefined,
systemFunctionExecutionTime: undefined,
userDefinedFunctionExecutionTime: undefined,
},
totalQueryExecutionTime: undefined,
});
}
private _generateQueryMetricsCsvData(): string {
if (!this.queryMetrics()) {
return null;
}
const queryMetrics = this.queryMetrics();
let csvData: string = "";
const columnHeaders: string =
[
"Partition key range id",
"Retrieved document count",
"Retrieved document size (in bytes)",
"Output document count",
"Output document size (in bytes)",
"Index hit document count",
"Index lookup time (ms)",
"Document load time (ms)",
"Query engine execution time (ms)",
"System function execution time (ms)",
"User defined function execution time (ms)",
"Document write time (ms)",
].join(",") + "\n";
csvData = csvData + columnHeaders;
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
const partitionKeyRangeData: string =
[
partitionKeyRangeId,
queryMetric.retrievedDocumentCount,
queryMetric.retrievedDocumentSize,
queryMetric.outputDocumentCount,
queryMetric.outputDocumentSize,
queryMetric.indexHitDocumentCount,
queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(),
queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(),
queryMetric.runtimeExecutionTimes &&
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime &&
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(),
queryMetric.runtimeExecutionTimes &&
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime &&
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(),
queryMetric.runtimeExecutionTimes &&
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime &&
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(),
queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(),
].join(",") + "\n";
csvData = csvData + partitionKeyRangeData;
});
return csvData;
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (this.executeQueryButton.visible()) {
const label = this._executeQueryButtonTitle();
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: this.onExecuteQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.executeQueryButton.enabled(),
});
}
if (this.saveQueryButton.visible()) {
const label = "Save Query";
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveQueryButton.enabled(),
});
}
return buttons;
}
private _buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([this.executeQueryButton.visible, this.executeQueryButton.enabled, this._executeQueryButtonTitle])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
}

View File

@ -0,0 +1,58 @@
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
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 {
useTabs.getState().activateTab(this);
this.iTabAccessor.onTabClickEvent();
}
public onCloseTabButtonClick(): void {
useTabs.getState().closeTab(this);
if (this.iTabAccessor) {
this.iTabAccessor.onCloseClickEvent(true);
}
}
public getContainer(): Explorer {
return this.props.container;
}
}

View File

@ -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

View File

@ -1,5 +1,5 @@
import * as ko from "knockout";
import Q from "q";
import React from "react";
import AddEntityIcon from "../../../images/AddEntity.svg";
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
import EditEntityIcon from "../../../images/Edit-entity.svg";
@ -7,13 +7,16 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
import QueryTextIcon from "../../../images/Query-Text.svg";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "../Panes/Tables/EditTableEntityPanel";
import TableCommands from "../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
import { TableDataClient } from "../Tables/TableDataClient";
import { CassandraAPIDataClient, TableDataClient } from "../Tables/TableDataClient";
import template from "./QueryTablesTab.html";
import TabsBase from "./TabsBase";
@ -130,34 +133,36 @@ export default class QueryTablesTab extends TabsBase {
this.buildCommandBarOptions();
}
public onExecuteQueryClick = (): Q.Promise<any> => {
this.queryViewModel().runQuery();
return null;
public onAddEntityClick = (): void => {
useSidePanel
.getState()
.openSidePanel(
"Add Table Entity",
<AddTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
};
public onQueryBuilderClick = (): Q.Promise<any> => {
this.queryViewModel().selectHelper();
return null;
public onEditEntityClick = (): void => {
useSidePanel
.getState()
.openSidePanel(
"Edit Table Entity",
<EditTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
};
public onQueryTextClick = (): Q.Promise<any> => {
this.queryViewModel().selectEditor();
return null;
};
public onAddEntityClick = (): Q.Promise<any> => {
this.container.openAddTableEntityPanel(this, this.tableEntityListViewModel());
return null;
};
public onEditEntityClick = (): Q.Promise<any> => {
this.container.openEditTableEntityPanel(this, this.tableEntityListViewModel());
return null;
};
public onDeleteEntityClick = (): Q.Promise<any> => {
public onDeleteEntityClick = (): void => {
this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel());
return null;
};
public onActivate(): void {
@ -166,7 +171,7 @@ export default class QueryTablesTab extends TabsBase {
!!this.tableEntityListViewModel() &&
!!this.tableEntityListViewModel().table &&
this.tableEntityListViewModel().table.columns;
if (!!columns) {
if (columns) {
columns.adjust();
$(window).resize();
}
@ -179,7 +184,7 @@ export default class QueryTablesTab extends TabsBase {
buttons.push({
iconSrc: QueryBuilderIcon,
iconAlt: label,
onCommandClick: this.onQueryBuilderClick,
onCommandClick: () => this.queryViewModel().selectHelper(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
@ -193,7 +198,7 @@ export default class QueryTablesTab extends TabsBase {
buttons.push({
iconSrc: QueryTextIcon,
iconAlt: label,
onCommandClick: this.onQueryTextClick,
onCommandClick: () => this.queryViewModel().selectEditor(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
@ -207,7 +212,7 @@ export default class QueryTablesTab extends TabsBase {
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: this.onExecuteQueryClick,
onCommandClick: () => this.queryViewModel().runQuery(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,

View File

@ -1,139 +1,23 @@
import ko from "knockout";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as DataModels from "../../Contracts/DataModels";
import React from "react";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent";
import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter";
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
import TabsBase from "./TabsBase";
export class SettingsTabV2 extends TabsBase {
public readonly html = '<div style="height: 100%" data-bind="react:settingsComponentAdapter"></div>';
public settingsComponentAdapter: SettingsComponentAdapter;
constructor(options: ViewModels.TabOptions) {
super(options);
const props: SettingsComponentProps = {
settingsTab: this,
};
this.settingsComponentAdapter = new SettingsComponentAdapter(props);
public render(): JSX.Element {
return <SettingsComponent settingsTab={this} />;
}
}
export class CollectionSettingsTabV2 extends SettingsTabV2 {
private notificationRead: ko.Observable<boolean>;
private notification: DataModels.Notification;
private offerRead: ko.Observable<boolean>;
constructor(options: ViewModels.TabOptions) {
super(options);
this.tabId = "SettingsV2-" + this.tabId;
this.notificationRead = ko.observable(false);
this.offerRead = ko.observable(false);
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
if (this.notificationRead() && this.offerRead()) {
this.pendingNotification(this.notification);
this.notification = undefined;
this.offerRead(false);
this.notificationRead(false);
return true;
}
return false;
});
}
public async onActivate(): Promise<void> {
try {
this.isExecuting(true);
const collection: ViewModels.Collection = this.collection as ViewModels.Collection;
await collection.loadOffer();
// passed in options and set by parent as "Settings" by default
this.tabTitle(collection.offer() ? "Settings" : "Scale & Settings");
const data: DataModels.Notification = await collection.getPendingThroughputSplitNotification();
this.notification = data;
this.notificationRead(true);
} catch (error) {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`);
throw error;
} finally {
this.offerRead(true);
this.isExecuting(false);
}
public onActivate(): void {
super.onActivate();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
}
}
export class DatabaseSettingsTabV2 extends SettingsTabV2 {
private notificationRead: ko.Observable<boolean>;
private notification: DataModels.Notification;
constructor(options: ViewModels.TabOptions) {
super(options);
this.tabId = "DatabaseSettingsV2-" + this.tabId;
this.notificationRead = ko.observable(false);
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
if (this.notificationRead()) {
this.pendingNotification(this.notification);
this.notification = undefined;
this.notificationRead(false);
return true;
}
return false;
});
}
public async onActivate(): Promise<void> {
try {
this.isExecuting(true);
const data: DataModels.Notification = await this.database.getPendingThroughputSplitNotification();
this.notification = data;
this.notificationRead(true);
} catch (error) {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
traceFailure(
Action.Tab,
{
databaseName: this.database.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
logConsoleError(`Error while fetching database settings for database ${this.database.id()}: ${errorMessage}`);
throw error;
} finally {
this.isExecuting(false);
}
public onActivate(): void {
super.onActivate();
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettingsV2);
}

View File

@ -1,89 +0,0 @@
<div class="tab-pane flexContainer stored-procedure-tab" data-bind="attr:{ id: tabId }" role="tabpanel">
<!-- Stored Procedure Tab Form - Start -->
<div class="storedTabForm flexContainer">
<div class="formTitleFirst">Stored Procedure Id</div>
<span class="formTitleTextbox">
<input
class="formTree"
type="text"
required
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
aria-label="Stored procedure id"
placeholder="Enter the new stored procedure id"
size="40"
data-bind="
textInput: id"
/>
</span>
<div class="spUdfTriggerHeader">Stored Procedure Body</div>
<editor
params="{
content: originalSprocBody,
contentType: 'javascript',
isReadOnly: false,
ariaLabel: 'Stored procedure body',
lineNumbers: 'on',
updatedContent: editorContent,
theme: _theme
}"
data-bind="attr: { id: editorId }"
></editor>
<!-- Results & Errors Content - Start-->
<div class="results-container" data-bind="visible: hasResults">
<div
class="toggles"
id="execute-storedproc-toggles"
aria-label="Successful execution of stored procedure"
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="Result"
>Result</span
>
</div>
<div class="tab">
<input type="radio" class="radio" value="logs" />
<span
class="toggleSwitch"
role="button"
tabindex="0"
data-bind="click: toggleLogs, css:{ selectedToggle: isLogsToggled(), unselectedToggle: !isLogsToggled() }"
aria-label="console.log"
>console.log</span
>
</div>
</div>
<json-editor
params="{ content: resultsData, isReadOnly: true, ariaLabel: 'Execute stored procedure result' }"
data-bind="attr: { id: executeResultsEditorId }, visible: hasResults() && isResultToggled()"
>
</json-editor>
<json-editor
params="{ content: logsData, isReadOnly: true, ariaLabel: 'Execute stored procedure logs' }"
data-bind="attr: { id: executeLogsEditorId }, visible: hasResults() && isLogsToggled()"
></json-editor>
</div>
<div class="errors-container" data-bind="visible: hasErrors">
<div class="errors-header">Errors:</div>
<div class="errorContent">
<span class="errorMessage" data-bind="text: error"></span>
<span class="errorDetailsLink">
<a
data-bind="click: $data.onErrorDetailsClick, event: { keypress: $data.onErrorDetailsKeyPress }"
aria-label="Error details link"
>More details</a
>
</span>
</div>
</div>
<!-- Results & Errors Content - End-->
</div>
</div>

View File

@ -1,287 +0,0 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
import * as _ from "underscore";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import * as Constants from "../../Common/Constants";
import { createStoredProcedure } from "../../Common/dataAccess/createStoredProcedure";
import { updateStoredProcedure } from "../../Common/dataAccess/updateStoredProcedure";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
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 { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import StoredProcedure from "../Tree/StoredProcedure";
import ScriptTabBase from "./ScriptTabBase";
import template from "./StoredProcedureTab.html";
enum ToggleState {
Result = "result",
Logs = "logs",
}
export default class StoredProcedureTab extends ScriptTabBase {
public readonly html = template;
public collection: ViewModels.Collection;
public node: StoredProcedure;
public executeResultsEditorId: string;
public executeLogsEditorId: string;
public toggleState: ko.Observable<ToggleState>;
public originalSprocBody: ViewModels.Editable<string>;
public resultsData: ko.Observable<string>;
public logsData: ko.Observable<string>;
public error: ko.Observable<string>;
public hasResults: ko.Observable<boolean>;
public hasErrors: ko.Observable<boolean>;
constructor(options: ViewModels.ScriptTabOption) {
super(options);
super.onActivate.bind(this);
this.executeResultsEditorId = `executestoredprocedureresults${this.tabId}`;
this.executeLogsEditorId = `executestoredprocedurelogs${this.tabId}`;
this.toggleState = ko.observable<ToggleState>(ToggleState.Result);
this.originalSprocBody = editable.observable<string>(this.editorContent());
this.resultsData = ko.observable<string>();
this.logsData = ko.observable<string>();
this.error = ko.observable<string>();
this.hasResults = ko.observable<boolean>(false);
this.hasErrors = ko.observable<boolean>(false);
this.error.subscribe((error: string) => {
this.hasErrors(error != null);
this.hasResults(error == null);
});
this.ariaLabel("Stored Procedure Body");
this.buildCommandBarOptions();
}
public onSaveClick = (): Promise<StoredProcedureDefinition & Resource> => {
return this._createStoredProcedure({
id: this.id(),
body: this.editorContent(),
});
};
public onDiscard = (): Q.Promise<any> => {
this.setBaselines();
const original = this.editorContent.getEditableOriginalValue();
this.originalSprocBody(original);
this.originalSprocBody.valueHasMutated(); // trigger a re-render of the editor
return Q();
};
public onUpdateClick = (): Promise<any> => {
const data = this._getResource();
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateStoredProcedure, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
return updateStoredProcedure(this.collection.databaseId, this.collection.id(), data)
.then(
(updatedResource) => {
this.resource(updatedResource);
this.tabTitle(updatedResource.id);
this.node.id(updatedResource.id);
this.node.body(updatedResource.body as string);
this.setBaselines();
const editorModel = this.editor() && this.editor().getModel();
editorModel && editorModel.setValue(updatedResource.body as string);
this.editorContent.setBaseline(updatedResource.body as string);
TelemetryProcessor.traceSuccess(
Action.UpdateStoredProcedure,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error: any) => {
this.isExecutionError(true);
TelemetryProcessor.traceFailure(
Action.UpdateStoredProcedure,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onExecuteSprocsResult(result: any, logsData: any): void {
const resultData: string = this.renderObjectForEditor(_.omit(result, "scriptLogs").result, null, 4);
const scriptLogs: string = (result.scriptLogs && decodeURIComponent(result.scriptLogs)) || "";
const logs: string = this.renderObjectForEditor(scriptLogs, null, 4);
this.error(null);
this.resultsData(resultData);
this.logsData(logs);
}
public onExecuteSprocsError(error: string): void {
this.isExecutionError(true);
console.error(error);
this.error(error);
}
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.resultsData.valueHasMutated(); // needed to refresh the json-editor component
}
public toggleLogs(): void {
this.toggleState(ToggleState.Logs);
this.logsData.valueHasMutated(); // needed to refresh the json-editor component
}
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.toggleLogs();
event.stopPropagation();
return false;
}
return true;
};
public isResultToggled(): boolean {
return this.toggleState() === ToggleState.Result;
}
public isLogsToggled(): boolean {
return this.toggleState() === ToggleState.Logs;
}
protected updateSelectedNode(): void {
if (this.collection == null) {
return;
}
const database: ViewModels.Database = this.collection.getDatabase();
if (!database.isDatabaseExpanded()) {
this.collection.container.selectedNode(database);
} else if (!this.collection.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) {
this.collection.container.selectedNode(this.collection);
} else {
this.collection.container.selectedNode(this.node);
}
}
protected buildCommandBarOptions(): void {
ko.computed(() => ko.toJSON([this.isNew, this.formIsDirty])).subscribe(() => this.updateNavbarWithTabsButtons());
super.buildCommandBarOptions();
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const label = "Execute";
return super.getTabsButtons().concat({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: () => {
this.collection && this.collection.container.openExecuteSprocParamsPanel(this.node);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: this.isNew() || this.formIsDirty(),
});
}
private _getResource() {
return {
id: this.id(),
body: this.editorContent(),
};
}
private _createStoredProcedure(resource: StoredProcedureDefinition): Promise<StoredProcedureDefinition & Resource> {
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateStoredProcedure, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
return createStoredProcedure(this.collection.databaseId, this.collection.id(), resource)
.then(
(createdResource) => {
this.tabTitle(createdResource.id);
this.isNew(false);
this.resource(createdResource);
this.hashLocation(
`${Constants.HashRoutePrefixes.collectionsWithIds(
this.collection.databaseId,
this.collection.id()
)}/sprocs/${createdResource.id}`
);
this.setBaselines();
const editorModel = this.editor() && this.editor().getModel();
editorModel && editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createStoredProcedureNode(createdResource);
TelemetryProcessor.traceSuccess(
Action.CreateStoredProcedure,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
return createdResource;
},
(createError) => {
this.isExecutionError(true);
TelemetryProcessor.traceFailure(
Action.CreateStoredProcedure,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(createError),
errorStack: getErrorStack(createError),
},
startKey
);
return Promise.reject(createError);
}
)
.finally(() => this.isExecuting(false));
}
public onDelete(): Q.Promise<any> {
// TODO
return Q();
}
}

View File

@ -0,0 +1,70 @@
import React from "react";
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure";
import ScriptTabBase from "../ScriptTabBase";
import StoredProcedureTabComponent, {
IStoredProcTabComponentProps,
IStorProcTabComponentAccessor,
} from "./StoredProcedureTabComponent";
export interface IStoredProcTabProps {
container: Explorer;
collection: ViewModels.Collection;
}
export class NewStoredProcedureTab extends ScriptTabBase {
public queryText: string;
public currentQuery: string;
public partitionKey: DataModels.PartitionKey;
public iStoredProcTabComponentProps: IStoredProcTabComponentProps;
public iStoreProcAccessor: IStorProcTabComponentAccessor;
public node: StoredProcedure;
public onSaveClick: () => void;
public onUpdateClick: () => Promise<void>;
constructor(options: ViewModels.ScriptTabOption, private props: IStoredProcTabProps) {
super(options);
this.partitionKey = options.partitionKey;
this.iStoredProcTabComponentProps = {
resource: options.resource,
isNew: options.isNew,
tabKind: options.tabKind,
title: options.title,
tabPath: options.tabPath,
collectionBase: options.collection,
node: options.node,
scriptTabBaseInstance: this,
collection: props.collection,
iStorProcTabComponentAccessor: (instance: IStorProcTabComponentAccessor) => {
this.iStoreProcAccessor = instance;
},
container: props.container,
};
}
public render(): JSX.Element {
return <StoredProcedureTabComponent {...this.iStoredProcTabComponentProps} />;
}
public onTabClick(): void {
useTabs.getState().activateTab(this);
this.iStoreProcAccessor.onTabClickEvent();
}
public onCloseTabButtonClick(): void {
useTabs.getState().closeTab(this);
}
public onExecuteSprocsResult(result: ExecuteSprocResult): void {
this.iStoreProcAccessor.onExecuteSprocsResultEvent(result);
}
public onExecuteSprocsError(error: string): void {
this.iStoreProcAccessor.onExecuteSprocsErrorEvent(error);
}
}

View File

@ -0,0 +1,597 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { Pivot, PivotItem } from "@fluentui/react";
import React from "react";
import DiscardIcon from "../../../../images/discard.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { NormalizedEventKey } from "../../../Common/Constants";
import { createStoredProcedure } from "../../../Common/dataAccess/createStoredProcedure";
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import StoredProcedure from "../../Tree/StoredProcedure";
import { useSelectedNode } from "../../useSelectedNode";
import ScriptTabBase from "../ScriptTabBase";
export interface IStorProcTabComponentAccessor {
onExecuteSprocsResultEvent: (result: ExecuteSprocResult) => void;
onExecuteSprocsErrorEvent: (error: string) => void;
onTabClickEvent: () => void;
}
export interface Button {
visible: boolean;
enabled: boolean;
isSelected?: boolean;
}
interface IStoredProcTabComponentStates {
hasResults: boolean;
hasErrors: boolean;
error: string;
resultData: string;
logsData: string;
originalSprocBody: string;
initialEditorContent: string;
sProcEditorContent: string;
id: string;
executeButton: Button;
saveButton: Button;
updateButton: Button;
discardButton: Button;
}
export interface IStoredProcTabComponentProps {
resource: StoredProcedureDefinition;
isNew: boolean;
tabKind: ViewModels.CollectionTabKind;
title: string;
tabPath: string;
collectionBase: ViewModels.CollectionBase;
//eslint-disable-next-line
node?: any;
scriptTabBaseInstance: ScriptTabBase;
collection: ViewModels.Collection;
iStorProcTabComponentAccessor: (instance: IStorProcTabComponentAccessor) => void;
container: Explorer;
}
export default class StoredProcedureTabComponent extends React.Component<
IStoredProcTabComponentProps,
IStoredProcTabComponentStates
> {
public node: StoredProcedure;
public executeResultsEditorId: string;
public executeLogsEditorId: string;
public collection: ViewModels.Collection;
constructor(
public storedProcTabCompProps: IStoredProcTabComponentProps,
private storedProcTabCompStates: IStoredProcTabComponentStates
) {
super(storedProcTabCompProps);
this.state = {
error: "",
hasErrors: false,
hasResults: false,
resultData: "",
logsData: "",
originalSprocBody: this.props.resource.body.toString(),
initialEditorContent: this.props.resource.body.toString(),
sProcEditorContent: this.props.resource.body.toString(),
id: this.props.resource.id,
executeButton: {
enabled: !this.props.scriptTabBaseInstance.isNew(),
visible: true,
},
saveButton: {
enabled: (() => {
if (!this.props.scriptTabBaseInstance.formIsValid()) {
return false;
}
if (!this.props.scriptTabBaseInstance.formIsDirty()) {
return false;
}
return true;
})(),
visible: this.props.scriptTabBaseInstance.isNew(),
},
updateButton: {
enabled: (() => {
if (!this.props.scriptTabBaseInstance.formIsValid()) {
return false;
}
if (!this.props.scriptTabBaseInstance.formIsDirty()) {
return false;
}
return true;
})(),
visible: !this.props.scriptTabBaseInstance.isNew(),
},
discardButton: {
enabled: (() => {
if (!this.props.scriptTabBaseInstance.formIsValid()) {
return false;
}
if (!this.props.scriptTabBaseInstance.formIsDirty()) {
return false;
}
return true;
})(),
visible: true,
},
};
this.collection = this.props.collection;
this.executeResultsEditorId = `executestoredprocedureresults${this.props.scriptTabBaseInstance.tabId}`;
this.executeLogsEditorId = `executestoredprocedurelogs${this.props.scriptTabBaseInstance.tabId}`;
this.props.scriptTabBaseInstance.ariaLabel("Stored Procedure Body");
this.props.iStorProcTabComponentAccessor({
onExecuteSprocsResultEvent: this.onExecuteSprocsResult.bind(this),
onExecuteSprocsErrorEvent: this.onExecuteSprocsError.bind(this),
onTabClickEvent: this.onTabClick.bind(this),
});
this.node = this.props.node;
this.buildCommandBarOptions();
}
public onTabClick(): void {
if (useTabs.getState().openedTabs.length > 0) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
}
public onSaveClick = (): Promise<StoredProcedureDefinition & Resource> => {
return this._createStoredProcedure({
id: this.state.id,
body: this.state.sProcEditorContent,
});
};
public onDiscard = (): Promise<unknown> => {
const onDiscardPromise = new Promise(() => {
this.props.scriptTabBaseInstance.setBaselines();
const original = this.props.scriptTabBaseInstance.editorContent.getEditableOriginalValue();
if (this.state.updateButton.visible) {
this.setState({
updateButton: {
enabled: false,
visible: true,
},
sProcEditorContent: original,
discardButton: {
enabled: false,
visible: true,
},
executeButton: {
enabled: true,
visible: true,
},
});
} else {
this.setState({
saveButton: {
enabled: false,
visible: true,
},
sProcEditorContent: original,
discardButton: {
enabled: false,
visible: true,
},
executeButton: {
enabled: false,
visible: true,
},
id: "",
});
}
});
setTimeout(() => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}, 100);
return onDiscardPromise;
};
public onUpdateClick = (): Promise<void> => {
const data = this._getResource();
this.props.scriptTabBaseInstance.isExecutionError(false);
this.props.scriptTabBaseInstance.isExecuting(true);
return updateStoredProcedure(
this.props.scriptTabBaseInstance.collection.databaseId,
this.props.scriptTabBaseInstance.collection.id(),
data
)
.then(
(updatedResource) => {
this.props.scriptTabBaseInstance.resource(updatedResource);
this.props.scriptTabBaseInstance.tabTitle(updatedResource.id);
this.node.id(updatedResource.id);
this.node.body(updatedResource.body as string);
this.props.scriptTabBaseInstance.setBaselines();
const editorModel =
this.props.scriptTabBaseInstance.editor() && this.props.scriptTabBaseInstance.editor().getModel();
editorModel && editorModel.setValue(updatedResource.body as string);
this.props.scriptTabBaseInstance.editorContent.setBaseline(updatedResource.body as string);
this.setState({
discardButton: {
enabled: false,
visible: true,
},
updateButton: {
enabled: false,
visible: true,
},
executeButton: {
enabled: true,
visible: true,
},
});
useCommandBar.getState().setContextButtons(this.getTabsButtons());
},
() => {
this.props.scriptTabBaseInstance.isExecutionError(true);
}
)
.finally(() => this.props.scriptTabBaseInstance.isExecuting(false));
};
public onExecuteSprocsResult(result: ExecuteSprocResult): void {
const resultData: string = this.props.scriptTabBaseInstance.renderObjectForEditor(result.result, undefined, 4);
const scriptLogs: string = (result.scriptLogs && decodeURIComponent(result.scriptLogs)) || "";
const logs: string = this.props.scriptTabBaseInstance.renderObjectForEditor(scriptLogs, undefined, 4);
this.setState({
hasResults: false,
});
setTimeout(() => {
this.setState({
error: undefined,
resultData: resultData,
logsData: logs,
hasResults: resultData ? true : false,
hasErrors: false,
});
}, 100);
}
public onExecuteSprocsError(error: string): void {
this.props.scriptTabBaseInstance.isExecutionError(true);
console.error(error);
this.setState({
error: error,
hasErrors: true,
hasResults: false,
});
}
public onErrorDetailsClick = (): boolean => {
useNotificationConsole.getState().expandConsole();
return false;
};
public onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
this.onErrorDetailsClick();
return false;
}
return true;
};
protected updateSelectedNode(): void {
if (this.props.collectionBase === undefined) {
return;
}
const database: ViewModels.Database = this.props.collectionBase.getDatabase();
const setSelectedNode = useSelectedNode.getState().setSelectedNode;
if (!database.isDatabaseExpanded()) {
setSelectedNode(database);
} else if (!this.props.collectionBase.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) {
setSelectedNode(this.props.collectionBase);
} else {
setSelectedNode(this.node);
}
}
protected buildCommandBarOptions(): void {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = "Save";
if (this.state.saveButton.visible) {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.state.saveButton.enabled,
});
}
if (this.state.updateButton.visible) {
const label = "Update";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onUpdateClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.state.updateButton.enabled,
});
}
if (this.state.discardButton.visible) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onDiscard,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.state.discardButton.enabled,
});
}
if (this.state.executeButton.visible) {
const label = "Execute";
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: () => {
this.collection.container.openExecuteSprocParamsPanel(this.node);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.state.executeButton.enabled,
});
}
return buttons;
}
private _getResource() {
return {
id: this.state.id,
body: this.state.sProcEditorContent,
};
}
private _createStoredProcedure(resource: StoredProcedureDefinition): Promise<StoredProcedureDefinition & Resource> {
this.props.scriptTabBaseInstance.isExecutionError(false);
this.props.scriptTabBaseInstance.isExecuting(true);
return createStoredProcedure(this.props.collectionBase.databaseId, this.props.collectionBase.id(), resource)
.then(
(createdResource) => {
this.props.scriptTabBaseInstance.tabTitle(createdResource.id);
this.props.scriptTabBaseInstance.isNew(false);
this.props.scriptTabBaseInstance.resource(createdResource);
this.props.scriptTabBaseInstance.setBaselines();
const editorModel =
this.props.scriptTabBaseInstance.editor() && this.props.scriptTabBaseInstance.editor().getModel();
editorModel && editorModel.setValue(createdResource.body as string);
this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createStoredProcedureNode(createdResource);
this.props.scriptTabBaseInstance.node = this.node;
useTabs.getState().updateTab(this.props.scriptTabBaseInstance);
this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
this.setState({
executeButton: {
enabled: false,
visible: true,
},
});
setTimeout(() => {
this.setState({
executeButton: {
enabled: true,
visible: true,
},
updateButton: {
enabled: false,
visible: true,
},
saveButton: {
enabled: false,
visible: false,
},
discardButton: {
enabled: false,
visible: true,
},
sProcEditorContent: this.state.sProcEditorContent,
});
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}, 100);
return createdResource;
},
(createError) => {
this.props.scriptTabBaseInstance.isExecutionError(true);
return Promise.reject(createError);
}
)
.finally(() => this.props.scriptTabBaseInstance.isExecuting(false));
}
public onDelete(): Promise<unknown> {
const isDeleted = false;
const onDeletePromise = new Promise((resolve) => {
resolve(isDeleted);
});
return onDeletePromise;
}
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (this.state.saveButton.visible) {
this.setState({
id: event.target.value,
saveButton: {
enabled: true,
visible: this.props.scriptTabBaseInstance.isNew(),
},
discardButton: {
enabled: true,
visible: true,
},
});
}
setTimeout(() => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}, 1000);
}
public onChangeContent(newConent: string): void {
if (this.state.updateButton.visible) {
this.setState({
updateButton: {
enabled: true,
visible: true,
},
discardButton: {
enabled: true,
visible: true,
},
executeButton: {
enabled: false,
visible: true,
},
sProcEditorContent: newConent,
});
} else {
this.setState({
saveButton: {
enabled: false,
visible: this.props.scriptTabBaseInstance.isNew(),
},
executeButton: {
enabled: false,
visible: true,
},
discardButton: {
enabled: true,
visible: true,
},
sProcEditorContent: newConent,
});
}
setTimeout(() => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}, 100);
}
render(): JSX.Element {
return (
<div className="tab-pane flexContainer stored-procedure-tab" role="tabpanel">
<div className="storedTabForm flexContainer">
<div className="formTitleFirst">Stored Procedure Id</div>
<span className="formTitleTextbox">
<input
className="formTree"
type="text"
required
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
aria-label="Stored procedure id"
placeholder="Enter the new stored procedure id"
size={40}
value={this.state.id}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => this.handleIdOnChange(event)}
/>
</span>
<div className="spUdfTriggerHeader">Stored Procedure Body</div>
<EditorReact
language={"javascript"}
content={this.state.sProcEditorContent}
isReadOnly={false}
ariaLabel={"Stored procedure body"}
lineNumbers={"on"}
theme={"_theme"}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
/>
{this.state.hasResults && (
<div className="results-container">
<Pivot aria-label="Successful execution of stored procedure" style={{ height: "100%" }}>
<PivotItem
headerText="Result"
headerButtonProps={{
"data-order": 1,
"data-title": "Result",
}}
style={{ height: "100%" }}
>
<EditorReact
language={"javascript"}
content={this.state.resultData}
isReadOnly={true}
ariaLabel={"Execute stored procedure result"}
/>
</PivotItem>
<PivotItem
headerText="console.log"
headerButtonProps={{
"data-order": 2,
"data-title": "console.log",
}}
style={{ height: "100%" }}
>
<EditorReact
language={"javascript"}
content={this.state.logsData}
isReadOnly={true}
ariaLabel={"Execute stored procedure logs"}
/>
</PivotItem>
</Pivot>
</div>
)}
{this.state.hasErrors && (
<div className="errors-container">
<div className="errors-header">Errors:</div>
<div className="errorContent">
<span className="errorMessage">{this.state.error}</span>
<span className="errorDetailsLink">
<a
aria-label="Error details link"
onClick={() => this.onErrorDetailsClick()}
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => this.onErrorDetailsKeyPress(event)}
>
More details
</a>
</span>
</div>
</div>
)}
</div>
</div>
);
}
}

View File

@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable";
import { useTabs } from "../../hooks/useTabs";
import TabsBase from "./TabsBase";
type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => (
export const Tabs = (): JSX.Element => {
const { openedTabs, activeTab } = useTabs();
return (
<div className="tabsManagerContainer">
<div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{tabs.map((tab) => (
{openedTabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</ul>
</div>
<div className="tabPanesContainer">
{tabs.map((tab) => (
{openedTabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</div>
</div>
</div>
);
};
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false);

View File

@ -4,15 +4,14 @@ import * as ThemeUtility from "../../Common/ThemeUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { RouteHandler } from "../../RouteHandlers/RouteHandler";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel {
private static id = 0;
@ -26,11 +25,9 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public tabKind: ViewModels.CollectionTabKind;
public tabTitle: ko.Observable<string>;
public tabPath: ko.Observable<string>;
public hashLocation: ko.Observable<string>;
public isExecutionError = ko.observable(false);
public isExecuting = ko.observable(false);
public pendingNotification?: ko.Observable<DataModels.Notification>;
public manager?: TabsManager;
protected _theme: string;
public onLoadStartKey: number;
@ -50,8 +47,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
this.onLoadStartKey = options.onLoadStartKey;
this.hashLocation = ko.observable<string>(options.hashLocation || "");
this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation));
this.closeTabButton = {
enabled: ko.computed<boolean>(() => {
return true;
@ -64,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onCloseTabButtonClick(): void {
this.manager?.closeTab(this);
useTabs.getState().closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
tabName: this.constructor.name,
dataExplorerArea: Constants.Areas.Tab,
@ -74,17 +69,18 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onTabClick(): void {
this.manager?.activateTab(this);
useTabs.getState().activateTab(this);
}
protected updateSelectedNode(): void {
const relatedDatabase = (this.collection && this.collection.getDatabase()) || this.database;
const setSelectedNode = useSelectedNode.getState().setSelectedNode;
if (relatedDatabase && !relatedDatabase.isDatabaseExpanded()) {
this.getContainer().selectedNode(relatedDatabase);
setSelectedNode(relatedDatabase);
} else if (this.collection && !this.collection.isCollectionExpanded()) {
this.getContainer().selectedNode(this.collection);
setSelectedNode(this.collection);
} else {
this.getContainer().selectedNode(this.node);
setSelectedNode(this.node);
}
}
@ -108,14 +104,13 @@ export default class TabsBase extends WaitsForTemplateViewModel {
/** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
public isActive() {
return this === this.manager?.activeTab();
return this === useTabs.getState().activeTab;
}
public onActivate(): void {
this.updateSelectedNode();
this.collection?.selectedSubnodeKind(this.tabKind);
this.database?.selectedSubnodeKind(this.tabKind);
this.updateGlobalHash(this.hashLocation());
this.updateNavbarWithTabsButtons();
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, {
tabName: this.constructor.name,
@ -149,14 +144,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
/** 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 JSON.stringify(value, replacer, space);
}
private updateGlobalHash(newHash: string): void {
RouteHandler.getInstance().updateRouteHashLocation(newHash);
}
/**
* @return buttons that are displayed in the navbar
*/
@ -164,7 +155,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return [];
}
protected updateNavbarWithTabsButtons = (): void => {
public updateNavbarWithTabsButtons = (): void => {
if (this.isActive()) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}

View File

@ -1,131 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
import QueryTab from "./QueryTab";
import { TabsManager } from "./TabsManager";
describe("Tabs manager tests", () => {
let tabsManager: TabsManager;
let explorer: Explorer;
let database: ViewModels.Database;
let collection: ViewModels.Collection;
let queryTab: QueryTab;
let documentsTab: DocumentsTab;
beforeAll(() => {
explorer = new Explorer();
updateUserContext({
databaseAccount: {
id: "test",
name: "test",
location: "",
type: "",
kind: "",
properties: undefined,
},
});
database = {
container: explorer,
id: ko.observable<string>("test"),
isDatabaseShared: () => false,
} as ViewModels.Database;
database.isDatabaseExpanded = ko.observable<boolean>(true);
database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
collection = {
container: explorer,
databaseId: "test",
id: ko.observable<string>("test"),
} as ViewModels.Collection;
collection.getDatabase = (): ViewModels.Database => database;
collection.isCollectionExpanded = ko.observable<boolean>(true);
collection.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
queryTab = new QueryTab({
tabKind: ViewModels.CollectionTabKind.Query,
collection,
database,
title: "",
tabPath: "",
hashLocation: "",
});
documentsTab = new DocumentsTab({
partitionKey: undefined,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
collection,
title: "",
tabPath: "",
hashLocation: "",
});
// make sure tabs have different tabId
queryTab.tabId = "1";
documentsTab.tabId = "2";
});
beforeEach(() => (tabsManager = new TabsManager()));
it("open new tabs", () => {
tabsManager.activateNewTab(queryTab);
expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
tabsManager.activateNewTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(2);
expect(tabsManager.openedTabs()[1]).toEqual(documentsTab);
expect(tabsManager.activeTab()).toEqual(documentsTab);
expect(queryTab.isActive()).toBe(false);
expect(documentsTab.isActive()).toBe(true);
});
it("open existing tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
tabsManager.activateTab(queryTab);
expect(tabsManager.openedTabs().length).toBe(2);
expect(tabsManager.activeTab()).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false);
});
it("get tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query);
expect(queryTabs.length).toBe(1);
expect(queryTabs[0]).toEqual(queryTab);
const documentsTabs = tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab) => tab.tabId === documentsTab.tabId
);
expect(documentsTabs.length).toBe(1);
expect(documentsTabs[0]).toEqual(documentsTab);
});
it("close tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
tabsManager.closeTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false);
tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
expect(tabsManager.openedTabs().length).toBe(0);
expect(tabsManager.activeTab()).toEqual(undefined);
expect(queryTab.isActive()).toBe(false);
});
});

View File

@ -1,54 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "./TabsBase";
export class TabsManager {
public openedTabs = ko.observableArray<TabsBase>([]);
public activeTab = ko.observable<TabsBase>();
public activateNewTab(tab: TabsBase): void {
this.openedTabs.push(tab);
this.activateTab(tab);
}
public activateTab(tab: TabsBase): void {
if (this.openedTabs().includes(tab)) {
tab.manager = this;
this.activeTab(tab);
tab.onActivate();
}
}
public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] {
return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab)));
}
public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate();
}
public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void {
this.openedTabs()
.filter(comparator)
.forEach((tab) => tab.onCloseTabButtonClick());
}
public closeTab(tab: TabsBase): void {
const tabIndex = this.openedTabs().indexOf(tab);
if (tabIndex !== -1) {
this.openedTabs.remove(tab);
tab.manager = undefined;
if (this.openedTabs().length === 0) {
this.activeTab(undefined);
}
if (tab === this.activeTab()) {
const tabToTheRight = this.openedTabs()[tabIndex];
const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1];
this.activateTab(tabToTheRight ?? lastOpenTab);
}
}
}
}

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