Compare commits

..

3 Commits

Author SHA1 Message Date
artrejo
8c287770d7 redirect mongo proxy calls to tools federation 2021-07-20 14:19:04 -07:00
artrejo
6ce45fd04b fix casing 2021-07-20 13:56:29 -07:00
artrejo
e4e602a81e Add support for pseudo enum flags 2021-07-20 13:22:48 -07:00
117 changed files with 9930 additions and 8269 deletions

View File

@@ -1 +1,16 @@
PORTAL_RUNNER_USERNAME=
PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_CONNECTION_STRING=
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html

View File

@@ -21,8 +21,16 @@ src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts src/Common/NotificationsClientBase.ts
src/Common/QueriesClient.ts src/Common/QueriesClient.ts
src/Common/Splitter.ts src/Common/Splitter.ts
src/Config.ts
src/Contracts/ActionContracts.ts
src/Contracts/DataModels.ts
src/Contracts/Diagnostics.ts
src/Contracts/ExplorerContracts.ts
src/Contracts/Versions.ts
src/Contracts/ViewModels.ts
src/Controls/Heatmap/Heatmap.test.ts src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts src/Controls/Heatmap/Heatmap.ts
src/Controls/Heatmap/HeatmapDatatypes.ts
src/Definitions/datatables.d.ts src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts src/Definitions/gif.d.ts
src/Definitions/globals.d.ts src/Definitions/globals.d.ts
@@ -36,10 +44,29 @@ src/Definitions/png.d.ts
src/Definitions/svg.d.ts src/Definitions/svg.d.ts
src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.test.ts
src/Explorer/ComponentRegisterer.ts src/Explorer/ComponentRegisterer.ts
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
src/Explorer/Controls/DynamicList/DynamicList.test.ts
src/Explorer/Controls/DynamicList/DynamicListComponent.ts
src/Explorer/Controls/Editor/EditorComponent.ts src/Explorer/Controls/Editor/EditorComponent.ts
src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
src/Explorer/Controls/Toolbar/IToolbarAction.ts
src/Explorer/Controls/Toolbar/IToolbarDisplayable.ts
src/Explorer/Controls/Toolbar/IToolbarDropDown.ts
src/Explorer/Controls/Toolbar/IToolbarItem.ts
src/Explorer/Controls/Toolbar/IToolbarSeperator.ts
src/Explorer/Controls/Toolbar/IToolbarToggle.ts
src/Explorer/Controls/Toolbar/KeyCodes.ts
src/Explorer/Controls/Toolbar/Toolbar.ts
src/Explorer/Controls/Toolbar/ToolbarAction.ts
src/Explorer/Controls/Toolbar/ToolbarDropDown.ts
src/Explorer/Controls/Toolbar/ToolbarToggle.ts
src/Explorer/Controls/Toolbar/Utilities.ts
src/Explorer/DataSamples/ContainerSampleGenerator.test.ts src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
src/Explorer/DataSamples/ContainerSampleGenerator.ts src/Explorer/DataSamples/ContainerSampleGenerator.ts
src/Explorer/DataSamples/DataSamplesUtil.test.ts src/Explorer/DataSamples/DataSamplesUtil.test.ts
@@ -164,4 +191,4 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
src/Explorer/Tree/ResourceTreeAdapter.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx
__mocks__/monaco-editor.ts __mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTree.tsx src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx

View File

@@ -143,7 +143,7 @@ jobs:
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts - ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off - ./test/notebooks/upload.spec.ts
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:

View File

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 842 B

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,7 +9,6 @@
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@GrayScale: "grayscale()";
@xSmallFontSize: 4px; @xSmallFontSize: 4px;
@smallFontSize: 8px; @smallFontSize: 8px;

View File

@@ -2,7 +2,6 @@
.dataResourceTree { .dataResourceTree {
margin-left: @MediumSpace; margin-left: @MediumSpace;
overflow: auto;
.databaseHeader { .databaseHeader {
font-size: 14px; font-size: 14px;
@@ -19,10 +18,6 @@
.notebookHeader { .notebookHeader {
font-size: 12px; font-size: 12px;
} }
.clickDisabled {
pointer-events: none;
}
} }

View File

@@ -1,270 +1,273 @@
@import "./Common/Constants"; @import "./Common/Constants";
.resourceTree { .resourceTree {
height: 100%;
flex: 0 0 auto;
.main {
height: 100%; height: 100%;
} width: 20%;
flex: 0 0 auto;
.main {
height: 100%;
}
} }
.resourceTreeScroll { .resourceTreeScroll {
height: 100%; height: 100%;
display: flex; display: flex;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding-right: 10px; padding-right: 10px;
} }
.userSelectNone { .userSelectNone {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
} }
.treeHovermargin { .treeHovermargin {
margin-left: 16px; margin-left: 16px;
} }
.highlight { .highlight {
padding: @SmallSpace 2px; padding: @SmallSpace 2px;
outline: 0; outline: 0;
&:hover { &:hover {
.hover(); .hover();
} }
&:active { &:active {
.active(); .active();
} }
&:focus { &:focus {
.focus(); .focus();
} }
} }
.contextmenushowing { .contextmenushowing {
background-color: #eee; background-color: #EEE;
} }
.collectionstree { .collectionstree {
width: 100%; width: 100%;
margin-top: @DefaultSpace; margin-top: @DefaultSpace;
.databaseList {
list-style-type: none;
padding-left: 0px;
.collectionList { .databaseList {
padding-left: (2 * @MediumSpace); list-style-type: none;
padding-left: 0px;
.collectionList {
padding-left:(2 * @MediumSpace);
}
.collectionChildList {
padding-left: @LargeSpace;
}
.databaseDocuments {
padding-left: (5 * @MediumSpace);
}
} }
.collectionChildList {
padding-left: @LargeSpace;
}
.databaseDocuments {
padding-left: (5 * @MediumSpace);
}
}
} }
.pointerCursor { .pointerCursor {
cursor: pointer; cursor: pointer;
} }
.menuEllipsis { .menuEllipsis {
padding-right: 6px; padding-right: 6px;
font-weight: bold; font-weight: bold;
font-size: 18px; font-size: 18px;
position: relative; position: relative;
top: -5px; top: -5px;
left: 0px; left: 0px;
float: right; float: right;
display: none; display: none;
padding-left: 6px !important; padding-left: 6px!important;
line-height: @TreeLineHeight; line-height: @TreeLineHeight;
} }
.databaseMenu { .databaseMenu {
.flex-display(); .flex-display();
} }
.databaseMenu:hover .menuEllipsis, .databaseMenu:hover .menuEllipsis,
.databaseMenu:focus .menuEllipsis { .databaseMenu:focus .menuEllipsis {
display: block; display: block;
} }
.databaseCollChildTextOverflow { .databaseCollChildTextOverflow {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
flex: 1; flex: 1;
} }
.collectionMenu { .collectionMenu {
.flex-display(); .flex-display();
} }
.collectionMenu:hover .menuEllipsis, .collectionMenu:hover .menuEllipsis,
.collectionMenu:focus .menuEllipsis { .collectionMenu:focus .menuEllipsis {
display: block; display: block;
} }
.documentsMenu:hover .menuEllipsis, .documentsMenu:hover .menuEllipsis,
.documentsMenu:focus .menuEllipsis { .documentsMenu:focus .menuEllipsis {
display: block; display: block;
} }
.treeChildMenu { .treeChildMenu {
display: flex; display: flex;
} }
.storedProcedureMenu:hover .menuEllipsis, .storedProcedureMenu:hover .menuEllipsis,
.storedProcedureMenu:focus .menuEllipsis { .storedProcedureMenu:focus .menuEllipsis {
display: block; display: block;
} }
.childMenu { .childMenu {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
padding-left: (6 * @MediumSpace); padding-left: (6 * @MediumSpace);
width: 100%; width: 100%;
} }
.storedChildMenu:hover .menuEllipsis, .storedChildMenu:hover .menuEllipsis,
.storedChildMenu:focus .menuEllipsis { .storedChildMenu:focus .menuEllipsis {
display: block; display: block;
} }
.contextmenu6 { .contextmenu6 {
top: -29px; top: -29px;
} }
.userDefinedMenu:hover .contextmenu6 { .userDefinedMenu:hover .contextmenu6 {
display: block; display: block;
} }
.userDefinedchildMenu:hover .menuEllipsis, .userDefinedchildMenu:hover .menuEllipsis,
.userDefinedchildMenu:focus .menuEllipsis { .userDefinedchildMenu:focus .menuEllipsis {
display: block; display: block;
} }
.triggersMenu:hover .menuEllipsis, .triggersMenu:hover .menuEllipsis,
.triggersMenu:focus .menuEllipsis { .triggersMenu:focus .menuEllipsis {
display: block; display: block;
} }
.triggersChildMenu:hover .menuEllipsis, .triggersChildMenu:hover .menuEllipsis,
.triggersChildMenu:focus .menuEllipsis { .triggersChildMenu:focus .menuEllipsis {
display: block; display: block;
} }
.databaseId { .databaseId {
font-size: 14px; font-size: 14px;
} }
.storedUdfTriggerMenu { .storedUdfTriggerMenu {
padding-left: 0px; padding-left: 0px;
} }
.collectionstree img { .collectionstree img {
width: 16px; width: 16px;
height: 16px; height: 16px;
vertical-align: text-top; vertical-align: text-top;
} }
img.collectionsTreeCollapseExpand { img.collectionsTreeCollapseExpand {
width: 10px; width: 10px;
height: 10px; height: 10px;
vertical-align: middle; vertical-align: middle;
margin-bottom: 5px; margin-bottom: 5px;
} }
.collapsed::before { .collapsed::before {
content: "\23F5"; content: "\23F5";
margin-left: 0px; margin-left: 0px;
font-size: 15px; font-size: 15px;
} }
.expanded::before { .expanded::before {
content: "\23F7"; content: '\23F7';
margin-left: 0px; margin-left: 0px;
font-size: 15px; font-size: 15px;
} }
.collectionMenuChildren { .collectionMenuChildren {
padding-left: 42px; padding-left: 42px;
} }
.main-nav { .main-nav {
width: 100vh; width: 100vh;
height: 40px; height: 40px;
background: white; background: white;
transform-origin: left top; transform-origin: left top;
-webkit-transform-origin: left top; -webkit-transform-origin: left top;
-ms-transform-origin: left top; -ms-transform-origin: left top;
transform: rotate(-90deg) translateX(-100%); transform: rotate(-90deg) translateX(-100%);
-webkit-transform: rotate(-90deg) translateX(-100%); -webkit-transform: rotate(-90deg) translateX(-100%);
-ms-transform: rotate(-90deg) translateX(-100%); -ms-transform: rotate(-90deg) translateX(-100%);
border-bottom: 1px solid #ccc; border-bottom: 1px solid #CCC;
} }
.main-nav-img { .main-nav-img {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin: -32px 0 0 0; margin: -32px 0 0 0;
transform: rotate(-90deg) translateX(-100%); transform: rotate(-90deg) translateX(-100%);
-webkit-transform: rotate(-90deg) translateX(-100%); -webkit-transform: rotate(-90deg) translateX(-100%);
-ms-transform: rotate(-90deg) translateX(-100%); -ms-transform: rotate(-90deg) translateX(-100%);
} }
.main-nav-img.main-nav-sub-img { .main-nav-img.main-nav-sub-img {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin: 0px 0px 0 0; margin: 0px 0px 0 0;
transform: rotate(180deg) translateX(0%); transform: rotate(180deg) translateX(0%);
-webkit-transform: rotate(180deg) translateX(0%); -webkit-transform: rotate(180deg) translateX(0%);
-ms-transform: rotate(180deg) translateX(0%); -ms-transform: rotate(180deg) translateX(0%);
position: absolute; position: absolute;
right: -8px; right: -8px;
top: 16px; top: 16px;
} }
ul.nav { ul.nav {
margin: 0 auto; margin: 0 auto;
margin-top: 0px; margin-top: 0px;
margin-left: 0px; margin-left: 0px;
} }
.mini ul.nav li { .mini ul.nav li {
float: right; float: right;
line-height: 25px; line-height: 25px;
height: auto; height: auto;
margin-top: 3px; margin-top: 3px;
} }
.spancolchildstyle { .spancolchildstyle {
padding: 4px; padding: 4px;
} }
.contextmenubutton { .contextmenubutton {
float: right; float: right;
display: none; display: none;
} }
.highlight:hover > .contextmenubutton { .highlight:hover>.contextmenubutton {
display: unset; display: unset;
} }
.highlight:hover > .contextmenubutton::after { .highlight:hover>.contextmenubutton::after {
content: "\2026"; content: "\2026";
font-size: 12px; font-size: 12px;
} }
.showEllipsis { .showEllipsis {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }

4670
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,15 +42,14 @@
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3", "@phosphor/widgets": "1.9.3",
"@testing-library/jest-dom": "5.11.9", "@testing-library/jest-dom": "5.11.9",
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
"clean-webpack-plugin": "3.0.0", "clean-webpack-plugin": "0.1.19",
"clipboard-copy": "4.0.1", "clipboard-copy": "4.0.1",
"copy-webpack-plugin": "9.0.1", "copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"d3": "6.1.1", "d3": "6.1.1",
@@ -81,10 +80,10 @@
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42", "post-robot": "10.0.42",
"q": "1.5.1", "q": "1.5.1",
"react": "16.14.0", "react": "16.13.1",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
"react-dnd": "14.0.2", "react-dnd": "9.4.0",
"react-dnd-html5-backend": "14.0.0", "react-dnd-html5-backend": "9.4.0",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "11.8.5", "react-i18next": "11.8.5",
@@ -98,7 +97,7 @@
"sanitize-html": "2.3.3", "sanitize-html": "2.3.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "5.1.4", "terser-webpack-plugin": "3.1.0",
"underscore": "1.9.1", "underscore": "1.9.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"zustand": "3.5.0" "zustand": "3.5.0"
@@ -132,7 +131,6 @@
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0", "@typescript-eslint/parser": "4.22.0",
"@webpack-cli/serve": "1.5.2",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"buffer": "5.1.0", "buffer": "5.1.0",
@@ -154,45 +152,44 @@
"html-inline-css-webpack-plugin": "1.11.0", "html-inline-css-webpack-plugin": "1.11.0",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "5.3.2", "html-webpack-plugin": "4.5.2",
"jest": "26.6.3", "jest": "25.5.4",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.1.0",
"jest-playwright-preset": "1.5.1", "jest-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0", "mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"playwright": "1.13.0", "playwright": "1.10.0",
"prettier": "2.2.1", "prettier": "2.2.1",
"process": "0.11.10",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4", "react-dev-utils": "11.0.4",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "9.2.4", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",
"typedoc": "0.20.36", "typedoc": "0.20.36",
"typescript": "4.3.4", "typescript": "4.3.4",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "5.47.0", "webpack": "4.46.0",
"webpack-bundle-analyzer": "4.4.2", "webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "4.7.2", "webpack-cli": "3.3.10",
"webpack-dev-server": "3.11.2" "webpack-dev-server": "3.11.0"
}, },
"scripts": { "scripts": {
"start": "webpack serve --mode development", "start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build", "dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci", "build:dataExplorer:ci": "npm run build:ci",
"build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers", "build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
"build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast", "build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
"pack:prod": "webpack --mode production", "pack:prod": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode production",
"pack:fast": "webpack --mode development --progress", "pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
"copyToConsumers": "node copyToConsumers", "copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest", "test": "rimraf coverage && jest",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles", "test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",

View File

@@ -95,7 +95,6 @@ export class Flights {
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -350,11 +349,6 @@ export class Notebook {
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 120000;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -1,5 +1,7 @@
jest.mock("./MessageHandler"); jest.mock("./MessageHandler");
import { LogEntryLevel } from "../Contracts/Diagnostics";
import * as Logger from "./Logger"; import * as Logger from "./Logger";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
describe("Logger", () => { describe("Logger", () => {

View File

@@ -6,6 +6,7 @@ import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
@@ -141,7 +142,8 @@ export function readDocument(
: "", : "",
}; };
const endpoint = getEndpoint(); const endpoint = getFeatureEndpointOrDefault("readDocument");
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
method: "GET", method: "GET",
@@ -342,6 +344,10 @@ export function getEndpoint(): string {
return url; return url;
} }
function getFeatureEndpointOrDefault(feature: string): string {
return (hasFlag("mongoProxyFeatures", feature)) ? userContext.features.mongoProxyEndpoint : getEndpoint();
}
// TODO: This function throws most of the time except on Forbidden which is a bit strange // TODO: This function throws most of the time except on Forbidden which is a bit strange
// It causes problems for TypeScript understanding the types // It causes problems for TypeScript understanding the types
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> { async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {

View File

@@ -2,22 +2,17 @@ import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg"; import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export interface ResourceTreeContainerProps { export interface ResourceTreeProps {
toggleLeftPaneExpanded: () => void; toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean; isLeftPaneExpanded: boolean;
container: Explorer;
} }
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({ export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
toggleLeftPaneExpanded, toggleLeftPaneExpanded,
isLeftPaneExpanded, isLeftPaneExpanded,
container, }: ResourceTreeProps): JSX.Element => {
}: ResourceTreeContainerProps): JSX.Element => {
return ( return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}> <div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */} {/* Collections Window - - Start */}
@@ -53,11 +48,9 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
</div> </div>
</div> </div>
{userContext.authType === AuthType.ResourceToken ? ( {userContext.authType === AuthType.ResourceToken ? (
<ResourceTokenTree /> <div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : ( ) : (
<ResourceTree container={container} /> <div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
)} )}
</div> </div>
{/* Collections Window - End */} {/* Collections Window - End */}

View File

@@ -1,10 +1,7 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
import ko from "knockout";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { Database } from "../../Contracts/ViewModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import { armRequest } from "../../Utils/arm/request"; import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@@ -26,15 +23,6 @@ describe("createCollection", () => {
} as DatabaseAccount, } as DatabaseAccount,
apiType: "SQL", apiType: "SQL",
}); });
useDatabases.setState({
databases: [
{
id: ko.observable("testDatabase"),
loadCollections: () => undefined,
collections: ko.observableArray([]),
} as Database,
],
});
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {

View File

@@ -4,16 +4,20 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils"; import {
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; createUpdateCassandraTable,
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; getCassandraTable,
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import {
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@@ -55,16 +59,6 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
}; };
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
if (!params.createNewDatabase) {
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
if (!isValid) {
const collectionName = getCollectionName().toLocaleLowerCase();
throw new Error(
`Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists`
);
}
}
const { apiType } = userContext; const { apiType } = userContext;
switch (apiType) { switch (apiType) {
case "SQL": case "SQL":
@@ -83,6 +77,23 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams
}; };
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.SqlContainerResource = { const resource: ARMTypes.SqlContainerResource = {
id: params.collectionId, id: params.collectionId,
@@ -120,6 +131,23 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }]; const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }];
try {
const getResponse = await getMongoDBCollection(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.MongoDBCollectionResource = { const resource: ARMTypes.MongoDBCollectionResource = {
id: params.collectionId, id: params.collectionId,
@@ -161,6 +189,23 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
}; };
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getCassandraTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.CassandraTableResource = { const resource: ARMTypes.CassandraTableResource = {
id: params.collectionId, id: params.collectionId,
@@ -188,6 +233,23 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams):
}; };
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getGremlinGraph(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.GremlinGraphResource = { const resource: ARMTypes.GremlinGraphResource = {
id: params.collectionId, id: params.collectionId,
@@ -222,6 +284,22 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise<D
}; };
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.TableResource = { const resource: ARMTypes.TableResource = {
id: params.collectionId, id: params.collectionId,

View File

@@ -2,13 +2,20 @@ import { DatabaseResponse } from "@azure/cosmos";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getDatabaseName } from "../../Utils/APITypeUtils"; import {
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; createUpdateCassandraKeyspace,
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; getCassandraKeyspace,
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import {
createUpdateGremlinDatabase,
getGremlinDatabase,
} from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import {
createUpdateMongoDBDatabase,
getMongoDBDatabase,
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { import {
CassandraKeyspaceCreateUpdateParameters, CassandraKeyspaceCreateUpdateParameters,
CreateUpdateOptions, CreateUpdateOptions,
@@ -41,11 +48,6 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
} }
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
const databaseName = getDatabaseName().toLocaleLowerCase();
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
}
const { apiType } = userContext; const { apiType } = userContext;
switch (apiType) { switch (apiType) {
@@ -63,6 +65,22 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P
} }
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = { const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: { properties: {
@@ -83,6 +101,22 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
} }
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getMongoDBDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: MongoDBDatabaseCreateUpdateParameters = { const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
properties: { properties: {
@@ -103,6 +137,22 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
} }
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: CassandraKeyspaceCreateUpdateParameters = { const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
properties: { properties: {
@@ -123,6 +173,22 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
} }
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getGremlinDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: GremlinDatabaseCreateUpdateParameters = { const rpPayload: GremlinDatabaseCreateUpdateParameters = {
properties: { properties: {

View File

@@ -22,8 +22,8 @@ describe("The Heatmap Control", () => {
}; };
let heatmap: Heatmap; let heatmap: Heatmap;
const theme: PortalTheme = 1; let theme: PortalTheme = 1;
const divElement = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
describe("drawHeatmap rendering", () => { describe("drawHeatmap rendering", () => {
beforeEach(() => { beforeEach(() => {
@@ -100,7 +100,7 @@ describe("iframe rendering when there is no data", () => {
}); });
it("should show a no data message with a dark theme", () => { it("should show a no data message with a dark theme", () => {
const data = { let data = {
data: { data: {
signature: "pcIframe", signature: "pcIframe",
data: { data: {
@@ -111,7 +111,7 @@ describe("iframe rendering when there is no data", () => {
}, },
}; };
const divElement = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement; document.body.innerHTML = divElement;
handleMessage(data as MessageEvent); handleMessage(data as MessageEvent);
@@ -120,7 +120,7 @@ describe("iframe rendering when there is no data", () => {
}); });
it("should show a no data message with a white theme", () => { it("should show a no data message with a white theme", () => {
const data = { let data = {
data: { data: {
signature: "pcIframe", signature: "pcIframe",
data: { data: {
@@ -131,7 +131,7 @@ describe("iframe rendering when there is no data", () => {
}, },
}; };
const divElement = `<div id="${Heatmap.elementId}"></div>`; const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement; document.body.innerHTML = divElement;
handleMessage(data as MessageEvent); handleMessage(data as MessageEvent);

View File

@@ -39,7 +39,7 @@ export class Heatmap {
} }
} }
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings { private _getFontStyles(size: number = StyleConstants.MediumFontSize, color: string = "#838383"): FontSettings {
return { return {
family: StyleConstants.DataExplorerFont, family: StyleConstants.DataExplorerFont,
size, size,
@@ -78,9 +78,9 @@ export class Heatmap {
// go thru all rows and create 2d matrix for heatmap... // go thru all rows and create 2d matrix for heatmap...
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
output.yAxisPoints.push(rows[i]); output.yAxisPoints.push(rows[i]);
const dataPoints: number[] = []; let dataPoints: number[] = [];
for (let a = 0; a < output.xAxisPoints.length; a++) { for (let a = 0; a < output.xAxisPoints.length; a++) {
const row: PartitionTimeStampToData = data[rows[i]]; let row: PartitionTimeStampToData = data[rows[i]];
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]); dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
} }
output.dataPoints.push(dataPoints); output.dataPoints.push(dataPoints);
@@ -193,7 +193,7 @@ export class Heatmap {
this._getLayoutSettings(), this._getLayoutSettings(),
this._getChartDisplaySettings() this._getChartDisplaySettings()
); );
const plotDiv: any = document.getElementById(Heatmap.elementId); let plotDiv: any = document.getElementById(Heatmap.elementId);
plotDiv.on("plotly_click", (data: any) => { plotDiv.on("plotly_click", (data: any) => {
let timeSelected: string = data.points[0].x; let timeSelected: string = data.points[0].x;
timeSelected = timeSelected.replace(" ", "T"); timeSelected = timeSelected.replace(" ", "T");
@@ -205,7 +205,7 @@ export class Heatmap {
break; break;
} }
} }
const output = []; let output = [];
for (let i = 0; i < this._chartData.dataPoints.length; i++) { for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]); output.push(this._chartData.dataPoints[i][xAxisIndex]);
} }

View File

@@ -83,7 +83,6 @@ export const createCollectionContextMenuButton = (
items.push({ items.push({
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {

View File

@@ -23,75 +23,13 @@ export interface DialogState {
dialogProps?: DialogProps; dialogProps?: DialogProps;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
closeDialog: () => void; closeDialog: () => void;
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
) => void;
showOkModalDialog: (title: string, subText: string) => void;
} }
export const useDialog: UseStore<DialogState> = create((set, get) => ({ export const useDialog: UseStore<DialogState> = create((set) => ({
visible: false, visible: false,
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
closeDialog: () => closeDialog: () =>
set( set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true),
(state) => ({
visible: false,
openDialog: state.openDialog,
closeDialog: state.closeDialog,
showOkCancelModalDialog: state.showOkCancelModalDialog,
showOkModalDialog: state.showOkModalDialog,
}),
true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above
),
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
get().closeDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
get().closeDialog();
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
}),
showOkModalDialog: (title: string, subText: string): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
get().closeDialog();
},
onSecondaryButtonClick: undefined,
}),
})); }));
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {

View File

@@ -5,9 +5,6 @@
display: inline-block; display: inline-block;
width: 100%; width: 100%;
.input-type-head-text-field {
width: 100%;
}
textarea { textarea {
width: 100%; width: 100%;
line-height: 1; line-height: 1;
@@ -24,11 +21,4 @@
} }
} }
} }
.input-typeahead-chocies-container {
border: 1px solid lightgrey;
padding: 5px 10px 5px 10px;
cursor: pointer;
.choice-caption{
font-size: 14px;
}
}

View File

@@ -6,13 +6,14 @@
* typeaheadOverrideOptions: { dynamic:false } * typeaheadOverrideOptions: { dynamic:false }
* *
*/ */
import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react"; import "jquery-typeahead";
import * as React from "react"; import * as React from "react";
import { KeyCodes } from "../../../Common/Constants";
import "./InputTypeahead.less"; import "./InputTypeahead.less";
export interface Item { export interface Item {
caption: string; caption: string;
value: string; value: any;
} }
/** /**
@@ -74,125 +75,170 @@ export interface InputTypeaheadComponentProps {
useTextarea?: boolean; useTextarea?: boolean;
} }
interface InputTypeaheadComponentState { interface OnClickItem {
isSuggestionVisible: boolean; matchedKey: string;
selectedChoice: Item; value: any;
filteredChoices: Item[]; caption: string;
group: string;
} }
interface Cache {
inputValue: string;
selection: Item;
}
interface InputTypeaheadComponentState {}
export class InputTypeaheadComponent extends React.Component< export class InputTypeaheadComponent extends React.Component<
InputTypeaheadComponentProps, InputTypeaheadComponentProps,
InputTypeaheadComponentState InputTypeaheadComponentState
> { > {
constructor(props: InputTypeaheadComponentProps) { private inputElt: HTMLElement;
private containerElt: HTMLElement;
private cache: Cache;
private inputValue: string;
private selection: Item;
public constructor(props: InputTypeaheadComponentProps) {
super(props); super(props);
this.state = { this.cache = {
isSuggestionVisible: false, inputValue: null,
filteredChoices: [], selection: null,
selectedChoice: {
caption: "",
value: "",
},
}; };
} }
private onRenderCell = (item: Item): JSX.Element => { /**
return ( * Props have changed
<div className="input-typeahead-chocies-container" onClick={() => this.onChoiceClick(item)}> * @param prevProps
<p className="choice-caption">{item.caption}</p> * @param prevState
<span>{item.value}</span> * @param snapshot
</div> */
); public componentDidUpdate(
}; prevProps: InputTypeaheadComponentProps,
prevState: InputTypeaheadComponentState,
private onChoiceClick = (item: Item): void => { snapshot: any
this.props.onNewValue(item.caption); ): void {
this.setState({ isSuggestionVisible: false, selectedChoice: item }); if (prevProps.defaultValue !== this.props.defaultValue) {
}; $(this.inputElt).val(this.props.defaultValue);
this.initializeTypeahead();
private handleChange = (value: string): void => {
if (!value) {
this.setState({ isSuggestionVisible: true });
} }
this.props.onNewValue(value); }
const filteredChoices = this.filterChoiceByValue(this.props.choices, value);
this.setState({ filteredChoices });
};
private onSubmit = (event: React.KeyboardEvent<HTMLElement>): void => { /**
if (event.key === "Enter") { * Executed once react is done building the DOM for this component
*/
public componentDidMount(): void {
this.initializeTypeahead();
}
public render(): JSX.Element {
return (
<span className="input-typeahead-container">
<div
className="input-typehead"
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(event)}
>
<div className="typeahead__container" ref={(input) => (this.containerElt = input)}>
<div className="typeahead__field">
<span className="typeahead__query">
{this.props.useTextarea ? (
<textarea
rows={1}
name="q"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
) : (
<input
name="q"
type="search"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
)}
</span>
{this.props.showSearchButton && (
<span className="typeahead__button">
<button type="submit">
<span className="typeahead__search-icon" />
</button>
</span>
)}
</div>
</div>
</div>
</span>
);
}
private onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
if (event.keyCode === KeyCodes.Enter) {
if (this.props.submitFct) { if (this.props.submitFct) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.props.submitFct(this.props.defaultValue, this.state.selectedChoice); this.props.submitFct(this.cache.inputValue, this.cache.selection);
this.setState({ isSuggestionVisible: false }); $(this.containerElt).children(".typeahead__result").hide();
} }
} }
}; }
private filterChoiceByValue = (choices: Item[], searchKeyword: string): Item[] => { /**
return choices.filter((choice) => * Must execute once ko is rendered, so that it can find the input element by id
// @ts-ignore */
Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase())) private initializeTypeahead(): void {
); const props = this.props;
}; let cache = this.cache;
let options: any = {
public render(): JSX.Element { input: this.inputElt,
const { defaultValue, useTextarea, placeholder, onNewValue } = this.props; order: "asc",
const { isSuggestionVisible, selectedChoice, filteredChoices } = this.state; minLength: 0,
const theme = getTheme(); searchOnFocus: true,
source: {
const iconButtonStyles = { display: "caption",
root: { data: () => {
color: theme.palette.neutralPrimary, return props.choices;
marginLeft: "10px !important", },
marginTop: "0px",
marginRight: "2px",
width: "42px",
}, },
rootHovered: { callback: {
color: theme.palette.neutralDark, onClick: (node: any, a: any, item: OnClickItem, event: any) => {
cache.selection = item;
if (props.onSelected) {
props.onSelected(item);
}
},
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
cache.inputValue = query;
if (props.onNewValue) {
props.onNewValue(query);
}
},
}, },
template: (query: string, item: any) => {
// Don't display id if caption *IS* the id
return item.caption === item.value
? "<span>{{caption}}</span>"
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
},
dynamic: true,
}; };
const cancelIcon: IIconProps = { iconName: "cancel" };
const searchIcon: IIconProps = { iconName: "Search" };
return ( // Override options
<div className="input-typeahead-container"> if (props.typeaheadOverrideOptions) {
<Stack horizontal> for (const p in props.typeaheadOverrideOptions) {
<TextField options[p] = props.typeaheadOverrideOptions[p];
multiline={useTextarea} }
rows={1} }
defaultValue={defaultValue}
ariaLabel="Input query" if (props.hasOwnProperty("showCancelButton")) {
placeholder={placeholder} options.cancelButton = props.showCancelButton;
className="input-type-head-text-field" }
value={defaultValue}
onKeyDown={this.onSubmit} $(this.inputElt).typeahead(options);
onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
/>
{this.props.showCancelButton && (
<IconButton
styles={iconButtonStyles}
iconProps={cancelIcon}
ariaLabel="cancel Button"
onClick={() => onNewValue("")}
/>
)}
{this.props.showSearchButton && (
<IconButton
styles={iconButtonStyles}
iconProps={searchIcon}
ariaLabel="Search Button"
onClick={() => this.props.submitFct(defaultValue, selectedChoice)}
/>
)}
</Stack>
{filteredChoices.length && isSuggestionVisible ? (
<List items={filteredChoices} onRenderCell={this.onRenderCell} />
) : undefined}
</div>
);
} }
} }

View File

@@ -1,43 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`inputTypeahead renders <input /> 1`] = ` exports[`inputTypeahead renders <input /> 1`] = `
<div <span
className="input-typeahead-container" className="input-typeahead-container"
> >
<Stack <div
horizontal={true} className="input-typehead"
onKeyDown={[Function]}
> >
<StyledTextFieldBase <div
ariaLabel="Input query" className="typeahead__container"
className="input-type-head-text-field" >
multiline={false} <div
onChange={[Function]} className="typeahead__field"
onFocus={[Function]} >
onKeyDown={[Function]} <span
placeholder="placeholder" className="typeahead__query"
rows={1} >
/> <input
</Stack> aria-label="Input query"
</div> autoComplete="off"
name="q"
type="search"
/>
</span>
</div>
</div>
</div>
</span>
`; `;
exports[`inputTypeahead renders <textarea /> 1`] = ` exports[`inputTypeahead renders <textarea /> 1`] = `
<div <span
className="input-typeahead-container" className="input-typeahead-container"
> >
<Stack <div
horizontal={true} className="input-typehead"
onKeyDown={[Function]}
> >
<StyledTextFieldBase <div
ariaLabel="Input query" className="typeahead__container"
className="input-type-head-text-field" >
multiline={true} <div
onChange={[Function]} className="typeahead__field"
onFocus={[Function]} >
onKeyDown={[Function]} <span
placeholder="placeholder" className="typeahead__query"
rows={1} >
/> <textarea
</Stack> aria-label="Input query"
</div> autoComplete="off"
name="q"
rows={1}
/>
</span>
</div>
</div>
</div>
</span>
`; `;

View File

@@ -29,7 +29,6 @@ import { QueriesClient } from "../../../Common/QueriesClient";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Dialog";
const title = "Open Saved Queries"; const title = "Open Saved Queries";
@@ -223,42 +222,35 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
key: "Delete", key: "Delete",
text: "Delete query", text: "Delete query",
onClick: async () => { onClick: async () => {
useDialog.getState().showOkCancelModalDialog( if (window.confirm("Are you sure you want to delete this query?")) {
"Confirm delete", const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
"Are you sure you want to delete this query?", dataExplorerArea: Constants.Areas.ContextualPane,
"Delete", paneTitle: title,
async () => { });
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { try {
dataExplorerArea: Constants.Areas.ContextualPane, await this.props.queriesClient.deleteQuery(query);
paneTitle: title, TelemetryProcessor.traceSuccess(
}); Action.DeleteSavedQuery,
try { {
await this.props.queriesClient.deleteQuery(query); dataExplorerArea: Constants.Areas.ContextualPane,
TelemetryProcessor.traceSuccess( paneTitle: title,
Action.DeleteSavedQuery, },
{ startKey
dataExplorerArea: Constants.Areas.ContextualPane, );
paneTitle: title, } catch (error) {
}, TelemetryProcessor.traceFailure(
startKey Action.DeleteSavedQuery,
); {
} catch (error) { dataExplorerArea: Constants.Areas.ContextualPane,
TelemetryProcessor.traceFailure( paneTitle: title,
Action.DeleteSavedQuery, error: getErrorMessage(error),
{ errorStack: getErrorStack(error),
dataExplorerArea: Constants.Areas.ContextualPane, },
paneTitle: title, startKey
error: getErrorMessage(error), );
errorStack: getErrorStack(error), }
}, await this.fetchSavedQueries(); // get latest state
startKey }
);
}
await this.fetchSavedQueries(); // get latest state
},
"Cancel",
undefined
);
}, },
}, },
], ],

View File

@@ -42,6 +42,19 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
}, },
@@ -109,6 +122,19 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
}, },

View File

@@ -17,6 +17,7 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]), collections: ko.observableArray<Collection>([collection]),
} as Database; } as Database;
const explorer = {} as Explorer; const explorer = {} as Explorer;
explorer.showOkModalDialog = () => {};
useDatabases.getState().addDatabases([database]); useDatabases.getState().addDatabases([database]);
const dataSamplesUtil = new DataSamplesUtil(explorer); const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,7 +1,6 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
@@ -21,7 +20,7 @@ export class DataSamplesUtil {
const containerName = generator.getCollectionId(); const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg); logConsoleError(msg);
return; return;
} }
@@ -30,7 +29,7 @@ export class DataSamplesUtil {
.createSampleContainerAsync() .createSampleContainerAsync()
.catch((error) => logConsoleError(`Error creating sample container: ${error}`)); .catch((error) => logConsoleError(`Error creating sample container: ${error}`));
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleInfo(msg); logConsoleInfo(msg);
} }

View File

@@ -1,3 +1,4 @@
import { IChoiceGroupProps } from "@fluentui/react";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
@@ -34,7 +35,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog"; import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import * as FileSystemUtil from "./Notebook/FileSystemUtil";
@@ -58,6 +59,7 @@ import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import { useDatabases } from "./useDatabases"; import { useDatabases } from "./useDatabases";
import { useSelectedNode } from "./useSelectedNode"; import { useSelectedNode } from "./useSelectedNode";
@@ -72,6 +74,9 @@ export default class Explorer {
// Resource Tree // Resource Tree
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
// Resource Token
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
// Tabs // Tabs
public isTabsContentExpanded: ko.Observable<boolean>; public isTabsContentExpanded: ko.Observable<boolean>;
@@ -181,6 +186,7 @@ export default class Explorer {
); );
this.resourceTree = new ResourceTreeAdapter(this); this.resourceTree = new ResourceTreeAdapter(this);
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
// Override notebook server parameters from URL parameters // Override notebook server parameters from URL parameters
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) { if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
@@ -356,9 +362,6 @@ export default class Explorer {
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken, authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
}); });
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.refreshNotebookList(); this.refreshNotebookList();
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
@@ -537,22 +540,17 @@ export default class Explorer {
} }
} }
public uploadFile( public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
name: string,
content: string,
parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled"; const error = "Attempt to upload notebook, but notebook is not enabled";
handleError(error, "Explorer/uploadFile"); handleError(error, "Explorer/uploadFile");
throw new Error(error); throw new Error(error);
} }
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
promise promise
.then(() => this.resourceTree.triggerRender()) .then(() => this.resourceTree.triggerRender())
.catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); .catch((reason) => this.showOkModalDialog("Unable to upload file", reason));
return promise; return promise;
} }
@@ -618,6 +616,51 @@ export default class Explorer {
this.notebookManager?.openCopyNotebookPane(name, content); this.notebookManager?.openCopyNotebookPane(name, content);
} }
public showOkModalDialog(title: string, msg: string): void {
useDialog.getState().openDialog({
isModal: true,
title,
subText: msg,
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
useDialog.getState().closeDialog();
},
onSecondaryButtonClick: undefined,
});
}
public showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean
): void {
useDialog.getState().openDialog({
isModal: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
useDialog.getState().closeDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
useDialog.getState().closeDialog();
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled: isPrimaryButtonDisabled,
});
}
/** /**
* Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree.
* Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder.
@@ -677,7 +720,7 @@ export default class Explorer {
return true; return true;
} }
public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { public renameNotebook(notebookFile: NotebookContentItem): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled"; const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook"); handleError(error, "Explorer/renameNotebook");
@@ -691,9 +734,7 @@ export default class Explorer {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
}); });
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
useDialog this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
.getState()
.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else { } else {
useSidePanel.getState().openSidePanel( useSidePanel.getState().openSidePanel(
"Rename Notebook", "Rename Notebook",
@@ -710,7 +751,7 @@ export default class Explorer {
paneTitle="Rename Notebook" paneTitle="Rename Notebook"
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> => onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
} }
notebookFile={notebookFile} notebookFile={notebookFile}
/> />
@@ -718,7 +759,7 @@ export default class Explorer {
} }
} }
public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { public onCreateDirectory(parent: NotebookContentItem): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled"; const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory"); handleError(error, "Explorer/onCreateDirectory");
@@ -740,7 +781,7 @@ export default class Explorer {
submitButtonLabel="Create" submitButtonLabel="Create"
defaultInput="" defaultInput=""
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> => onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input)
} }
notebookFile={parent} notebookFile={parent}
/> />
@@ -801,15 +842,13 @@ export default class Explorer {
} }
await this.resourceTree.initialize(); await this.resourceTree.initialize();
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.notebookManager?.refreshPinnedRepos(); this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) { if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
} }
}; };
public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> { public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to delete notebook file, but notebook is not enabled"; const error = "Attempt to delete notebook file, but notebook is not enabled";
handleError(error, "Explorer/deleteNotebookFile"); handleError(error, "Explorer/deleteNotebookFile");
@@ -823,9 +862,7 @@ export default class Explorer {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
}); });
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
useDialog this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
.getState()
.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
return Promise.reject(); return Promise.reject();
} }
@@ -842,7 +879,7 @@ export default class Explorer {
return Promise.reject(); return Promise.reject();
} }
return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
() => logConsoleInfo(`Successfully deleted: ${item.path}`), () => logConsoleInfo(`Successfully deleted: ${item.path}`),
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`) (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
); );
@@ -851,7 +888,7 @@ export default class Explorer {
/** /**
* This creates a new notebook file, then opens the notebook * This creates a new notebook file, then opens the notebook
*/ */
public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void { public onNewNotebookClicked(parent?: NotebookContentItem): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled"; const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked"); handleError(error, "Explorer/onNewNotebookClicked");
@@ -866,7 +903,7 @@ export default class Explorer {
}); });
this.notebookManager?.notebookContentClient this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent, isGithubTree) .createNewNotebookFile(parent)
.then((newFile: NotebookContentItem) => { .then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`); logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -895,15 +932,14 @@ export default class Explorer {
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
} }
// TODO: Delete this function when ResourceTreeAdapter is removed. public refreshContentItem(item: NotebookContentItem): Promise<void> {
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled"; const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem"); handleError(error, "Explorer/refreshContentItem");
return Promise.reject(new Error(error)); return Promise.reject(new Error(error));
} }
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); return this.notebookManager?.notebookContentClient.updateItemChildren(item);
} }
public openNotebookTerminal(kind: ViewModels.TerminalKind) { public openNotebookTerminal(kind: ViewModels.TerminalKind) {
@@ -1084,13 +1120,11 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
if (!userContext.features.notebooksTemporarilyDown) { if (isNotebookEnabled) {
if (isNotebookEnabled) { await this.initNotebooks(userContext.databaseAccount);
await this.initNotebooks(userContext.databaseAccount); } else if (this.notebookToImport) {
} else if (this.notebookToImport) { // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane this._openSetupNotebooksPaneForQuickstart();
this._openSetupNotebooksPaneForQuickstart();
}
} }
} }
} }

View File

@@ -1,11 +1,11 @@
import { GraphData, GremlinEdge, GremlinVertex } from "./GraphData"; import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData";
describe("Graph Data", () => { describe("Graph Data", () => {
it("should set only one node as root", () => { it("should set only one node as root", () => {
const graphData = new GraphData<GremlinVertex, GremlinEdge>(); const graphData = new GraphData<GremlinVertex, GremlinEdge>();
const v1: GremlinVertex = { id: "1", label: undefined }; const v1: GremlinVertex = { id: "1", label: null };
const v2: GremlinVertex = { id: "2", label: undefined }; const v2: GremlinVertex = { id: "2", label: null };
const v3: GremlinVertex = { id: "3", label: undefined }; const v3: GremlinVertex = { id: "3", label: null };
v3._isRoot = true; v3._isRoot = true;
graphData.addVertex(v1); graphData.addVertex(v1);
@@ -28,9 +28,9 @@ describe("Graph Data", () => {
it("should properly find root id", () => { it("should properly find root id", () => {
const graphData = new GraphData(); const graphData = new GraphData();
const v1: GremlinVertex = { id: "1", label: undefined }; const v1: GremlinVertex = { id: "1", label: null };
const v2: GremlinVertex = { id: "2", label: undefined }; const v2: GremlinVertex = { id: "2", label: null };
const v3: GremlinVertex = { id: "3", label: undefined }; const v3: GremlinVertex = { id: "3", label: null };
graphData.addVertex(v1); graphData.addVertex(v1);
graphData.addVertex(v2); graphData.addVertex(v2);
@@ -44,12 +44,12 @@ describe("Graph Data", () => {
it("should remove edge from graph", () => { it("should remove edge from graph", () => {
const graphData = new GraphData(); const graphData = new GraphData();
graphData.addVertex({ id: "v1", label: undefined }); graphData.addVertex({ id: "v1", label: null });
graphData.addVertex({ id: "v2", label: undefined }); graphData.addVertex({ id: "v2", label: null });
graphData.addVertex({ id: "v3", label: undefined }); graphData.addVertex({ id: "v3", label: null });
graphData.addEdge({ id: "e1", inV: "v1", outV: "v2", label: "" }); graphData.addEdge({ id: "e1", inV: "v1", outV: "v2", label: null });
graphData.addEdge({ id: "e2", inV: "v1", outV: "v3", label: "" }); graphData.addEdge({ id: "e2", inV: "v1", outV: "v3", label: null });
// in edge // in edge
graphData.removeEdge("e1", false); graphData.removeEdge("e1", false);

View File

@@ -1,5 +1,5 @@
import { SimulationLinkDatum, SimulationNodeDatum } from "d3";
import _ from "underscore"; import _ from "underscore";
import { SimulationNodeDatum, SimulationLinkDatum } from "d3";
export interface PaginationInfo { export interface PaginationInfo {
total: number; total: number;
@@ -10,7 +10,7 @@ export interface PaginationInfo {
export interface GremlinVertex { export interface GremlinVertex {
id: string; id: string;
label?: string; label: string;
inE?: { [label: string]: GremlinShortInEdge[] }; inE?: { [label: string]: GremlinShortInEdge[] };
outE?: { [label: string]: GremlinShortOutEdge[] }; outE?: { [label: string]: GremlinShortOutEdge[] };
properties?: { [propName: string]: GremlinProperty[] }; properties?: { [propName: string]: GremlinProperty[] };

View File

@@ -6,9 +6,9 @@
import * as React from "react"; import * as React from "react";
import CancelIcon from "../../../../images/cancel.svg"; import CancelIcon from "../../../../images/cancel.svg";
import CheckIcon from "../../../../images/check-1.svg"; import CheckIcon from "../../../../images/check.svg";
import DeleteIcon from "../../../../images/delete.svg"; import DeleteIcon from "../../../../images/delete.svg";
import EditIcon from "../../../../images/edit-1.svg"; import EditIcon from "../../../../images/edit.svg";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel"; import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";

View File

@@ -103,25 +103,19 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
//TODO: modify once notebooks are available expect(enableNotebookBtn.disabled).toBe(false);
expect(enableNotebookBtn).toBeUndefined(); expect(enableNotebookBtn.tooltipText).toBe("");
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(false);
//expect(enableNotebookBtn.tooltipText).toBe("");
}); });
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
//TODO: modify once notebooks are available expect(enableNotebookBtn.disabled).toBe(true);
expect(enableNotebookBtn).toBeUndefined(); expect(enableNotebookBtn.tooltipText).toBe(
//expect(enableNotebookBtn).toBeDefined(); "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//expect(enableNotebookBtn.disabled).toBe(true); );
//expect(enableNotebookBtn.tooltipText).toBe(
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//);
}); });
}); });
@@ -198,11 +192,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openMongoShellBtn.tooltipText).toBe("");
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
@@ -212,11 +203,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openMongoShellBtn.tooltipText).toBe("");
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
@@ -302,13 +290,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openCassandraShellBtn.tooltipText).toBe("");
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
@@ -318,11 +302,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openCassandraShellBtn.tooltipText).toBe("");
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
}); });

View File

@@ -67,50 +67,35 @@ export function createStaticCommandBarButtons(
newCollectionBtn.children.push(newDatabaseBtn); newCollectionBtn.children.push(newDatabaseBtn);
} }
if (useNotebook.getState().isNotebookEnabled) { buttons.push(createDivider());
buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = [];
if (useNotebook.getState().isNotebookEnabled) {
const newNotebookButton = createNewNotebookButton(container); const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)]; newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
notebookButtons.push(newNotebookButton); buttons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) { if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container)); buttons.push(createManageGitHubAccountButton(container));
} }
notebookButtons.push(createOpenTerminalButton(container)); buttons.push(createOpenTerminalButton(container));
notebookButtons.push(createNotebookWorkspaceResetButton(container)); buttons.push(createNotebookWorkspaceResetButton(container));
if ( if (
(userContext.apiType === "Mongo" && (userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled && useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) || selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra" userContext.apiType === "Cassandra"
) { ) {
notebookButtons.push(createDivider()); buttons.push(createDivider());
if (userContext.apiType === "Cassandra") { if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenCassandraTerminalButton(container)); buttons.push(createOpenCassandraTerminalButton(container));
} else { } else {
notebookButtons.push(createOpenMongoTerminalButton(container)); buttons.push(createOpenMongoTerminalButton(container));
} }
} }
notebookButtons.forEach((btn) => {
if (userContext.features.notebooksTemporarilyDown) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
} else {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
}
buttons.push(btn);
});
} else { } else {
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) { if (!isRunningOnNationalCloud()) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container)); buttons.push(createEnableNotebooksButton(container));
} }
} }
@@ -167,9 +152,7 @@ export function createContextCommandBarButtons(
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {
if (!userContext.features.notebooksTemporarilyDown) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
@@ -177,13 +160,7 @@ export function createContextCommandBarButtons(
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
tooltipText: disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
}; };
buttons.push(newMongoShellBtn); buttons.push(newMongoShellBtn);
} }
@@ -411,13 +388,6 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
return buttons; return buttons;
} }
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps { function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook"; const label = "New Notebook";
return { return {

View File

@@ -22,13 +22,6 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => { export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight; const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
const getFilter = (isDisabled: boolean): string => {
if (isDisabled) {
return StyleConstants.GrayScale;
}
return undefined;
};
return btns return btns
.filter((btn) => btn) .filter((btn) => btn)
.map( .map(
@@ -44,7 +37,6 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
style: { style: {
width: StyleConstants.CommandBarIconWidth, // 16 width: StyleConstants.CommandBarIconWidth, // 16
alignSelf: btn.iconName ? "baseline" : undefined, alignSelf: btn.iconName ? "baseline" : undefined,
filter: getFilter(btn.disabled),
}, },
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined, imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName, iconName: btn.iconName,
@@ -131,12 +123,8 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
width: 12, width: 12,
paddingLeft: 1, paddingLeft: 1,
paddingTop: 6, paddingTop: 6,
filter: getFilter(btn.disabled),
},
imageProps: {
src: ChevronDownIcon,
alt: btn.iconAlt,
}, },
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt },
}; };
} }

View File

@@ -6,7 +6,7 @@ import { Dropdown, IDropdownOption } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import AnimateHeight from "react-animate-height"; import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear-1.svg"; import ClearIcon from "../../../../images/Clear.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg"; import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";

View File

@@ -277,10 +277,6 @@ export class NotebookComponentBootstrapper {
return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>); return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>);
} }
public isNotebookUntrusted(): boolean {
return NotebookUtil.isNotebookUntrusted(this.getStore().getState(), this.contentRef);
}
/** /**
* For display purposes, only return non-killed kernels * For display purposes, only return non-killed kernels
*/ */

View File

@@ -1,14 +1,12 @@
import { AppState, ContentRef, selectors } from "@nteract/core"; import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps { interface VirtualCommandBarComponentProps {
kernelSpecName: string; kernelSpecName: string;
kernelStatus: string; kernelStatus: string;
currentCellType: string; currentCellType: string;
isNotebookUntrusted: boolean;
onRender: () => void; onRender: () => void;
} }
@@ -22,8 +20,7 @@ class VirtualCommandBarComponent extends React.Component<VirtualCommandBarCompon
return ( return (
this.props.kernelStatus !== nextProps.kernelStatus || this.props.kernelStatus !== nextProps.kernelStatus ||
this.props.kernelSpecName !== nextProps.kernelSpecName || this.props.kernelSpecName !== nextProps.kernelSpecName ||
this.props.currentCellType !== nextProps.currentCellType || this.props.currentCellType !== nextProps.currentCellType
this.props.isNotebookUntrusted !== nextProps.isNotebookUntrusted
); );
} }
@@ -53,7 +50,6 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
} as VirtualCommandBarComponentProps; } as VirtualCommandBarComponentProps;
} }
@@ -73,7 +69,6 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
onRender: initialProps.onRender, onRender: initialProps.onRender,
}; };
}; };

View File

@@ -38,7 +38,6 @@ import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { useDialog } from "../../Controls/Dialog";
import * as FileSystemUtil from "../FileSystemUtil"; import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
@@ -687,8 +686,10 @@ const handleKernelConnectionLostEpic = (
logConsoleError(msg); logConsoleError(msg);
logFailureToTelemetry(state, "Kernel restart error", msg); logFailureToTelemetry(state, "Kernel restart error", msg);
useDialog.getState().showOkModalDialog("kernel restarts", msg); const explorer = window.dataExplorer;
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
return of(EMPTY); return of(EMPTY);
} }
@@ -772,7 +773,8 @@ const closeUnsupportedMimetypesEpic = (
ofType(actions.FETCH_CONTENT_FULFILLED), ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap((action) => { mergeMap((action) => {
const mimetype = action.payload.model.mimetype; const mimetype = action.payload.model.mimetype;
if (!TextFile.handles(mimetype)) { const explorer = window.dataExplorer;
if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
useTabs useTabs
@@ -781,7 +783,7 @@ const closeUnsupportedMimetypesEpic = (
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) (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.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
useDialog.getState().showOkModalDialog("File cannot be rendered", msg); explorer.showOkModalDialog("File cannot be rendered", msg);
logConsoleError(msg); logConsoleError(msg);
} }
return EMPTY; return EMPTY;
@@ -801,16 +803,19 @@ const closeContentFailedToFetchEpic = (
return action$.pipe( return action$.pipe(
ofType(actions.FETCH_CONTENT_FAILED), ofType(actions.FETCH_CONTENT_FAILED),
mergeMap((action) => { mergeMap((action) => {
const filepath = action.payload.filepath; const explorer = window.dataExplorer;
// Close tab and show error message if (explorer) {
useTabs const filepath = action.payload.filepath;
.getState() // Close tab and show error message
.closeTabsByComparator( useTabs
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) .getState()
); .closeTabsByComparator(
const msg = `Failed to load file: ${filepath}.`; (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
useDialog.getState().showOkModalDialog("Failure to load", msg); );
logConsoleError(msg); const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg);
logConsoleError(msg);
}
return EMPTY; return EMPTY;
}) })
); );

View File

@@ -1,6 +1,5 @@
import { stringifyNotebook } from "@nteract/commutable"; import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core"; import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { cloneDeep } from "lodash";
import { AjaxResponse } from "rxjs/ajax"; import { AjaxResponse } from "rxjs/ajax";
import * as StringUtils from "../../Utils/StringUtils"; import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil"; import * as FileSystemUtil from "./FileSystemUtil";
@@ -15,17 +14,7 @@ export class NotebookContentClient {
* This updates the item and points all the children's parent to this item * This updates the item and points all the children's parent to this item
* @param item * @param item
*/ */
public async updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> { public updateItemChildren(item: NotebookContentItem): Promise<void> {
const subItems = await this.fetchNotebookFiles(item.path);
const clonedItem = cloneDeep(item);
subItems.forEach((subItem) => (subItem.parent = clonedItem));
clonedItem.children = subItems;
return clonedItem;
}
// TODO: Delete this function when ResourceTreeAdapter is removed.
public async updateItemChildrenInPlace(item: NotebookContentItem): Promise<void> {
return this.fetchNotebookFiles(item.path).then((subItems) => { return this.fetchNotebookFiles(item.path).then((subItems) => {
item.children = subItems; item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item)); subItems.forEach((subItem) => (subItem.parent = item));
@@ -36,7 +25,7 @@ export class NotebookContentClient {
* *
* @param parent parent folder * @param parent parent folder
*/ */
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> { public createNewNotebookFile(parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
} }
@@ -57,8 +46,6 @@ export class NotebookContentClient {
const notebookFile = xhr.response; const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) { if (parent.children) {
item.parent = parent; item.parent = parent;
parent.children.push(item); parent.children.push(item);
@@ -68,20 +55,18 @@ export class NotebookContentClient {
}); });
} }
public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> { public deleteContentItem(item: NotebookContentItem): Promise<void> {
const path = await this.deleteNotebookFile(item.path); return this.deleteNotebookFile(item.path).then((path: string) => {
useNotebook.getState().deleteNotebookItem(item, isGithubTree); if (!path || path !== item.path) {
throw new Error("No path provided");
}
// TODO: Delete once old resource tree is removed if (item.parent && item.parent.children) {
if (!path || path !== item.path) { // Remove deleted child
throw new Error("No path provided"); const newChildren = item.parent.children.filter((child) => child.path !== path);
} item.parent.children = newChildren;
}
if (item.parent && item.parent.children) { });
// Remove deleted child
const newChildren = item.parent.children.filter((child) => child.path !== path);
item.parent.children = newChildren;
}
} }
/** /**
@@ -93,8 +78,7 @@ export class NotebookContentClient {
public async uploadFileAsync( public async uploadFileAsync(
name: string, name: string,
content: string, content: string,
parent: NotebookContentItem, parent: NotebookContentItem
isGithubTree?: boolean
): Promise<NotebookContentItem> { ): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
@@ -118,8 +102,6 @@ export class NotebookContentClient {
.then((xhr: AjaxResponse) => { .then((xhr: AjaxResponse) => {
const notebookFile = xhr.response; const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) { if (parent.children) {
item.parent = parent; item.parent = parent;
parent.children.push(item); parent.children.push(item);
@@ -142,11 +124,7 @@ export class NotebookContentClient {
* @param sourcePath * @param sourcePath
* @param targetName is not prefixed with path * @param targetName is not prefixed with path
*/ */
public renameNotebook( public renameNotebook(item: NotebookContentItem, targetName: string): Promise<NotebookContentItem> {
item: NotebookContentItem,
targetName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
const sourcePath = item.path; const sourcePath = item.path;
// Match extension // Match extension
if (sourcePath.indexOf(".") !== -1) { if (sourcePath.indexOf(".") !== -1) {
@@ -172,9 +150,6 @@ export class NotebookContentClient {
item.name = notebookFile.name; item.name = notebookFile.name;
item.path = notebookFile.path; item.path = notebookFile.path;
item.timestamp = NotebookUtil.getCurrentTimestamp(); item.timestamp = NotebookUtil.getCurrentTimestamp();
useNotebook.getState().updateNotebookItem(item, isGithubTree);
return item; return item;
}); });
} }
@@ -184,11 +159,7 @@ export class NotebookContentClient {
* @param parent * @param parent
* @param newDirectoryName basename of the new directory * @param newDirectoryName basename of the new directory
*/ */
public async createDirectory( public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise<NotebookContentItem> {
parent: NotebookContentItem,
newDirectoryName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (parent.type !== NotebookContentItemType.Directory) { if (parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent is not a directory: ${parent.path}`); throw new Error(`Parent is not a directory: ${parent.path}`);
} }
@@ -215,11 +186,8 @@ export class NotebookContentClient {
const dir = xhr.response; const dir = xhr.response;
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
item.parent = parent; item.parent = parent;
parent.children?.push(item); parent.children?.push(item);
return item; return item;
}); });
} }

View File

@@ -18,7 +18,6 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
@@ -30,7 +29,6 @@ import { SnapshotRequest } from "./NotebookComponent/types";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils"; import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
import { useNotebook } from "./useNotebook";
type NotebookPaneContent = string | ImmutableNotebook; type NotebookPaneContent = string | ImmutableNotebook;
@@ -112,7 +110,6 @@ export default class NotebookManager {
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender(); this.params.resourceTree.triggerRender();
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
}); });
this.refreshPinnedRepos(); this.refreshPinnedRepos();
} }
@@ -144,7 +141,6 @@ export default class NotebookManager {
notebookContentRef={notebookContentRef} notebookContentRef={notebookContentRef}
onTakeSnapshot={onTakeSnapshot} onTakeSnapshot={onTakeSnapshot}
/>, />,
"440px",
onClosePanel onClosePanel
); );
} }
@@ -173,33 +169,31 @@ export default class NotebookManager {
if (error.status === HttpStatusCodes.Unauthorized) { if (error.status === HttpStatusCodes.Unauthorized) {
this.gitHubOAuthService.resetToken(); this.gitHubOAuthService.resetToken();
useDialog this.params.container.showOkCancelModalDialog(
.getState() undefined,
.showOkCancelModalDialog( "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
undefined, "Connect to GitHub",
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", () =>
"Connect to GitHub", useSidePanel
() => .getState()
useSidePanel .openSidePanel(
.getState() "Connect to GitHub",
.openSidePanel( <GitHubReposPanel
"Connect to GitHub", explorer={this.params.container}
<GitHubReposPanel gitHubClientProp={this.params.container.notebookManager.gitHubClient}
explorer={this.params.container} junoClientProp={this.junoClient}
gitHubClientProp={this.params.container.notebookManager.gitHubClient} />
junoClientProp={this.junoClient} ),
/> "Cancel",
), undefined
"Cancel", );
undefined
);
} }
}; };
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => { private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
let commitMsg = "Committed from Azure Cosmos DB Notebooks"; let commitMsg = "Committed from Azure Cosmos DB Notebooks";
useDialog.getState().showOkCancelModalDialog( this.params.container.showOkCancelModalDialog(
title || "Commit", title || "Commit",
undefined, undefined,
primaryButtonLabel || "Commit", primaryButtonLabel || "Commit",

View File

@@ -6,7 +6,7 @@ import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import * as React from "react"; import * as React from "react";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import HTML5Backend from "react-dnd-html5-backend";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
@@ -14,7 +14,6 @@ import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform"; import loadTransform from "../NotebookComponent/loadTransform";
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types"; import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
import { AzureTheme } from "./AzureTheme"; import { AzureTheme } from "./AzureTheme";
import "./base.css"; import "./base.css";
import CellCreator from "./decorators/CellCreator"; import CellCreator from "./decorators/CellCreator";
@@ -108,7 +107,6 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
return ( return (
<> <>
<div className="NotebookRendererContainer"> <div className="NotebookRendererContainer">
<SecurityWarningBar contentRef={this.props.contentRef} />
<div className="NotebookRenderer" ref={this.notebookRendererRef}> <div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}> <KeyboardShortcuts contentRef={this.props.contentRef}>

View File

@@ -19,12 +19,6 @@
} }
} }
.disabledRunCellButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh;
}
}
.greyStopButton { .greyStopButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon { .runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh; color: @BaseMediumHigh;

View File

@@ -5,7 +5,6 @@ import { Dispatch } from "redux";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { CdbAppState } from "../NotebookComponent/types"; import { CdbAppState } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
export interface PassedPromptProps { export interface PassedPromptProps {
id: string; id: string;
@@ -13,7 +12,6 @@ export interface PassedPromptProps {
status?: string; status?: string;
executionCount?: number; executionCount?: number;
isHovered?: boolean; isHovered?: boolean;
isRunDisabled?: boolean;
runCell?: () => void; runCell?: () => void;
stopCell?: () => void; stopCell?: () => void;
} }
@@ -22,7 +20,6 @@ interface ComponentProps {
id: string; id: string;
contentRef: ContentRef; contentRef: ContentRef;
isHovered?: boolean; isHovered?: boolean;
isNotebookUntrusted?: boolean;
children: (props: PassedPromptProps) => React.ReactNode; children: (props: PassedPromptProps) => React.ReactNode;
} }
@@ -50,7 +47,6 @@ export class PromptPure extends React.Component<Props> {
runCell: this.props.executeCell, runCell: this.props.executeCell,
stopCell: this.props.stopExecution, stopCell: this.props.stopExecution,
isHovered: this.props.isHovered, isHovered: this.props.isHovered,
isRunDisabled: this.props.isNotebookUntrusted,
})} })}
</div> </div>
); );
@@ -79,7 +75,6 @@ const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((s
status, status,
executionCount, executionCount,
isHovered, isHovered,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -1,27 +0,0 @@
import { shallow } from "enzyme";
import { PassedPromptProps } from "./Prompt";
import { promptContent } from "./PromptContent";
describe("PromptContent", () => {
it("renders for busy status", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
status: "busy",
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
it("renders when hovered", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
isHovered: true,
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,6 +1,5 @@
import { IconButton, Spinner, SpinnerSize } from "@fluentui/react"; import { IconButton, Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NotebookUtil } from "../NotebookUtil";
import { PassedPromptProps } from "./Prompt"; import { PassedPromptProps } from "./Prompt";
import "./Prompt.less"; import "./Prompt.less";
@@ -24,18 +23,15 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
</div> </div>
); );
} else if (props.isHovered) { } else if (props.isHovered) {
const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell"; const playButtonText = "Run cell";
return ( return (
<div className={props.isRunDisabled ? "disabledRunCellButton" : ""}> <IconButton
<IconButton className="runCellButton"
className="runCellButton" iconProps={{ iconName: "MSNVideosSolid" }}
iconProps={{ iconName: "MSNVideosSolid" }} title={playButtonText}
title={playButtonText} ariaLabel={playButtonText}
ariaLabel={playButtonText} onClick={props.runCell}
disabled={props.isRunDisabled} />
onClick={props.runCell}
/>
</div>
); );
} else { } else {
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>; return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;

View File

@@ -36,7 +36,6 @@ interface StateProps {
cellIdAbove: CellId; cellIdAbove: CellId;
cellIdBelow: CellId; cellIdBelow: CellId;
hasCodeOutput: boolean; hasCodeOutput: boolean;
isNotebookUntrusted: boolean;
} }
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> { class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
@@ -44,16 +43,12 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
render(): JSX.Element { render(): JSX.Element {
let items: IContextualMenuItem[] = []; let items: IContextualMenuItem[] = [];
const isNotebookUntrusted = this.props.isNotebookUntrusted;
const runTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
if (this.props.cellType === "code") { if (this.props.cellType === "code") {
items = items.concat([ items = items.concat([
{ {
key: "Run", key: "Run",
text: "Run", text: "Run",
title: runTooltip,
disabled: isNotebookUntrusted,
onClick: () => { onClick: () => {
this.props.executeCell(); this.props.executeCell();
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark); this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
@@ -228,7 +223,6 @@ const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state
cellIdAbove, cellIdAbove,
cellIdBelow, cellIdBelow,
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell), hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, ownProps.contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PromptContent renders for busy status 1`] = `
<div
className="greyStopButton"
style={
Object {
"left": 0,
"maxHeight": "100%",
"position": "sticky",
"top": 0,
"width": "100%",
"zIndex": 300,
}
}
>
<CustomizedIconButton
ariaLabel="Stop cell execution"
className="runCellButton"
iconProps={
Object {
"iconName": "CircleStopSolid",
}
}
style={
Object {
"position": "absolute",
}
}
title="Stop cell execution"
/>
<StyledSpinnerBase
size={3}
style={
Object {
"paddingTop": 5,
"position": "absolute",
"width": "100%",
}
}
/>
</div>
`;
exports[`PromptContent renders when hovered 1`] = `
<div
className=""
>
<CustomizedIconButton
ariaLabel="Run cell"
className="runCellButton"
iconProps={
Object {
"iconName": "MSNVideosSolid",
}
}
title="Run cell"
/>
</div>
`;

View File

@@ -11,6 +11,7 @@ import {
DropTargetConnector, DropTargetConnector,
DropTargetMonitor, DropTargetMonitor,
} from "react-dnd"; } from "react-dnd";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import styled, { StyledComponent } from "styled-components"; import styled, { StyledComponent } from "styled-components";
@@ -122,10 +123,9 @@ export const cellTarget = {
drop(props: Props, monitor: DropTargetMonitor, component: any): void { drop(props: Props, monitor: DropTargetMonitor, component: any): void {
if (monitor) { if (monitor) {
const hoverUpperHalf = isDragUpper(props, monitor, component.el); const hoverUpperHalf = isDragUpper(props, monitor, component.el);
const item: Props = monitor.getItem();
// DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation. // DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation.
props.moveCell({ props.moveCell({
id: item.id, id: monitor.getItem().id,
destinationId: props.id, destinationId: props.id,
above: hoverUpperHalf, above: hoverUpperHalf,
contentRef: props.contentRef, contentRef: props.contentRef,

View File

@@ -4,7 +4,6 @@ import Immutable from "immutable";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { NotebookUtil } from "../../../NotebookUtil";
interface ComponentProps { interface ComponentProps {
contentRef: ContentRef; contentRef: ContentRef;
@@ -15,7 +14,6 @@ interface StateProps {
cellMap: Immutable.Map<string, any>; cellMap: Immutable.Map<string, any>;
cellOrder: Immutable.List<string>; cellOrder: Immutable.List<string>;
focusedCell?: string | null; focusedCell?: string | null;
isNotebookUntrusted: boolean;
} }
interface DispatchProps { interface DispatchProps {
@@ -61,13 +59,8 @@ export class KeyboardShortcuts extends React.Component<Props> {
cellOrder, cellOrder,
focusedCell, focusedCell,
cellMap, cellMap,
isNotebookUntrusted,
} = this.props; } = this.props;
if (isNotebookUntrusted) {
return;
}
let ctrlKeyPressed = e.ctrlKey; let ctrlKeyPressed = e.ctrlKey;
// Allow cmd + enter (macOS) to operate like ctrl + enter // Allow cmd + enter (macOS) to operate like ctrl + enter
if (process.platform === "darwin") { if (process.platform === "darwin") {
@@ -132,7 +125,6 @@ export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps)
cellOrder, cellOrder,
cellMap, cellMap,
focusedCell, focusedCell,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -1,5 +1,4 @@
import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable"; import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable";
import { AppState, selectors } from "@nteract/core";
import domtoimage from "dom-to-image"; import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas"; import Html2Canvas from "html2canvas";
import path from "path"; import path from "path";
@@ -12,8 +11,6 @@ import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentI
export type FileType = "directory" | "file" | "notebook"; export type FileType = "directory" | "file" | "notebook";
// Utilities for notebooks // Utilities for notebooks
export class NotebookUtil { export class NotebookUtil {
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
/** /**
* It's a notebook file if the filename ends with .ipynb. * It's a notebook file if the filename ends with .ipynb.
*/ */
@@ -156,16 +153,6 @@ export class NotebookUtil {
); );
} }
public static isNotebookUntrusted(state: AppState, contentRef: string): boolean {
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const metadata = selectors.notebook.metadata(content.model);
return metadata.getIn(["untrusted"]) as boolean;
}
return false;
}
/** /**
* Find code cells with display * Find code cells with display
* @param notebookObject * @param notebookObject

View File

@@ -1,31 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { SecurityWarningBar } from "./SecurityWarningBar";
describe("SecurityWarningBar", () => {
it("renders if notebook is untrusted", () => {
const wrapper = shallow(
<SecurityWarningBar
contentRef={"contentRef"}
isNotebookUntrusted={true}
markNotebookAsTrusted={undefined}
saveNotebook={undefined}
/>
);
expect(wrapper).toMatchSnapshot();
});
it("renders if notebook is trusted", () => {
const wrapper = shallow(
<SecurityWarningBar
contentRef={"contentRef"}
isNotebookUntrusted={false}
markNotebookAsTrusted={undefined}
saveNotebook={undefined}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,93 +0,0 @@
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { actions, AppState } from "@nteract/core";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { NotebookUtil } from "../NotebookUtil";
export interface SecurityWarningBarPureProps {
contentRef: string;
}
interface SecurityWarningBarDispatchProps {
markNotebookAsTrusted: (contentRef: string) => void;
saveNotebook: (contentRef: string) => void;
}
type SecurityWarningBarProps = SecurityWarningBarPureProps & StateProps & SecurityWarningBarDispatchProps;
interface SecurityWarningBarState {
isBarDismissed: boolean;
}
export class SecurityWarningBar extends React.Component<SecurityWarningBarProps, SecurityWarningBarState> {
constructor(props: SecurityWarningBarProps) {
super(props);
this.state = {
isBarDismissed: false,
};
}
render(): JSX.Element {
return this.props.isNotebookUntrusted && !this.state.isBarDismissed ? (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={false}
onDismiss={() => this.setState({ isBarDismissed: true })}
dismissButtonAriaLabel="Close"
actions={
<MessageBarButton
onClick={() => {
this.props.markNotebookAsTrusted(this.props.contentRef);
this.props.saveNotebook(this.props.contentRef);
}}
>
Trust Notebook
</MessageBarButton>
}
>
{" "}
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone
else may involve security risks.
</MessageBar>
) : (
<></>
);
}
}
interface StateProps {
isNotebookUntrusted: boolean;
}
interface InitialProps {
contentRef: string;
}
// Redux
const makeMapStateToProps = (_state: AppState, initialProps: InitialProps) => {
const mapStateToProps = (state: AppState): StateProps => ({
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, initialProps.contentRef),
});
return mapStateToProps;
};
const makeMapDispatchToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch): SecurityWarningBarDispatchProps => {
return {
markNotebookAsTrusted: (contentRef: string) => {
return dispatch(
actions.deleteMetadataField({
contentRef,
field: "untrusted",
})
);
},
saveNotebook: (contentRef: string) => dispatch(actions.save({ contentRef })),
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SecurityWarningBar);

View File

@@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SecurityWarningBar renders if notebook is trusted 1`] = `<Fragment />`;
exports[`SecurityWarningBar renders if notebook is untrusted 1`] = `
<StyledMessageBar
actions={
<CustomizedMessageBarButton
onClick={[Function]}
>
Trust Notebook
</CustomizedMessageBarButton>
}
dismissButtonAriaLabel="Close"
isMultiline={false}
messageBarType={5}
onDismiss={[Function]}
>
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone else may involve security risks.
</StyledMessageBar>
`;

View File

@@ -1,4 +1,3 @@
import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@@ -6,14 +5,8 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
interface NotebookState { interface NotebookState {
isNotebookEnabled: boolean; isNotebookEnabled: boolean;
@@ -25,9 +18,6 @@ interface NotebookState {
isShellEnabled: boolean; isShellEnabled: boolean;
notebookBasePath: string; notebookBasePath: string;
isInitializingNotebooks: boolean; isInitializingNotebooks: boolean;
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -37,15 +27,9 @@ interface NotebookState {
setIsShellEnabled: (isShellEnabled: boolean) => void; setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void; setNotebookBasePath: (notebookBasePath: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>; refreshNotebooksEnabledStateForAccount: () => Promise<void>;
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook: UseStore<NotebookState> = create((set) => ({
isNotebookEnabled: false, isNotebookEnabled: false,
isNotebooksEnabledForAccount: false, isNotebooksEnabledForAccount: false,
notebookServerInfo: { notebookServerInfo: {
@@ -62,9 +46,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isShellEnabled: false, isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath, notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false, isInitializingNotebooks: false,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -122,128 +103,4 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ isNotebooksEnabledForAccount: false }); set({ isNotebooksEnabledForAccount: false });
} }
}, },
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
const currentItem = root || get().myNotebooksContentRoot;
if (currentItem) {
if (currentItem.path === item.path && currentItem.name === item.name) {
return currentItem;
}
if (currentItem.children) {
for (const childItem of currentItem.children) {
const result = get().findItem(childItem, item);
if (result) {
return result;
}
}
}
}
return undefined;
},
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, parent);
item.parent = parentItem;
if (parentItem.children) {
parentItem.children.push(item);
} else {
parentItem.children = [item];
}
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item);
item.parent = parentItem;
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const myNotebooksContentRoot = {
name: "My Notebooks",
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos",
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
}
: undefined;
set({
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
});
if (get().notebookServerInfo?.notebookServerEndpoint) {
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
set({ myNotebooksContentRoot: updatedRoot });
if (updatedRoot?.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
updatedRoot.children.forEach((notebookItem) => {
switch (notebookItem.type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
}
},
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
if (gitHubNotebooksContentRoot) {
gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
children: [],
parent: gitHubNotebooksContentRoot,
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
parent: repoTreeItem,
});
});
gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
set({ gitHubNotebooksContentRoot });
}
},
})); }));

View File

@@ -113,7 +113,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: "", collectionId: "",
enableIndexing: true, enableIndexing: true,
isSharded: userContext.apiType !== "Tables", isSharded: userContext.apiType !== "Tables",
partitionKey: this.getPartitionKey(), partitionKey:
(userContext.features.partitionKeyDefault && userContext.apiType === "SQL") ||
(userContext.features.partitionKeyDefault && userContext.apiType === "Mongo")
? "/id"
: "",
enableDedicatedThroughput: false, enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"), createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
useHashV2: false, useHashV2: false,
@@ -550,72 +554,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
<Text className="panelTextBold" variant="small">
Analytical store
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getAnalyticalStorageTooltipContent()}
>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Off</span>
</Stack>
{!this.isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account.{" "}
<Link href="https://aka.ms/cosmosdb-synapselink" target="_blank">
Learn more
</Link>
</Text>
<DefaultButton
text="Enable"
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
</Stack>
)}
</Stack>
)}
{userContext.apiType !== "Tables" && ( {userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title="Advanced"
@@ -670,6 +608,72 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
/> />
)} )}
{this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
<Text className="panelTextBold" variant="small">
Analytical store
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getAnalyticalStorageTooltipContent()}
>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Off</span>
</Stack>
{!this.isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account.{" "}
<Link href="https://aka.ms/cosmosdb-synapselink" target="_blank">
Learn more
</Link>
</Text>
<DefaultButton
text="Enable"
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
</Stack>
)}
</Stack>
)}
</Stack> </Stack>
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
)} )}
@@ -811,19 +815,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return tooltipText; return tooltipText;
} }
private getPartitionKey(): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
return "";
}
private getPartitionKeySubtext(): string { private getPartitionKeySubtext(): string {
if ( if (
userContext.features.partitionKeyDefault && userContext.features.partitionKeyDefault &&

View File

@@ -98,7 +98,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => { const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem; let parent: NotebookContentItem;
let isGithubTree: boolean;
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
@@ -106,23 +105,21 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
path: useNotebook.getState().notebookBasePath, path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
isGithubTree = false;
break; break;
case "GitHub": case "GitHub":
parent = { parent = {
name: selectedLocation.branch, name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
isGithubTree = true;
break; break;
default: default:
throw new Error(`Unsupported location type ${location.type}`); throw new Error(`Unsupported location type ${location.type}`);
} }
return container.uploadFile(name, content, parent, isGithubTree); return container.uploadFile(name, content, parent);
}; };
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => { const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {

View File

@@ -31,6 +31,19 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
}, },

View File

@@ -150,6 +150,9 @@
.backImageIcon { .backImageIcon {
margin-top: 8px; margin-top: 8px;
} }
.entityValueTextField {
margin: 24px;
}
.addEntityDatePicker { .addEntityDatePicker {
max-width: 145px; max-width: 145px;
} }

View File

@@ -85,12 +85,11 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
export const SidePanel: React.FC = () => { export const SidePanel: React.FC = () => {
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded); const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => { const { isOpen, panelContent, headerText } = useSidePanel((state) => {
return { return {
isOpen: state.isOpen, isOpen: state.isOpen,
panelContent: state.panelContent, panelContent: state.panelContent,
headerText: state.headerText, headerText: state.headerText,
panelWidth: state.panelWidth,
}; };
}); });
// TODO Refactor PanelContainerComponent into a functional component and remove this wrapper // TODO Refactor PanelContainerComponent into a functional component and remove this wrapper
@@ -101,7 +100,6 @@ export const SidePanel: React.FC = () => {
panelContent={panelContent} panelContent={panelContent}
headerText={headerText} headerText={headerText}
isConsoleExpanded={isConsoleExpanded} isConsoleExpanded={isConsoleExpanded}
panelWidth={panelWidth}
/> />
); );
}; };

View File

@@ -105,6 +105,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
notebookName, notebookName,
notebookDescription, notebookDescription,
notebookTags?.split(","), notebookTags?.split(","),
author,
imageSrc, imageSrc,
content content
); );

View File

@@ -21,6 +21,19 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
} }

View File

@@ -1,11 +1,11 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg"; import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity"; import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants"; import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
@@ -15,7 +15,7 @@ import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../.
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities"; import * as Utilities from "../../Tables/Utilities";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { PanelContainerComponent } from "../PanelContainerComponent";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@@ -30,7 +30,9 @@ import {
getCassandraDefaultEntities, getCassandraDefaultEntities,
getDefaultEntities, getDefaultEntities,
getEntityValuePlaceholder, getEntityValuePlaceholder,
getPanelTitle,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
@@ -59,6 +61,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
tableEntityListViewModel, tableEntityListViewModel,
cassandraApiClient, cassandraApiClient,
}: AddTableEntityPanelProps): JSX.Element => { }: AddTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]); const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0); const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>(""); const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
@@ -67,8 +70,6 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
isEntityValuePanelOpen, isEntityValuePanelOpen,
{ setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse }, { setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse },
] = useBoolean(false); ] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
/* Get default and previous saved entity headers */ /* Get default and previous saved entity headers */
useEffect(() => { useEffect(() => {
@@ -97,36 +98,19 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
}; };
/* Add new entity attribute */ /* Add new entity attribute */
const onSubmit = async (): Promise<void> => { const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
for (let i = 0; i < entities.length; i++) { if (!isValidEntities(entities)) {
const { property, type } = entities[i]; return undefined;
if (property === "" || property === undefined) {
setFormError(`Property name cannot be empty. Please enter a property name`);
return;
}
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
} }
event.preventDefault();
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities); const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try { await tableEntityListViewModel.addEntityToCache(newEntity);
await tableEntityListViewModel.addEntityToCache(newEntity); if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { tableEntityListViewModel.redrawTableThrottled();
tableEntityListViewModel.redrawTableThrottled();
}
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
handleError(errorMessage, "AddTableRow");
throw error;
} finally {
setIsExecuting(false);
} }
closeSidePanel();
}; };
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -216,80 +200,110 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsEntityValuePanelTrue(); setIsEntityValuePanelTrue();
}; };
const renderPanelContent = (): JSX.Element => {
return (
<form className="panelFormWrapper">
<div className="panelFormWrapper">
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
<div className="paneFooter">
<div className="leftpanel-okbut">
<input
type="submit"
onClick={submit}
className="genericPaneSubmitBtn"
value={getButtonLabel(userContext.apiType)}
/>
</div>
</div>
</div>
</form>
);
};
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => {
return (
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
);
};
if (isEntityValuePanelOpen) { if (isEntityValuePanelOpen) {
return ( return (
<Stack style={{ padding: "20px 34px" }}> <PanelContainerComponent
<Stack horizontal {...columnProps}> headerText=""
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} /> onRenderNavigationContent={onRenderNavigationContent}
<Label>{entityAttributeProperty}</Label> panelWidth="700px"
</Stack> isOpen={true}
<TextField panelContent={
multiline <TextField
rows={5} multiline
value={entityAttributeValue} rows={5}
onChange={(event, newInput?: string) => { className="entityValueTextField"
entityChange(newInput, selectedRow, "value"); value={entityAttributeValue}
setEntityAttributeValue(newInput); onChange={(event, newInput?: string) => {
}} entityChange(newInput, selectedRow, "value");
/> setEntityAttributeValue(newInput);
</Stack> }}
/>
}
isConsoleExpanded={false}
/>
); );
} }
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: getButtonLabel(userContext.apiType),
onSubmit,
};
return ( return (
<RightPaneForm {...props}> <PanelContainerComponent
<div className="panelMainContent"> headerText={getPanelTitle(userContext.apiType)}
{entities.map((entity, index) => { panelWidth="700px"
return ( isOpen={true}
<TableEntity panelContent={renderPanelContent()}
key={"" + entity.id + index} isConsoleExpanded={false}
isDeleteOptionVisible={entity.isDeleteOptionVisible} />
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
</RightPaneForm>
); );
}; };

View File

@@ -1,11 +1,11 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg"; import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity"; import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants"; import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
@@ -14,7 +14,7 @@ import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { PanelContainerComponent } from "../PanelContainerComponent";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@@ -29,6 +29,7 @@ import {
getEntityValuePlaceholder, getEntityValuePlaceholder,
getFormattedTime, getFormattedTime,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
@@ -58,13 +59,12 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
tableEntityListViewModel, tableEntityListViewModel,
cassandraApiClient, cassandraApiClient,
}: EditTableEntityPanelProps): JSX.Element => { }: EditTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]); const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0); const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>(""); const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({}); const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({});
const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>(""); const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [ const [
isEntityValuePanelOpen, isEntityValuePanelOpen,
@@ -190,44 +190,26 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
return displayValue; return displayValue;
}; };
const onSubmit = async (): Promise<void> => { const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
for (let i = 0; i < entities.length; i++) { if (!isValidEntities(entities)) {
const { property, type } = entities[i]; return undefined;
if (property === "" || property === undefined) {
setFormError(`Property name cannot be empty. Please enter a property name`);
return;
}
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
} }
event.preventDefault();
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities); const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient; const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient;
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument; const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
try { queryTablesTab.collection,
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument( originalDocumentData,
queryTablesTab.collection, entity
originalDocumentData, );
entity await tableEntityListViewModel.updateCachedEntity(newEntity);
); if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
await tableEntityListViewModel.updateCachedEntity(newEntity); tableEntityListViewModel.redrawTableThrottled();
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
} catch (error) {
const errorMessage = getErrorMessage(error);
handleError(errorMessage, "EditTableRow");
throw error;
} finally {
setIsExecuting(false);
} }
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
closeSidePanel();
}; };
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -317,81 +299,109 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
setIsEntityValuePanelTrue(); setIsEntityValuePanelTrue();
}; };
const renderPanelContent = (): JSX.Element => {
return (
<form className="panelFormWrapper">
<div className="panelFormWrapper">
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value?.toString()}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
{renderPanelFooter()}
</div>
</form>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<input type="submit" onClick={submit} className="genericPaneSubmitBtn" value="Update Entity" />
</div>
</div>
);
};
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => (
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
);
if (isEntityValuePanelOpen) { if (isEntityValuePanelOpen) {
return ( return (
<Stack style={{ padding: "20px 34px" }}> <PanelContainerComponent
<Stack horizontal {...columnProps}> headerText=""
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} /> onRenderNavigationContent={onRenderNavigationContent}
<Label>{entityAttributeProperty}</Label> panelWidth="700px"
</Stack> isOpen={true}
<TextField panelContent={
multiline <TextField
rows={5} multiline
value={entityAttributeValue} rows={5}
onChange={(event, newInput?: string) => { className="entityValueTextField"
setEntityAttributeValue(newInput); value={entityAttributeValue}
entityChange(newInput, selectedRow, "value"); onChange={(event, newInput?: string) => {
}} setEntityAttributeValue(newInput);
/> entityChange(newInput, selectedRow, "value");
</Stack> }}
/>
}
isConsoleExpanded={false}
/>
); );
} }
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: "Update",
onSubmit,
};
return ( return (
<RightPaneForm {...props}> <PanelContainerComponent
<div className="panelMainContent"> headerText="Edit Table Entity"
{entities.map((entity, index) => { panelWidth="700px"
return ( isOpen={true}
<TableEntity panelContent={renderPanelContent()}
key={"" + entity.id + index} isConsoleExpanded={false}
isDeleteOptionVisible={entity.isDeleteOptionVisible} />
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value?.toString()}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
</RightPaneForm>
); );
}; };

View File

@@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2
export const columnProps: Partial<IStackProps> = { export const columnProps: Partial<IStackProps> = {
tokens: { childrenGap: 10 }, tokens: { childrenGap: 10 },
styles: { root: { marginBottom: 8 } }, styles: { root: { width: 680 } },
}; };
// helper functions // helper functions
@@ -134,8 +134,8 @@ export const getEntityValuePlaceholder = (entityType: string | number): string =
export const isValidEntities = (entities: EntityRowType[]): boolean => { export const isValidEntities = (entities: EntityRowType[]): boolean => {
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
const { property, type } = entities[i]; const { property } = entities[i];
if (property === "" || property === undefined || !type) { if (property === "" || property === undefined) {
return false; return false;
} }
} }
@@ -170,6 +170,13 @@ export const getDefaultEntities = (headers: string[], entityTypes: EntityType):
return defaultEntities; return defaultEntities;
}; };
export const getPanelTitle = (apiType: string): string => {
if (apiType === "Cassandra") {
return "Add Table Row";
}
return "Add Table Row";
};
export const getAddButtonLabel = (apiType: string): string => { export const getAddButtonLabel = (apiType: string): string => {
if (apiType === "Cassandra") { if (apiType === "Cassandra") {
return "Add Row"; return "Add Row";

View File

@@ -91,7 +91,9 @@ export const UploadItemsPane: FunctionComponent = () => {
accept="application/json" accept="application/json"
multiple multiple
tabIndex={0} tabIndex={0}
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets." tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON
documents. The combined size of all files in an individual upload operation must be less than 2 MB. You
can perform multiple upload operations for larger data sets."
/> />
{uploadFileData?.length > 0 && ( {uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer"> <div className="fileUploadSummaryContainer">

View File

@@ -83,11 +83,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public render(): JSX.Element { public render(): JSX.Element {
const mainItems = this.createMainItems(); const mainItems = this.createMainItems();
const commonTaskItems = this.createCommonTaskItems(); const commonTaskItems = this.createCommonTaskItems();
let recentItems = this.createRecentItems(); const recentItems = this.createRecentItems();
if (userContext.features.notebooksTemporarilyDown) {
recentItems = recentItems.filter((item) => item.description !== "Notebook");
}
const tipsItems = this.createTipsItems(); const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent; const onClearRecent = this.clearMostRecent;
@@ -223,7 +219,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}); });
} }
if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) { if (useNotebook.getState().isNotebookEnabled) {
heroes.push({ heroes.push({
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
title: "New Notebook", title: "New Notebook",

View File

@@ -1,6 +1,5 @@
import Q from "q"; import Q from "q";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { useDialog } from "../../Controls/Dialog";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import * as DataTableUtilities from "./DataTableUtilities"; import * as DataTableUtilities from "./DataTableUtilities";
@@ -70,28 +69,19 @@ export default class TableCommands {
return null; // Error return null; // Error
} }
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
const deleteMessage: string = let deleteMessage: string = "Are you sure you want to delete the selected entities?";
userContext.apiType === "Cassandra" if (userContext.apiType === "Cassandra") {
? "Are you sure you want to delete the selected rows?" deleteMessage = "Are you sure you want to delete the selected rows?";
: "Are you sure you want to delete the selected entities?"; }
if (window.confirm(deleteMessage)) {
useDialog.getState().showOkCancelModalDialog( viewModel.queryTablesTab.container.tableDataClient
"Confirm delete", .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
deleteMessage, .then((results: any) => {
"Delete", return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
() => { viewModel.redrawTableThrottled();
viewModel.queryTablesTab.container.tableDataClient
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
.then((results: any) => {
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
viewModel.redrawTableThrottled();
});
}); });
}, });
"Cancel", }
undefined
);
return null; return null;
} }

View File

@@ -21,7 +21,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import ConflictId from "../Tree/ConflictId"; import ConflictId from "../Tree/ConflictId";
@@ -229,7 +228,7 @@ export default class ConflictsTab extends TabsBase {
this._documentsIterator = this.createIterator(); this._documentsIterator = this.createIterator();
await this.loadNextPage(); await this.loadNextPage();
} catch (error) { } catch (error) {
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); window.alert(getErrorMessage(error));
} }
} }
@@ -253,23 +252,10 @@ export default class ConflictsTab extends TabsBase {
} }
public onAcceptChangesClick = async (): Promise<void> => { public onAcceptChangesClick = async (): Promise<void> => {
if (this.isEditorDirty()) { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
useDialog return;
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
async () => await this.resolveConflict(),
"Cancel",
undefined
);
} else {
await this.resolveConflict();
} }
};
private resolveConflict = async (): Promise<void> => {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@@ -332,7 +318,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) { } catch (error) {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.ResolveConflict, Action.ResolveConflict,
{ {
@@ -386,7 +372,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) { } catch (error) {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteConflict, Action.DeleteConflict,
{ {
@@ -676,6 +662,11 @@ export default class ConflictsTab extends TabsBase {
return jsonObject; return jsonObject;
} }
private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg);
};
private _getPartitionKeyPropertyHeader(): string { private _getPartitionKeyPropertyHeader(): string {
return ( return (
(this.partitionKey && (this.partitionKey &&

View File

@@ -25,7 +25,6 @@ import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils"; import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
@@ -379,7 +378,7 @@ export default class DocumentsTab extends TabsBase {
this.isFilterExpanded(false); this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus(); document.getElementById("errorStatusIcon")?.focus();
} catch (error) { } catch (error) {
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); window.alert(getErrorMessage(error));
} }
} }
@@ -402,29 +401,18 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
} }
public onNewDocumentClick = (): void => { public onNewDocumentClick = (): Q.Promise<any> => {
if (this.isEditorDirty()) { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
useDialog return Q();
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
() => this.initializeNewDocument(),
"Cancel",
undefined
);
} else {
this.initializeNewDocument();
} }
};
private initializeNewDocument = (): void => {
this.selectedDocumentId(null); this.selectedDocumentId(null);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
this.initialDocumentContent(defaultDocument); this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
return Q();
}; };
public onSaveNewDocumentClick = (): Promise<any> => { public onSaveNewDocumentClick = (): Promise<any> => {
@@ -465,7 +453,7 @@ export default class DocumentsTab extends TabsBase {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Create document failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateDocument, Action.CreateDocument,
{ {
@@ -528,7 +516,7 @@ export default class DocumentsTab extends TabsBase {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Update document failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.UpdateDocument, Action.UpdateDocument,
{ {
@@ -558,16 +546,9 @@ export default class DocumentsTab extends TabsBase {
? "Are you sure you want to delete the selected item ?" ? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?"; : "Are you sure you want to delete the selected document ?";
useDialog if (window.confirm(msg)) {
.getState() await this._deleteDocument(selectedDocumentId);
.showOkCancelModalDialog( }
"Confirm delete",
msg,
"Delete",
async () => await this._deleteDocument(selectedDocumentId),
"Cancel",
undefined
);
}; };
public onValidDocumentEdit(): Q.Promise<any> { public onValidDocumentEdit(): Q.Promise<any> {
@@ -636,6 +617,11 @@ export default class DocumentsTab extends TabsBase {
} }
} }
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> { protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection, documentId); return deleteDocument(this.collection, documentId);
} }

View File

@@ -16,7 +16,6 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Controls/Dialog";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId"; import ObjectId from "../Tree/ObjectId";
import DocumentsTab from "./DocumentsTab"; import DocumentsTab from "./DocumentsTab";
@@ -112,7 +111,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Create document failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateDocument, Action.CreateDocument,
{ {
@@ -170,7 +169,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Update document failed", errorMessage); window.alert(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.UpdateDocument, Action.UpdateDocument,
{ {

View File

@@ -17,14 +17,12 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import * as CdbActions from "../Notebook/NotebookComponent/actions"; import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
@@ -61,16 +59,14 @@ export default class NotebookTabV2 extends NotebookTabBase {
}; };
if (this.notebookComponentAdapter.isContentDirty()) { if (this.notebookComponentAdapter.isContentDirty()) {
useDialog this.container.showOkCancelModalDialog(
.getState() "Close without saving?",
.showOkCancelModalDialog( `File has unsaved changes, close without saving?`,
"Close without saving?", "Close",
`File has unsaved changes, close without saving?`, cleanup,
"Close", "Cancel",
cleanup, undefined
"Cancel", );
undefined
);
return Q.resolve(null); return Q.resolve(null);
} else { } else {
cleanup(); cleanup();
@@ -88,13 +84,11 @@ export default class NotebookTabV2 extends NotebookTabBase {
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted();
const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
const saveLabel = "Save"; const saveLabel = "Save";
const copyToLabel = "Copy to ..."; const copyToLabel = "Copy to ...";
const publishLabel = "Publish to gallery"; const publishLabel = "Publish to gallery";
const workspaceLabel = "No Workspace";
const kernelLabel = "No Kernel"; const kernelLabel = "No Kernel";
const runLabel = "Run"; const runLabel = "Run";
const runActiveCellLabel = "Run Active Cell"; const runActiveCellLabel = "Run Active Cell";
@@ -111,6 +105,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
const copyLabel = "Copy"; const copyLabel = "Copy";
const cutLabel = "Cut"; const cutLabel = "Cut";
const pasteLabel = "Paste"; const pasteLabel = "Paste";
const undoLabel = "Undo";
const redoLabel = "Redo";
const cellCodeType = "code"; const cellCodeType = "code";
const cellMarkdownType = "markdown"; const cellMarkdownType = "markdown";
const cellRawType = "raw"; const cellRawType = "raw";
@@ -191,10 +187,9 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.traceTelemetry(Action.ExecuteCell); this.traceTelemetry(Action.ExecuteCell);
}, },
commandButtonLabel: runLabel, commandButtonLabel: runLabel,
tooltipText: runBtnTooltip,
ariaLabel: runLabel, ariaLabel: runLabel,
hasPopup: false, hasPopup: false,
disabled: isNotebookUntrusted, disabled: false,
children: [ children: [
{ {
iconSrc: RunIcon, iconSrc: RunIcon,

View File

@@ -137,14 +137,13 @@ export default class QueryTablesTab extends TabsBase {
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
"Add Table Row", "Add Table Entity",
<AddTableEntityPanel <AddTableEntityPanel
tableDataClient={this.tableDataClient} tableDataClient={this.tableDataClient}
queryTablesTab={this} queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()} tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()} cassandraApiClient={new CassandraAPIDataClient()}
/>, />
"700px"
); );
}; };
@@ -158,8 +157,7 @@ export default class QueryTablesTab extends TabsBase {
queryTablesTab={this} queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()} tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()} cassandraApiClient={new CassandraAPIDataClient()}
/>, />
"700px"
); );
}; };

View File

@@ -571,8 +571,8 @@ export default class Collection implements ViewModels.Collection {
}; };
public onSettingsClick = async (): Promise<void> => { public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
await this.loadOffer(); await this.loadOffer();
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node", description: "Settings node",

View File

@@ -1,11 +1,12 @@
import { extractPartitionKey } from "@azure/cosmos"; import Q from "q";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { readDocument } from "../../Common/dataAccess/readDocument";
import * as DataModels from "../../Contracts/DataModels";
import { useDialog } from "../Controls/Dialog";
import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { extractPartitionKey } from "@azure/cosmos";
import ConflictsTab from "../Tabs/ConflictsTab";
import { readDocument } from "../../Common/dataAccess/readDocument";
export default class ConflictId { export default class ConflictId {
public container: ConflictsTab; public container: ConflictsTab;
@@ -49,20 +50,13 @@ export default class ConflictId {
} }
public click() { public click() {
if (this.container.isEditorDirty()) { if (
useDialog !this.container.isEditorDirty() ||
.getState() window.confirm("Your unsaved changes will be lost. Do you want to continue?")
.showOkCancelModalDialog( ) {
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
() => this.loadConflict(),
"Cancel",
undefined
);
} else {
this.loadConflict(); this.loadConflict();
} }
return;
} }
public async loadConflict(): Promise<void> { public async loadConflict(): Promise<void> {

View File

@@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database {
this.isOfferRead = false; this.isOfferRead = false;
} }
public onSettingsClick = (): void => { public onSettingsClick = () => {
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
@@ -193,8 +193,6 @@ export default class Database implements ViewModels.Database {
//merge collections //merge collections
this.addCollectionsToList(collectionVMs); this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete); this.deleteCollectionsFromList(deltaCollections.toDelete);
useDatabases.getState().updateDatabase(this);
} }
public async openAddCollection(database: Database): Promise<void> { public async openAddCollection(database: Database): Promise<void> {

View File

@@ -1,6 +1,5 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDialog } from "../Controls/Dialog";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
export default class DocumentId { export default class DocumentId {
@@ -29,20 +28,10 @@ export default class DocumentId {
} }
public click() { public click() {
if (this.container.isEditorDirty()) { if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) {
useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
() => this.loadDocument(),
"Cancel",
undefined
);
} else {
this.loadDocument(); this.loadDocument();
} }
return;
} }
public partitionKeyHeader(): Object { public partitionKeyHeader(): Object {

View File

@@ -1,764 +0,0 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as React from "react";
import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { Areas, Notebook } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
export const MyNotebooksTitle = "My Notebooks";
export const GitHubReposTitle = "GitHub repos";
interface ResourceTreeProps {
container: Explorer;
}
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
const databases = useDatabases((state) => state.databases);
const {
isNotebookEnabled,
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
updateNotebookItem,
} = useNotebook(
(state) => ({
isNotebookEnabled: state.isNotebookEnabled,
myNotebooksContentRoot: state.myNotebooksContentRoot,
galleryContentRoot: state.galleryContentRoot,
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
updateNotebookItem: state.updateNotebookItem,
}),
shallow
);
const { activeTab, refreshActiveTab } = useTabs();
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const pseudoDirPath = "PsuedoDir";
const buildGalleryCallout = (): JSX.Element => {
if (
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
) {
return undefined;
}
const calloutProps: ICalloutProps = {
calloutMaxWidth: 350,
ariaLabel: "New gallery",
role: "alertdialog",
gapSpace: 0,
target: ".galleryHeader",
directionalHint: DirectionalHint.leftTopEdge,
onDismiss: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
},
setInitialFocus: true,
};
const openGalleryProps: ILinkProps = {
onClick: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
container.openGallery();
},
};
return (
<Callout {...calloutProps}>
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
<Text variant="xLarge" block>
New gallery
</Text>
<Text block>
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
contributors.
</Text>
<Link {...openGalleryProps}>Open gallery</Link>
</Stack>
</Callout>
);
};
const buildNotebooksTree = (): TreeNode => {
const notebooksTree: TreeNode = {
label: undefined,
isExpanded: true,
children: [],
};
if (userContext.features.notebooksTemporarilyDown) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (myNotebooksContentRoot) {
notebooksTree.children.push(buildMyNotebooksTree());
}
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(buildGitHubNotebooksTree());
}
}
return notebooksTree;
};
const buildNotebooksTemporarilyDownTree = (): TreeNode => {
return {
label: Notebook.temporarilyDownMsg,
className: "clickDisabled",
};
};
const buildGalleryNotebooksTree = (): TreeNode => {
return {
label: "Gallery",
iconSrc: GalleryIcon,
className: "notebookHeader galleryHeader",
onClick: () => container.openGallery(),
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
};
};
const buildMyNotebooksTree = (): TreeNode => {
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
}
);
myNotebooksTree.isExpanded = true;
myNotebooksTree.isAlphaSorted = true;
// Remove "Delete" menu item from context menu
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
return myNotebooksTree;
};
const buildGitHubNotebooksTree = (): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
},
true
);
gitHubNotebooksTree.contextMenu = [
{
label: "Manage GitHub settings",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Manage GitHub settings",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
{
label: "Disconnect from GitHub",
onClick: () => {
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
container.notebookManager?.gitHubOAuthService.logout();
},
},
];
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;
return gitHubNotebooksTree;
};
const buildChildNodes = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode[] => {
if (!item || !item.children) {
return [];
} else {
return item.children.map((item) => {
const result =
item.type === NotebookContentItemType.Directory
? buildNotebookDirectoryNode(item, onFileClick, isGithubTree)
: buildNotebookFileNode(item, onFileClick, isGithubTree);
result.timestamp = item.timestamp;
return result;
});
}
};
const buildNotebookFileNode = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode => {
return {
label: item.name,
iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon,
className: "notebookHeader",
onClick: () => onFileClick(item),
isSelected: () => {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: createFileContextMenu(container, item, isGithubTree),
data: item,
};
};
const createFileContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => container.deleteNotebookFile(item, isGithubTree),
"Cancel",
undefined
);
},
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => copyNotebook(container, item),
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => container.downloadFile(item),
},
];
if (item.type === NotebookContentItemType.Notebook) {
items.push({
label: "Publish to gallery",
iconSrc: PublishIcon,
onClick: async () => {
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
source: Source.ResourceTreeMenu,
});
const content = await container.readFile(item);
if (content) {
await container.publishNotebook(item.name, content);
}
},
});
}
// "Copy to ..." isn't needed if github locations are not available
if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ...");
}
return items;
};
const copyNotebook = async (container: Explorer, item: NotebookContentItem) => {
const content = await container.readFile(item);
if (content) {
container.copyNotebook(item.name, content);
}
};
const createDirectoryContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [
{
label: "Refresh",
iconSrc: RefreshIcon,
onClick: () => loadSubitems(item, isGithubTree),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}?"`,
"Delete",
() => container.deleteNotebookFile(item, isGithubTree),
"Cancel",
undefined
);
},
},
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree),
},
{
label: "New Directory",
iconSrc: NewNotebookIcon,
onClick: () => container.onCreateDirectory(item, isGithubTree),
},
{
label: "New Notebook",
iconSrc: NewNotebookIcon,
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
},
{
label: "Upload File",
iconSrc: NewNotebookIcon,
onClick: () => container.openUploadFilePanel(item),
},
];
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter(
(item) =>
item.label !== "Delete" &&
item.label !== "Rename" &&
item.label !== "New Directory" &&
item.label !== "Upload File"
);
}
return items;
};
const buildNotebookDirectoryNode = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode => {
return {
label: item.name,
iconSrc: undefined,
className: "notebookHeader",
isAlphaSorted: true,
isLeavesParentsSeparate: true,
onClick: () => {
if (!item.children) {
loadSubitems(item, isGithubTree);
}
},
isSelected: () => {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined,
data: item,
children: buildChildNodes(item, onFileClick, isGithubTree),
};
};
const buildDataTree = (): TreeNode => {
const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => {
const databaseNode: TreeNode = {
label: database.id(),
iconSrc: CosmosDBIcon,
isExpanded: false,
className: "databaseHeader",
children: [],
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
onClick: async (isExpanded) => {
useSelectedNode.getState().setSelectedNode(database);
// Rewritten version of expandCollapseDatabase():
if (isExpanded) {
database.collapseDatabase();
} else {
if (databaseNode.children?.length === 0) {
databaseNode.isLoading = true;
}
await database.expandDatabase();
}
databaseNode.isLoading = false;
useCommandBar.getState().setContextButtons([]);
refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id());
},
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
};
if (database.isDatabaseShared()) {
databaseNode.children.push({
label: "Scale",
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettingsV2]),
onClick: database.onSettingsClick.bind(database),
});
}
// Find collections
database
.collections()
.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(buildCollectionNode(database, collection))
);
database.collections.subscribe((collections: ViewModels.Collection[]) => {
collections.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(buildCollectionNode(database, collection))
);
});
return databaseNode;
});
return {
label: undefined,
isExpanded: true,
children: databaseTreeNodes,
};
};
const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
const children: TreeNode[] = [];
children.push({
label: collection.getLabel(),
onClick: () => {
collection.openTab();
// push to most recent
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
},
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Documents,
ViewModels.CollectionTabKind.Graph,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
});
if (
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
!userContext.features.notebooksTemporarilyDown
) {
children.push({
label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
});
}
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
children.push({
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.CollectionSettingsV2,
]),
});
}
const schemaNode: TreeNode = buildSchemaNode(collection);
if (schemaNode) {
children.push(schemaNode);
}
if (showScriptNodes) {
children.push(buildStoredProcedureNode(collection));
children.push(buildUserDefinedFunctionsNode(collection));
children.push(buildTriggerNode(collection));
}
// This is a rewrite of showConflicts
const showConflicts =
userContext?.databaseAccount?.properties.enableMultipleWriteLocations &&
collection.rawDataModel &&
!!collection.rawDataModel.conflictResolutionPolicy;
if (showConflicts) {
children.push({
label: "Conflicts",
onClick: collection.onConflictsClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]),
});
}
return {
label: collection.id(),
iconSrc: CollectionIcon,
isExpanded: false,
children: children,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
onClick: () => {
// Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
onExpanded: () => {
if (showScriptNodes) {
collection.loadStoredProcedures();
collection.loadUserDefinedFunctions();
collection.loadTriggers();
}
},
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
};
};
const buildStoredProcedureNode = (collection: ViewModels.Collection): TreeNode => {
return {
label: "Stored Procedures",
children: collection.storedProcedures().map((sp: StoredProcedure) => ({
label: sp.id(),
onClick: sp.open.bind(sp),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.StoredProcedures,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp),
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
};
};
const buildUserDefinedFunctionsNode = (collection: ViewModels.Collection): TreeNode => {
return {
label: "User Defined Functions",
children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({
label: udf.id(),
onClick: udf.open.bind(udf),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.UserDefinedFunctions,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf),
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
};
};
const buildTriggerNode = (collection: ViewModels.Collection): TreeNode => {
return {
label: "Triggers",
children: collection.triggers().map((trigger: Trigger) => ({
label: trigger.id(),
onClick: trigger.open.bind(trigger),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger),
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
};
};
const buildSchemaNode = (collection: ViewModels.Collection): TreeNode => {
if (collection.analyticalStorageTtl() === undefined) {
return undefined;
}
if (!collection.schema || !collection.schema.fields) {
return undefined;
}
return {
label: "Schema",
children: getSchemaNodes(collection.schema.fields),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid);
},
};
};
const getSchemaNodes = (fields: DataModels.IDataField[]): TreeNode[] => {
const schema: any = {};
//unflatten
fields.forEach((field: DataModels.IDataField) => {
const path: string[] = field.path.split(".");
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
let current: any = {};
path.forEach((name: string, pathIndex: number) => {
if (pathIndex === 0) {
if (schema[name] === undefined) {
if (pathIndex === path.length - 1) {
schema[name] = fieldProperties;
} else {
schema[name] = {};
}
}
current = schema[name];
} else {
if (current[name] === undefined) {
if (pathIndex === path.length - 1) {
current[name] = fieldProperties;
} else {
current[name] = {};
}
}
current = current[name];
}
});
});
const traverse = (obj: any): TreeNode[] => {
const children: TreeNode[] = [];
if (obj !== undefined && !Array.isArray(obj) && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => {
children.push({ label: key, children: traverse(value) });
});
} else if (Array.isArray(obj)) {
return [{ label: obj[0] }, { label: obj[1] }];
}
return children;
};
return traverse(schema);
};
const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise<void> => {
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
updateNotebookItem(updatedItem, isGithubTree);
};
const dataRootNode = buildDataTree();
if (isNotebookEnabled) {
return (
<>
<AccordionComponent>
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
</AccordionItemComponent>
</AccordionComponent>
{buildGalleryCallout()}
</>
);
}
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
};

View File

@@ -16,9 +16,10 @@ import { Areas } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -27,7 +28,6 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
@@ -56,6 +56,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
public galleryContentRoot: NotebookContentItem; public galleryContentRoot: NotebookContentItem;
public myNotebooksContentRoot: NotebookContentItem; public myNotebooksContentRoot: NotebookContentItem;
public gitHubNotebooksContentRoot: NotebookContentItem; public gitHubNotebooksContentRoot: NotebookContentItem;
public junoClient: JunoClient;
public gitHubOAuthService: GitHubOAuthService;
public constructor(private container: Explorer) { public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now()); this.parameters = ko.observable(Date.now());
@@ -72,6 +74,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
useDatabases.subscribe(() => this.triggerRender()); useDatabases.subscribe(() => this.triggerRender());
this.triggerRender(); this.triggerRender();
this.junoClient = new JunoClient();
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
} }
private traceMyNotebookTreeInfo() { private traceMyNotebookTreeInfo() {
@@ -635,7 +639,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
<GitHubReposPanel <GitHubReposPanel
explorer={this.container} explorer={this.container}
gitHubClientProp={this.container.notebookManager.gitHubClient} gitHubClientProp={this.container.notebookManager.gitHubClient}
junoClientProp={this.container.notebookManager.junoClient} junoClientProp={this.junoClient}
/> />
), ),
}, },
@@ -713,16 +717,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
useDialog this.container.showOkCancelModalDialog(
.getState() "Confirm delete",
.showOkCancelModalDialog( `Are you sure you want to delete "${item.name}"`,
"Confirm delete", "Delete",
`Are you sure you want to delete "${item.name}"`, () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Delete", "Cancel",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), undefined
"Cancel", );
undefined
);
}, },
}, },
{ {
@@ -780,16 +782,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
useDialog this.container.showOkCancelModalDialog(
.getState() "Confirm delete",
.showOkCancelModalDialog( `Are you sure you want to delete "${item.name}?"`,
"Confirm delete", "Delete",
`Are you sure you want to delete "${item.name}?"`, () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Delete", "Cancel",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), undefined
"Cancel", );
undefined
);
}, },
}, },
{ {

View File

@@ -0,0 +1,35 @@
import { shallow } from "enzyme";
import React from "react";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { TreeComponent, TreeComponentProps, TreeNode } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import ResourceTokenCollection from "./ResourceTokenCollection";
import { ResourceTreeAdapterForResourceToken } from "./ResourceTreeAdapterForResourceToken";
describe("Resource tree for resource token", () => {
const mockContainer = {} as Explorer;
const resourceTree = new ResourceTreeAdapterForResourceToken(mockContainer);
const mockCollection = {
_rid: "fakeRid",
_self: "fakeSelf",
id: "fakeId",
} as DataModels.Collection;
const mockResourceTokenCollection: ViewModels.CollectionBase = new ResourceTokenCollection(
mockContainer,
"fakeDatabaseId",
mockCollection
);
useDatabases.setState({ resourceTokenCollection: mockResourceTokenCollection });
it("should render", () => {
const rootNode: TreeNode = resourceTree.buildCollectionNode();
const props: TreeComponentProps = {
rootNode,
className: "dataResourceTree",
};
const wrapper = shallow(<TreeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,18 +1,45 @@
import React from "react"; import * as ko from "knockout";
import * as React from "react";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
export const ResourceTokenTree: React.FC = (): JSX.Element => { export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
const collection = useDatabases((state) => state.resourceTokenCollection); public parameters: ko.Observable<number>;
public myNotebooksContentRoot: NotebookContentItem;
const buildCollectionNode = (): TreeNode => { public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now());
useDatabases.subscribe(
() => this.triggerRender(),
(state) => state.resourceTokenCollection
);
useSelectedNode.subscribe(() => this.triggerRender());
useTabs.subscribe(
() => this.triggerRender(),
(state) => state.activeTab
);
this.triggerRender();
}
public renderComponent(): JSX.Element {
const dataRootNode = this.buildCollectionNode();
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
}
public buildCollectionNode(): TreeNode {
const collection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
if (!collection) { if (!collection) {
return { return {
label: undefined, label: undefined,
@@ -59,7 +86,9 @@ export const ResourceTokenTree: React.FC = (): JSX.Element => {
isExpanded: true, isExpanded: true,
children: [collectionNode], children: [collectionNode],
}; };
}; }
return <TreeComponent className="dataResourceTree" rootNode={buildCollectionNode()} />; public triggerRender() {
}; window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -8,7 +8,6 @@ import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { getErrorMessage } from "../Tables/Utilities"; import { getErrorMessage } from "../Tables/Utilities";
import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab"; import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab";
@@ -139,21 +138,16 @@ export default class StoredProcedure {
} }
}; };
public delete() { public delete() {
useDialog.getState().showOkCancelModalDialog( if (!window.confirm("Are you sure you want to delete the stored procedure?")) {
"Confirm delete", return;
"Are you sure you want to delete the stored procedure?", }
"Delete",
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
() => { this.collection.children.remove(this);
useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}
);
}, },
"Cancel", (reason) => {}
undefined
); );
} }

View File

@@ -6,7 +6,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import TriggerTab from "../Tabs/TriggerTab"; import TriggerTab from "../Tabs/TriggerTab";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -100,21 +99,16 @@ export default class Trigger {
}; };
public delete() { public delete() {
useDialog.getState().showOkCancelModalDialog( if (!window.confirm("Are you sure you want to delete the trigger?")) {
"Confirm delete", return;
"Are you sure you want to delete the trigger?", }
"Delete",
deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
() => { this.collection.children.remove(this);
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}
);
}, },
"Cancel", (reason) => {}
undefined
); );
} }
} }

View File

@@ -6,7 +6,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -96,23 +95,18 @@ export default class UserDefinedFunction {
} }
public delete() { public delete() {
useDialog.getState().showOkCancelModalDialog( if (!window.confirm("Are you sure you want to delete the user defined function?")) {
"Confirm delete", return;
"Are you sure you want to delete the user defined function?", }
"Delete",
deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
() => { this.collection.children.remove(this);
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
() => {
/**/
}
);
}, },
"Cancel", () => {
undefined /**/
}
); );
} }
} }

View File

@@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Resource tree for resource token should render 1`] = `
<div
className="treeComponent dataResourceTree"
role="tree"
>
<TreeNodeComponent
generation={0}
node={
Object {
"children": Array [
Object {
"children": Array [
Object {
"isSelected": [Function],
"label": "Items",
"onClick": [Function],
},
],
"className": "collectionHeader",
"iconSrc": "",
"isExpanded": true,
"isSelected": [Function],
"label": "fakeId",
"onClick": [Function],
},
],
"isExpanded": true,
"label": undefined,
}
}
paddingLeft={0}
/>
</div>
`;

View File

@@ -2,7 +2,6 @@ import _ from "underscore";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { useSelectedNode } from "./useSelectedNode"; import { useSelectedNode } from "./useSelectedNode";
interface DatabasesState { interface DatabasesState {
@@ -20,8 +19,6 @@ interface DatabasesState {
loadDatabaseOffers: () => Promise<void>; loadDatabaseOffers: () => Promise<void>;
isFirstResourceCreated: () => boolean; isFirstResourceCreated: () => boolean;
findSelectedDatabase: () => ViewModels.Database; findSelectedDatabase: () => ViewModels.Database;
validateDatabaseId: (id: string) => boolean;
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
} }
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
@@ -132,17 +129,4 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
return selectedNode.collection?.database; return selectedNode.collection?.database;
}, },
validateDatabaseId: (id: string): boolean => {
return !get().databases.some((database) => database.id() === id);
},
validateCollectionId: async (databaseId: string, collectionId: string): Promise<boolean> => {
const database = get().databases.find((db) => db.id() === databaseId);
// For a new tables account, database is undefined when creating the first table
if (!database && userContext.apiType === "Tables") {
return true;
}
await database.loadCollections();
return !database.collections().some((collection) => collection.id() === collectionId);
},
})); }));

23
src/Index.ts Normal file
View File

@@ -0,0 +1,23 @@
import "../less/index.less";
import "./Libs/jquery";
import * as ko from "knockout";
class Index {
public navigationSelection: ko.Observable<string>;
constructor() {
this.navigationSelection = ko.observable("quickstart");
}
public quickstart_click() {
this.navigationSelection("quickstart");
}
public explorer_click() {
this.navigationSelection("explorer");
}
}
var index = new Index();
ko.applyBindings(index);

View File

@@ -1,66 +0,0 @@
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Arrow from "../images/Arrow.svg";
import CosmosDB_20170829 from "../images/CosmosDB_20170829.svg";
import Explorer from "../images/Explorer.svg";
import Feedback from "../images/Feedback.svg";
import Quickstart from "../images/Quickstart.svg";
import "../less/index.less";
const Index = (): JSX.Element => {
const [navigationSelection, setNavigationSelection] = useState("quickstart");
const quickstart_click = () => {
setNavigationSelection("quickstart");
};
const explorer_click = () => {
setNavigationSelection("explorer");
};
return (
<React.Fragment>
<header className="header HeaderBg">
<div className="items">
<img className="DocDBicon" src={CosmosDB_20170829} alt="Azure Cosmos DB" />
<a className="createdocdbacnt" href="https://aka.ms/documentdbcreate" rel="noreferrer" target="_blank">
Create an Azure Cosmos DB account <img className="rightarrowimg" src={Arrow} alt="" />
</a>
<span className="title">Azure Cosmos DB Emulator</span>
</div>
</header>
<nav className="fixedleftpane">
<div
id="Quickstart"
onClick={quickstart_click}
className={navigationSelection === "quickstart" ? "topSelected" : ""}
>
<img id="imgiconwidth1" src={Quickstart} alt="Open Quick Start" />
<span className="menuQuickStart">Quickstart</span>
</div>
<div id="Explorer" onClick={explorer_click} className={navigationSelection === "explorer" ? "topSelected" : ""}>
<img id="imgiconwidth1" src={Explorer} alt="Open Data Explorer" />
<span className="menuExplorer">Explorer</span>
</div>
<div>
<a className="feedbackstyle" href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Emulator%20Feedback">
<img id="imgiconwidth1" src={Feedback} alt="Report Issue" />
<span className="menuExplorer">Report Issue</span>
</a>
</div>
</nav>
{navigationSelection === "quickstart" && (
<iframe name="quickstart" className="iframe" src="quickstart.html"></iframe>
)}
{navigationSelection === "explorer" && (
<iframe name="explorer" className="iframe" src="explorer.html?platform=Emulator"></iframe>
)}
</React.Fragment>
);
};
ReactDOM.render(<Index />, document.getElementById("root"));

View File

@@ -364,6 +364,7 @@ describe("Gallery", () => {
const name = "name"; const name = "name";
const description = "description"; const description = "description";
const tags = ["tag"]; const tags = ["tag"];
const author = "author";
const thumbnailUrl = "thumbnailUrl"; const thumbnailUrl = "thumbnailUrl";
const content = `{ "key": "value" }`; const content = `{ "key": "value" }`;
const addLinkToNotebookViewer = true; const addLinkToNotebookViewer = true;
@@ -372,7 +373,7 @@ describe("Gallery", () => {
json: () => undefined as any, json: () => undefined as any,
}); });
const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content); const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content);
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
@@ -390,6 +391,7 @@ describe("Gallery", () => {
name, name,
description, description,
tags, tags,
author,
thumbnailUrl, thumbnailUrl,
content: JSON.parse(content), content: JSON.parse(content),
addLinkToNotebookViewer, addLinkToNotebookViewer,

View File

@@ -61,6 +61,7 @@ export interface IPublishNotebookRequest {
name: string; name: string;
description: string; description: string;
tags: string[]; tags: string[];
author: string;
thumbnailUrl: string; thumbnailUrl: string;
content: any; content: any;
addLinkToNotebookViewer: boolean; addLinkToNotebookViewer: boolean;
@@ -358,6 +359,7 @@ export class JunoClient {
name: string, name: string,
description: string, description: string,
tags: string[], tags: string[],
author: string,
thumbnailUrl: string, thumbnailUrl: string,
content: string content: string
): Promise<IJunoResponse<IGalleryItem>> { ): Promise<IJunoResponse<IGalleryItem>> {
@@ -368,6 +370,7 @@ export class JunoClient {
name, name,
description, description,
tags, tags,
author,
thumbnailUrl, thumbnailUrl,
content: JSON.parse(content), content: JSON.parse(content),
addLinkToNotebookViewer: true, addLinkToNotebookViewer: true,

View File

@@ -40,7 +40,7 @@
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"ApproximateCost": "Approximate Cost Per Hour", "Cost": "Cost",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String", "ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",

View File

@@ -26,7 +26,7 @@ import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less"; import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less"; import "../less/tree.less";
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer"; import { ResourceTree } from "./Common/ResourceTree";
import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog } from "./Explorer/Controls/Dialog"; import { Dialog } from "./Explorer/Controls/Dialog";
@@ -84,11 +84,7 @@ const App: React.FunctionComponent = () => {
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree"> <div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter"> <div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */} {/* Collections Tree Expanded - Start */}
<ResourceTreeContainer <ResourceTree toggleLeftPaneExpanded={toggleLeftPaneExpanded} isLeftPaneExpanded={isLeftPaneExpanded} />
container={explorer}
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded}
/>
{/* Collections Tree Expanded - End */} {/* Collections Tree Expanded - End */}
{/* Collections Tree Collapsed - Start */} {/* Collections Tree Collapsed - Start */}
<CollapsedResourceTree <CollapsedResourceTree

View File

@@ -10,13 +10,11 @@ export type Features = {
readonly enableSchema: boolean; readonly enableSchema: boolean;
autoscaleDefault: boolean; autoscaleDefault: boolean;
partitionKeyDefault: boolean; partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
readonly executeSproc: boolean; readonly executeSproc: boolean;
readonly enableAadDataPlane: boolean; readonly enableAadDataPlane: boolean;
readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly livyEndpoint?: string; readonly livyEndpoint?: string;
@@ -28,7 +26,8 @@ export type Features = {
readonly pr?: string; readonly pr?: string;
readonly showMinRUSurvey: boolean; readonly showMinRUSurvey: boolean;
readonly ttl90Days: boolean; readonly ttl90Days: boolean;
readonly notebooksTemporarilyDown: boolean; readonly mongoProxyEndpoint: string;
readonly mongoProxyFeatures: string;
}; };
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features { export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
@@ -59,9 +58,10 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableSDKoperations: "true" === get("enablesdkoperations"), enableSDKoperations: "true" === get("enablesdkoperations"),
enableSpark: "true" === get("enablespark"), enableSpark: "true" === get("enablespark"),
enableTtl: "true" === get("enablettl"), enableTtl: "true" === get("enablettl"),
enableKoResourceTree: "true" === get("enablekoresourcetree"),
executeSproc: "true" === get("dataexplorerexecutesproc"), executeSproc: "true" === get("dataexplorerexecutesproc"),
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
mongoProxyEndpoint: get("mongoproxyendpoint"),
mongoProxyFeatures: get("mongoproxyfeatures"),
junoEndpoint: get("junoendpoint"), junoEndpoint: get("junoendpoint"),
livyEndpoint: get("livyendpoint"), livyEndpoint: get("livyendpoint"),
notebookBasePath: get("notebookbasepath"), notebookBasePath: get("notebookbasepath"),
@@ -74,7 +74,9 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
ttl90Days: "true" === get("ttl90days"), ttl90Days: "true" === get("ttl90days"),
autoscaleDefault: "true" === get("autoscaledefault"), autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
}; };
} }
export function hasFlag(value: string, flag: string): boolean {
return true;
}

View File

@@ -4,12 +4,7 @@ import { armRequestWithoutPolling } from "../../Utils/arm/request";
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import { RefreshResult } from "../SelfServeTypes"; import { RefreshResult } from "../SelfServeTypes";
import SqlX from "./SqlX"; import SqlX from "./SqlX";
import { import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes";
FetchPricesResponse,
RegionsResponse,
SqlxServiceResource,
UpdateDedicatedGatewayRequestParameters,
} from "./SqlxTypes";
const apiVersion = "2021-04-01-preview"; const apiVersion = "2021-04-01-preview";
@@ -133,67 +128,3 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResu
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
} }
}; };
const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
};
export const getReadRegions = async (): Promise<Array<string>> => {
try {
const readRegions = new Array<string>();
const response = await armRequestWithoutPolling<RegionsResponse>({
host: configContext.ARM_ENDPOINT,
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
method: "GET",
apiVersion: "2021-04-01-preview",
});
if (response.result.location !== undefined) {
readRegions.push(response.result.location.replace(" ", "").toLowerCase());
} else {
for (const location of response.result.locations) {
readRegions.push(location.locationName.replace(" ", "").toLowerCase());
}
}
return readRegions;
} catch (err) {
return new Array<string>();
}
};
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
};
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
try {
const priceMap = new Map<string, Map<string, number>>();
for (const region of regions) {
const regionPriceMap = new Map<string, number>();
const response = await armRequestWithoutPolling<FetchPricesResponse>({
host: configContext.ARM_ENDPOINT,
path: getFetchPricesPathForRegion(userContext.subscriptionId),
method: "POST",
apiVersion: "2020-01-01-preview",
queryParams: {
filter:
"armRegionNameeq '" +
region +
"'andserviceFamilyeq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
},
});
for (const item of response.result.Items) {
regionPriceMap.set(item.skuName, item.retailPrice);
}
priceMap.set(region, regionPriceMap);
}
return priceMap;
} catch (err) {
return undefined;
}
};

View File

@@ -16,13 +16,11 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
import { import {
deleteDedicatedGatewayResource, deleteDedicatedGatewayResource,
getCurrentProvisioningState, getCurrentProvisioningState,
getPriceMap,
getReadRegions,
refreshDedicatedGatewayProvisioning, refreshDedicatedGatewayProvisioning,
updateDedicatedGatewayResource, updateDedicatedGatewayResource,
} from "./SqlX.rp"; } from "./SqlX.rp";
const costPerHourDefaultValue: Description = { const costPerHourValue: Description = {
textTKey: "CostText", textTKey: "CostText",
type: DescriptionType.Text, type: DescriptionType.Text,
link: { link: {
@@ -55,10 +53,7 @@ const CosmosD16s = "Cosmos.D16s";
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => { const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentValues.set("sku", { value: newValue }); currentValues.set("sku", { value: newValue });
currentValues.set("costPerHour", { currentValues.set("costPerHour", { value: costPerHourValue });
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
});
return currentValues; return currentValues;
}; };
@@ -84,11 +79,6 @@ const onNumberOfInstancesChange = (
} else { } else {
currentValues.set("warningBanner", undefined); currentValues.set("warningBanner", undefined);
} }
currentValues.set("costPerHour", {
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
});
return currentValues; return currentValues;
}; };
@@ -121,11 +111,6 @@ const onEnableDedicatedGatewayChange = (
} as Description, } as Description,
hidden: false, hidden: false,
}); });
currentValues.set("costPerHour", {
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
hidden: false,
});
} else { } else {
currentValues.set("warningBanner", { currentValues.set("warningBanner", {
value: { value: {
@@ -137,8 +122,6 @@ const onEnableDedicatedGatewayChange = (
} as Description, } as Description,
hidden: false, hidden: false,
}); });
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
} }
const sku = currentValues.get("sku"); const sku = currentValues.get("sku");
const instances = currentValues.get("instances"); const instances = currentValues.get("instances");
@@ -154,6 +137,7 @@ const onEnableDedicatedGatewayChange = (
disabled: dedicatedGatewayOriginallyEnabled, disabled: dedicatedGatewayOriginallyEnabled,
}); });
currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes });
currentValues.set("connectionString", { currentValues.set("connectionString", {
value: connectionStringValue, value: connectionStringValue,
hidden: !newValue || !dedicatedGatewayOriginallyEnabled, hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
@@ -193,40 +177,6 @@ const NumberOfInstancesDropdownInfo: Info = {
}, },
}; };
const ApproximateCostDropDownInfo: Info = {
messageTKey: "CostText",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
textTKey: "DedicatedGatewayPricing",
},
};
let priceMap: Map<string, Map<string, number>>;
let regions: Array<string>;
const calculateCost = (skuName: string, instanceCount: number): Description => {
try {
let costPerHour = 0;
for (const region of regions) {
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
if (incrementalCost === undefined) {
throw new Error("Value not found in map.");
}
costPerHour += incrementalCost;
}
costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100;
return {
textTKey: `${costPerHour} USD`,
type: DescriptionType.Text,
};
} catch (err) {
return costPerHourDefaultValue;
}
};
@IsDisplayable() @IsDisplayable()
@RefreshOptions({ retryIntervalInMs: 20000 }) @RefreshOptions({ retryIntervalInMs: 20000 })
export default class SqlX extends SelfServeBaseClass { export default class SqlX extends SelfServeBaseClass {
@@ -324,15 +274,12 @@ export default class SqlX extends SelfServeBaseClass {
hidden: true, hidden: true,
}); });
regions = await getReadRegions();
priceMap = await getPriceMap(regions);
const response = await getCurrentProvisioningState(); const response = await getCurrentProvisioningState();
if (response.status && response.status !== "Deleting") { if (response.status && response.status !== "Deleting") {
defaults.set("enableDedicatedGateway", { value: true }); defaults.set("enableDedicatedGateway", { value: true });
defaults.set("sku", { value: response.sku, disabled: true }); defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("instances", { value: response.instances, disabled: false }); defaults.set("instances", { value: response.instances, disabled: false });
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); defaults.set("costPerHour", { value: costPerHourValue });
defaults.set("connectionString", { defaults.set("connectionString", {
value: connectionStringValue, value: connectionStringValue,
hidden: false, hidden: false,
@@ -391,9 +338,8 @@ export default class SqlX extends SelfServeBaseClass {
}) })
instances: number; instances: number;
@PropertyInfo(ApproximateCostDropDownInfo)
@Values({ @Values({
labelTKey: "ApproximateCost", labelTKey: "Cost",
isDynamicDescription: true, isDynamicDescription: true,
}) })
costPerHour: string; costPerHour: string;

View File

@@ -29,23 +29,3 @@ export type UpdateDedicatedGatewayRequestProperties = {
instanceCount: number; instanceCount: number;
serviceType: string; serviceType: string;
}; };
export type FetchPricesResponse = {
Items: Array<PriceItem>;
NextPageLink: string | undefined;
Count: number;
};
export type PriceItem = {
retailPrice: number;
skuName: string;
};
export type RegionsResponse = {
locations: Array<RegionItem>;
location: string;
};
export type RegionItem = {
locationName: string;
};

View File

@@ -1,9 +1,8 @@
import * as GalleryUtils from "./GalleryUtils";
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { useDialog } from "../Explorer/Controls/Dialog";
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "./GalleryUtils";
const galleryItem: IGalleryItem = { const galleryItem: IGalleryItem = {
id: "id", id: "id",
@@ -30,11 +29,11 @@ describe("GalleryUtils", () => {
it("downloadItem shows dialog in data explorer", () => { it("downloadItem shows dialog in data explorer", () => {
const container = {} as Explorer; const container = {} as Explorer;
container.showOkCancelModalDialog = jest.fn().mockImplementation();
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
expect(useDialog.getState().visible).toBe(true); expect(container.showOkCancelModalDialog).toBeCalled();
expect(useDialog.getState().dialogProps).toBeDefined();
expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks");
}); });
it("favoriteItem favorites item", async () => { it("favoriteItem favorites item", async () => {
@@ -67,11 +66,11 @@ describe("GalleryUtils", () => {
it("deleteItem shows dialog in data explorer", () => { it("deleteItem shows dialog in data explorer", () => {
const container = {} as Explorer; const container = {} as Explorer;
container.showOkCancelModalDialog = jest.fn().mockImplementation();
GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); GalleryUtils.deleteItem(container, undefined, galleryItem, undefined);
expect(useDialog.getState().visible).toBe(true); expect(container.showOkCancelModalDialog).toBeCalled();
expect(useDialog.getState().dialogProps).toBeDefined();
expect(useDialog.getState().dialogProps.title).toBe("Remove published notebook");
}); });
it("getGalleryViewerProps gets gallery viewer props correctly", () => { it("getGalleryViewerProps gets gallery viewer props correctly", () => {

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