Compare commits

...

41 Commits

Author SHA1 Message Date
Sunil Kumar Yadav
d8840a0dfd fixed aria-required property missing issue (#1020) 2021-09-16 14:25:28 -07:00
vaidankarswapnil
f853c4ec2f Fix 'Advanced' expand/collapse control and other issues under 'New Collection' blade (#1021)
* Fix ally issues for newCollection panel's advanced section

* Resolved test snapshot issue

* Removed unnecessary changes
2021-09-16 14:25:17 -07:00
Sunil Kumar Yadav
9bf5f48165 Fixed add collection tooltip accessibility issue (#1022)
* Fixed add collection tooltip accessibility issue

* Remove commented code

* cleanup commented code
2021-09-16 14:24:47 -07:00
Sunil Kumar Yadav
0b2a204b70 fixed delete database field missing issue at 200% resize mode (#1066) 2021-09-16 14:24:34 -07:00
vaidankarswapnil
c28593b752 Fix expand/collapse tree button of Data Explorer page is not accessible through keyboard (#1067)
* Fix ally issues for newCollection panel's advanced section

* Resolved test snapshot issue

* Fix a11y data explorer expand/collapse using keyboard issue
2021-09-16 14:24:24 -07:00
Sunil Kumar Yadav
4cbbef9574 change area label of UDF (#1068) 2021-09-16 14:24:11 -07:00
vaidankarswapnil
300c952a9b Fix a11y QueryTab focus and close button issue using keyboard (#1069) 2021-09-16 14:23:55 -07:00
Sunil Kumar Yadav
38c3761260 fixed input parameter keyboard accessibility issue (#1071)
* fixed input parameter keyboard accessibility issue

* Fixed autofocus and role issue

* make autofocus on close button
2021-09-16 14:23:43 -07:00
vaidankarswapnil
3032f689b6 Fix delete container and database labels appearing text are not associated with the edit fields (#1072)
* Fix a11y issues for delete container and database

* Update test snapshot issues
2021-09-16 14:23:29 -07:00
Hardikkumar Nai
8b30af3d9e Settings: At 200% resize mode controls present under 'Settings' blade are not visible while navigating over them. (#1075) 2021-09-16 14:23:03 -07:00
Sunil Kumar Yadav
e10240bd7a fixed setting keyboard accessibility issue (#1081) 2021-09-16 14:22:47 -07:00
vaidankarswapnil
ae9c27795e Fix execute query keyboard focus moves to hidden element under 'Results' section of executed Query 1 blade (#1082)
* fix a11y quertTab results section hidden element focus issue

* Removed commented code

* Resolved lint issues
2021-09-16 14:21:19 -07:00
Karthik chakravarthy
d7997d716e Data pane expand issue (#1085)
* Data pane expand issue

* Data pane expand issue-1

* Data pane expand issue format

* Data pane expand issue formating
2021-09-15 19:50:36 -04:00
victor-meng
af0dc3094b Temporarily lower test coverage threshold (#1084) 2021-09-15 16:38:51 -07:00
victor-meng
665270296f Fix throughput cost estimate in add collection panel (#1070) 2021-09-15 13:05:55 -07:00
Asier Isayas
2d945c8231 allowing azure client secret to be null in dev mode (#1079)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2021-09-14 12:33:09 -04:00
Asier Isayas
8866976bb4 fixed hasFlag test (#1076)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2021-09-14 12:28:33 -04:00
Asier Isayas
d10f3c69f1 MongoClient Feature Flag (#1073)
Adding a feature flag for Mongo Client that allows a user to specify a mongo endpoint and an API so that users can test specific APIs locally.

Example:

https://localhost:1234/hostedExplorer.html?feature.mongoproxyendpoint=https://localhost:12901&feature.mongoProxyAPIs=createDocument|readDocument

The above link says to test APIs createDocument and readDocument on https://localhost:12901

Co-authored-by: artrejo <ato9000@users.noreply.github.com>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2021-09-13 16:25:21 -04:00
kcheekuri
7e4f030547 Hidding container connection status behind the feature flag and initi… (#1019)
* Hidding container connection status behind the feature flag and initializing scratch issue

* maintaining connecting status UX part at notebooks context

* Changing scratch name to temporary and showing only after connected
2021-09-09 14:02:00 -04:00
siddjoshi-ms
05f46dd635 Sqlx approx cost bug fixes (#975)
* function naming changed

* bug fix: replacing multiple occurences of space correctly now
2021-09-08 14:04:31 -07:00
Meha Kaushik
65882ea831 Self-Server for GraphAPI Compute (#1017)
* Self-Server for GraphAPI Compute

* Update GraphAPICompute.json
2021-09-07 23:42:39 -07:00
kcheekuri
95c9b7ee31 Users/kcheekuri/aciconatinerpooling (#1008)
* initial changes for CP

* Added container unprovisioning

* Added postgreSQL terminal

* changed postgres terminal -> shell

* Initialize Container Request payload change

* added postgres button

* Added notebookServerInfo

* Feature flag enabling and integration with phoenix

* Remove postgre implementations

* fix issues

* fix format issues

* fix format issues-1

* fix format issues-2

* fix format issues-3

* fix format issues-4

* fix format issues-5

* connection status component

* connection status component-1

* connection status component-2

* connection status component-3

* address issues

* removal of ms

* removal of ms

* removal of ms-1

* removal of time after connected

* removal of time after connected

* removing unnecessary code

Co-authored-by: Srinath Narayanan <srnara@microsoft.com>
Co-authored-by: Bala Lakshmi Narayanasami <balalakshmin@microsoft.com>
2021-09-03 23:04:26 -07:00
Zachary Foster
39dd293fc1 Fetch aad token against tenant's authority (#1004) 2021-08-30 14:21:32 -05:00
victor-meng
8eeda41021 Move synapse link out of advanced section in add collection panel (#989) 2021-08-19 12:18:21 -07:00
Hardikkumar Nai
960cd9fc55 Resolve ESlint Controls (#990) 2021-08-19 12:16:35 -05:00
Hardikkumar Nai
9ec0ac9f54 Resolve ESLint Contracts (#986) 2021-08-19 12:15:52 -05:00
Steve Faulkner
b66aeb814a Polyfill Buffer (#988) 2021-08-18 21:12:40 -05:00
Tanuj Mittal
410f582378 Fix notebooksTemporarilyDown feature flag (#983) 2021-08-17 07:27:41 +05:30
Hardikkumar Nai
678ca51c77 Update to Webpack 5 (#964)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-08-16 15:44:40 -05:00
Steve Faulkner
2dfd90ca0f Disable Notebooks Test (#980) 2021-08-16 11:35:21 -05:00
Srinath Narayanan
4110be10bd Removing author from publish notebook payload (#966)
* removing author from publish payload

* fixed failing tests
2021-08-15 13:15:30 +05:30
Srinath Narayanan
6e55e397b3 Changes for Disabling notebook related features (#979)
* notebook removal changes

* fixed failing tests
2021-08-14 12:09:22 +05:30
Tanuj Mittal
24d0a16123 Disable notebooks temporarily (#978)
* Disable notebooks temporarily

* Updates
2021-08-14 07:03:58 +05:30
Tanuj Mittal
f8ac36962b Add Security Warning Bar for untrusted notebooks (#970)
* Add Security Warning Bar for untrusted notebooks

* Update

* Another update

* Add a snapshot test

* UX updates

* More updates

* Add tests

* Update test snapshot

* Update string

* Update security message
2021-08-10 23:54:26 +05:30
Sunil Kumar Yadav
51f3f9a718 Move inputTypeahead to react (#946)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-08-04 09:46:18 -05:00
victor-meng
ee4404c439 Fix enable synapse link error (#918)
* Fix enable synapse error

* Default all ARM requests to JSON

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-08-02 21:11:42 -05:00
Steve Faulkner
b65456f754 Fix bug in Dialog State store (#969) 2021-08-02 20:54:30 -05:00
victor-meng
56699ccb1b Fix new resource tree (#962) 2021-07-30 16:23:36 -07:00
victor-meng
042f980b89 Replace window.confirm and window.alert with modal dialog (#965) 2021-07-30 10:27:27 -07:00
t-tarabhatia
7e0c4b7290 Add changes to Partition Key A/B Test (#954) 2021-07-29 08:48:03 -05:00
victor-meng
a66fc06dad Turn off react resource tree (#963) 2021-07-28 21:29:45 -07:00
123 changed files with 5186 additions and 4341 deletions

View File

@@ -21,16 +21,8 @@ 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
@@ -44,29 +36,10 @@ 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

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 # - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
- ./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

@@ -37,8 +37,8 @@ module.exports = {
global: { global: {
branches: 25, branches: 25,
functions: 25, functions: 25,
lines: 30, lines: 29.5,
statements: 30, statements: 29.5,
}, },
}, },

View File

@@ -9,6 +9,7 @@
@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

@@ -19,6 +19,10 @@
.notebookHeader { .notebookHeader {
font-size: 12px; font-size: 12px;
} }
.clickDisabled {
pointer-events: none;
}
} }

4641
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -48,9 +48,9 @@
"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": "0.1.19", "clean-webpack-plugin": "3.0.0",
"clipboard-copy": "4.0.1", "clipboard-copy": "4.0.1",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "9.0.1",
"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 +81,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.13.1", "react": "16.14.0",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
"react-dnd": "9.4.0", "react-dnd": "14.0.2",
"react-dnd-html5-backend": "9.4.0", "react-dnd-html5-backend": "14.0.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 +98,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": "3.1.0", "terser-webpack-plugin": "5.1.4",
"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,6 +132,7 @@
"@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",
@@ -153,44 +154,45 @@
"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": "4.5.2", "html-webpack-plugin": "5.3.2",
"jest": "25.5.4", "jest": "26.6.3",
"jest-canvas-mock": "2.1.0", "jest-canvas-mock": "2.3.1",
"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": "0.4.3", "mini-css-extract-plugin": "2.1.0",
"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.13.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": "6.2.2", "ts-loader": "9.2.4",
"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": "4.46.0", "webpack": "5.47.0",
"webpack-bundle-analyzer": "3.6.1", "webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "3.3.10", "webpack-cli": "4.7.2",
"webpack-dev-server": "3.11.0" "webpack-dev-server": "3.11.2"
}, },
"scripts": { "scripts": {
"start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js", "start": "webpack serve --mode development",
"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": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode production", "pack:prod": "webpack --mode production",
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress", "pack:fast": "webpack --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

@@ -1,6 +1,7 @@
import React, { FunctionComponent } from "react"; import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { NormalizedEventKey } from "./Constants";
export interface CollapsedResourceTreeProps { export interface CollapsedResourceTreeProps {
toggleLeftPaneExpanded: () => void; toggleLeftPaneExpanded: () => void;
@@ -11,6 +12,21 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
toggleLeftPaneExpanded, toggleLeftPaneExpanded,
isLeftPaneExpanded, isLeftPaneExpanded,
}: CollapsedResourceTreeProps): JSX.Element => { }: CollapsedResourceTreeProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (focusButton.current) {
focusButton.current.focus();
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return ( return (
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}> <div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
<div className="main-nav nav"> <div className="main-nav nav">
@@ -21,11 +37,14 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label="Expand Tree" aria-label="Expand Tree"
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
ref={focusButton}
> >
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}> <span className="leftarrowCollapsed">
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" /> <img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span> </span>
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}> <span className="collectionCollapsed">
<span>{userContext.apiType} API</span> <span>{userContext.apiType} API</span>
</span> </span>
</li> </li>

View File

@@ -95,6 +95,8 @@ 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";
public static readonly Phoenix = "phoenix";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -336,6 +338,12 @@ export enum ConflictOperationType {
Delete = "delete", Delete = "delete",
} }
export enum ConnectionStatusType {
Connecting = "Connecting",
Connected = "Connected",
Failed = "Connection Failed",
}
export const EmulatorMasterKey = export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
@@ -349,6 +357,11 @@ 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

@@ -3,8 +3,16 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; import {
deleteDocument,
getEndpoint,
getFeatureEndpointOrDefault,
queryDocuments,
readDocument,
updateDocument,
} from "./MongoProxyClient";
const databaseId = "testDB"; const databaseId = "testDB";
@@ -246,4 +254,31 @@ describe("MongoProxyClient", () => {
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
}); });
}); });
describe("getFeatureEndpointOrDefault", () => {
beforeEach(() => {
resetConfigContext();
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
});
const params = new URLSearchParams({
"feature.mongoProxyEndpoint": "https://localhost:12901",
"feature.mongoProxyAPIs": "readDocument|createDocument",
});
const features = extractFeatures(params);
updateUserContext({
authType: AuthType.AAD,
features: features,
});
});
it("returns a local endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("readDocument");
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
});
it("returns a production endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
});
});
}); });

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";
@@ -78,7 +79,7 @@ export function queryDocuments(
: "", : "",
}; };
const endpoint = getEndpoint() || ""; const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
const headers = { const headers = {
...defaultHeaders, ...defaultHeaders,
@@ -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",
@@ -181,7 +183,7 @@ export function createDocument(
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
}; };
const endpoint = getEndpoint(); const endpoint = getFeatureEndpointOrDefault("createDocument");
return window return window
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, { .fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
@@ -225,7 +227,7 @@ export function updateDocument(
? documentId.partitionKeyProperty ? documentId.partitionKeyProperty
: "", : "",
}; };
const endpoint = getEndpoint(); const endpoint = getFeatureEndpointOrDefault("updateDocument");
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
? documentId.partitionKeyProperty ? documentId.partitionKeyProperty
: "", : "",
}; };
const endpoint = getEndpoint(); const endpoint = getFeatureEndpointOrDefault("deleteDocument");
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy(
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(), autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
}; };
const endpoint = getEndpoint(); const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
return window return window
.fetch( .fetch(
@@ -333,8 +335,15 @@ export function createMongoCollectionWithProxy(
}); });
} }
export function getEndpoint(): string { export function getFeatureEndpointOrDefault(feature: string): string {
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer"; return hasFlag(userContext.features.mongoProxyAPIs, feature)
? getEndpoint(userContext.features.mongoProxyEndpoint)
: getEndpoint();
}
export function getEndpoint(customEndpoint?: string): string {
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
url += "/api/mongo/explorer";
if (userContext.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo"); url = url.replace("api/mongo", "api/guest/mongo");

View File

@@ -1,4 +1,4 @@
import React, { FunctionComponent } from "react"; import React, { FunctionComponent, MutableRefObject, useEffect, useRef } 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";
@@ -6,6 +6,7 @@ import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree"; import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree } from "../Explorer/Tree/ResourceTree"; import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps { export interface ResourceTreeContainerProps {
toggleLeftPaneExpanded: () => void; toggleLeftPaneExpanded: () => void;
@@ -18,6 +19,22 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
isLeftPaneExpanded, isLeftPaneExpanded,
container, container,
}: ResourceTreeContainerProps): JSX.Element => { }: ResourceTreeContainerProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (isLeftPaneExpanded) {
if (focusButton.current) {
focusButton.current.focus();
}
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return ( return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}> <div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */} {/* Collections Window - - Start */}
@@ -43,9 +60,11 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
id="expandToggleLeftPaneButton" id="expandToggleLeftPaneButton"
role="button" role="button"
onClick={toggleLeftPaneExpanded} onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
tabIndex={0} tabIndex={0}
aria-label="Collapse Tree" aria-label="Collapse Tree"
title="Collapse Tree" title="Collapse Tree"
ref={focusButton}
> >
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" /> <img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span> </span>
@@ -54,7 +73,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
</div> </div>
{userContext.authType === AuthType.ResourceToken ? ( {userContext.authType === AuthType.ResourceToken ? (
<ResourceTokenTree /> <ResourceTokenTree />
) : userContext.features.enableKOResourceTree ? ( ) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" /> <div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : ( ) : (
<ResourceTree container={container} /> <ResourceTree container={container} />

View File

@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
return ( return (
<span> <span>
<TooltipHost content={children}> <TooltipHost content={children}>
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" /> <Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</span> </span>
); );

View File

@@ -1,3 +1,5 @@
import { ConnectionStatusType } from "../Common/Constants";
export interface DatabaseAccount { export interface DatabaseAccount {
id: string; id: string;
name: string; name: string;
@@ -496,3 +498,8 @@ export interface MemoryUsageInfo {
freeKB: number; freeKB: number;
totalKB: number; totalKB: number;
} }
export interface ContainerConnectionInfo {
status: ConnectionStatusType;
//need to add ram and rom info
}

View File

@@ -22,8 +22,8 @@ describe("The Heatmap Control", () => {
}; };
let heatmap: Heatmap; let heatmap: Heatmap;
let theme: PortalTheme = 1; const theme: PortalTheme = 1;
const divElement: string = `<div id="${Heatmap.elementId}"></div>`; const divElement = `<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", () => {
let data = { const data = {
data: { data: {
signature: "pcIframe", signature: "pcIframe",
data: { data: {
@@ -111,7 +111,7 @@ describe("iframe rendering when there is no data", () => {
}, },
}; };
const divElement: string = `<div id="${Heatmap.elementId}"></div>`; const divElement = `<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", () => {
let data = { const data = {
data: { data: {
signature: "pcIframe", signature: "pcIframe",
data: { data: {
@@ -131,7 +131,7 @@ describe("iframe rendering when there is no data", () => {
}, },
}; };
const divElement: string = `<div id="${Heatmap.elementId}"></div>`; const divElement = `<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: string = "#838383"): FontSettings { private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#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]);
let dataPoints: number[] = []; const dataPoints: number[] = [];
for (let a = 0; a < output.xAxisPoints.length; a++) { for (let a = 0; a < output.xAxisPoints.length; a++) {
let row: PartitionTimeStampToData = data[rows[i]]; const 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()
); );
let plotDiv: any = document.getElementById(Heatmap.elementId); const 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;
} }
} }
let output = []; const 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,6 +83,7 @@ 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

@@ -1,5 +1,6 @@
import { Icon, Label, Stack } from "@fluentui/react"; import { Icon, Label, Stack } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
export interface CollapsibleSectionProps { export interface CollapsibleSectionProps {
@@ -30,6 +31,13 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
} }
} }
private onKeyPress = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
this.toggleCollapsed();
event.stopPropagation();
}
};
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<> <>
@@ -39,6 +47,11 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
verticalAlign="center" verticalAlign="center"
tokens={accordionStackTokens} tokens={accordionStackTokens}
onClick={this.toggleCollapsed} onClick={this.toggleCollapsed}
onKeyPress={this.onKeyPress}
tabIndex={0}
aria-name="Advanced"
role="button"
aria-expanded={this.state.isExpanded}
> >
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} /> <Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
<Label>{this.props.title}</Label> <Label>{this.props.title}</Label>

View File

@@ -3,9 +3,14 @@
exports[`CollapsibleSectionComponent renders 1`] = ` exports[`CollapsibleSectionComponent renders 1`] = `
<Fragment> <Fragment>
<Stack <Stack
aria-expanded={true}
aria-name="Advanced"
className="collapsibleSection" className="collapsibleSection"
horizontal={true} horizontal={true}
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
tokens={ tokens={
Object { Object {
"childrenGap": 10, "childrenGap": 10,

View File

@@ -23,13 +23,75 @@ 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) => ({ export const useDialog: UseStore<DialogState> = create((set, get) => ({
visible: false, visible: false,
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
closeDialog: () => closeDialog: () =>
set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), set(
(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 {
@@ -119,8 +181,7 @@ export const Dialog: FC = () => {
text: secondaryButtonText, text: secondaryButtonText,
onClick: onSecondaryButtonClick, onClick: onSecondaryButtonClick,
} }
: {}; : undefined;
return visible ? ( return visible ? (
<FluentDialog {...dialogProps}> <FluentDialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />} {choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}

View File

@@ -5,6 +5,9 @@
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;
@@ -21,4 +24,11 @@
} }
} }
} }
.input-typeahead-chocies-container {
border: 1px solid lightgrey;
padding: 5px 10px 5px 10px;
cursor: pointer;
.choice-caption{
font-size: 14px;
}
}

View File

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

View File

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

View File

@@ -1,45 +1,45 @@
import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import { Urls, StyleConstants } from "../../../Common/Constants";
import { import {
getPriceCurrency, DetailsList,
getCurrencySign, DetailsListLayoutMode,
getAutoscalePricePerRu, DetailsRow,
getMultimasterMultiplier,
computeRUUsagePriceHourly,
getPricePerRu,
estimatedCostDisclaimer,
} from "../../../Utils/PricingUtils";
import {
ITextFieldStyles,
ICheckboxStyles, ICheckboxStyles,
IStackProps,
IStackTokens,
IChoiceGroupStyles, IChoiceGroupStyles,
Link, IColumn,
Text, IDetailsColumnStyles,
IMessageBarStyles,
ITextStyles,
IDetailsRowStyles,
IStackStyles,
IDetailsListStyles, IDetailsListStyles,
IDetailsRowProps,
IDetailsRowStyles,
IDropdownStyles, IDropdownStyles,
IMessageBarStyles,
ISeparatorStyles, ISeparatorStyles,
IStackProps,
IStackStyles,
IStackTokens,
ITextFieldStyles,
ITextStyles,
Link,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
Stack, SelectionMode,
Spinner, Spinner,
SpinnerSize, SpinnerSize,
DetailsList, Stack,
IColumn, Text,
SelectionMode,
DetailsListLayoutMode,
IDetailsRowProps,
DetailsRow,
IDetailsColumnStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { isDirtyTypes, isDirty } from "./SettingsUtils"; import * as React from "react";
import { StyleConstants, Urls } from "../../../Common/Constants";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import {
computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu,
getCurrencySign,
getMultimasterMultiplier,
getPriceCurrency,
getPricePerRu,
} from "../../../Utils/PricingUtils";
import { isDirty, isDirtyTypes } from "./SettingsUtils";
export interface EstimatedSpendingDisplayProps { export interface EstimatedSpendingDisplayProps {
costType: JSX.Element; costType: JSX.Element;
@@ -223,14 +223,15 @@ export const getRuPriceBreakdown = (
multimasterEnabled: isMultimaster, multimasterEnabled: isMultimaster,
isAutoscale: isAutoscale, isAutoscale: isAutoscale,
}); });
const basePricePerRu: number = isAutoscale const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) const pricePerRu: number = isAutoscale
: getPricePerRu(serverId); ? getAutoscalePricePerRu(serverId, multimasterMultiplier)
: getPricePerRu(serverId, multimasterMultiplier);
return { return {
hourlyPrice: hourlyPrice, hourlyPrice,
dailyPrice: hourlyPrice * 24, dailyPrice: hourlyPrice * 24,
monthlyPrice: hourlyPrice * hoursInAMonth, monthlyPrice: hourlyPrice * hoursInAMonth,
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), pricePerRu,
currency: getPriceCurrency(serverId), currency: getPriceCurrency(serverId),
currencySign: getCurrencySign(serverId), currencySign: getCurrencySign(serverId),
}; };

View File

@@ -34,6 +34,7 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -101,6 +102,7 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext";
import { import {
calculateEstimateNumber, calculateEstimateNumber,
computeRUUsagePriceHourly, computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu, getAutoscalePricePerRu,
getCurrencySign, getCurrencySign,
getMultimasterMultiplier, getMultimasterMultiplier,
@@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId); const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
: getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>; const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
if (isAutoscale) { if (isAutoscale) {
return ( return (

View File

@@ -118,6 +118,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<input <input
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
aria-label="Autoscale mode" aria-label="Autoscale mode"
aria-required={true}
checked={isAutoscaleSelected} checked={isAutoscaleSelected}
type="radio" type="radio"
role="radio" role="radio"
@@ -131,6 +132,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
aria-label="Manual mode" aria-label="Manual mode"
checked={!isAutoscaleSelected} checked={!isAutoscaleSelected}
type="radio" type="radio"
aria-required={true}
role="radio" role="radio"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")} onChange={(e) => handleOnChangeMode(e, "Manual")}

View File

@@ -345,12 +345,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
ariaLabel="Info" ariaLabel="Info"
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
tabIndex={0}
> >
<IconBase <IconBase
ariaLabel="Info" ariaLabel="Info"
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
styles={[Function]} styles={[Function]}
tabIndex={0}
theme={ theme={
Object { Object {
"disableGlobalClassNames": false, "disableGlobalClassNames": false,
@@ -630,6 +632,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="panelInfoIcon root-57" className="panelInfoIcon root-57"
data-icon-name="Info" data-icon-name="Info"
role="img" role="img"
tabIndex={0}
> >
</i> </i>
@@ -651,6 +654,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<input <input
aria-label="Autoscale mode" aria-label="Autoscale mode"
aria-required={true}
checked={true} checked={true}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
key=".0:$.0" key=".0:$.0"
@@ -667,6 +671,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</span> </span>
<input <input
aria-label="Manual mode" aria-label="Manual mode"
aria-required={true}
checked={false} checked={false}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
key=".0:$.2" key=".0:$.2"
@@ -1327,12 +1332,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
ariaLabel="Info" ariaLabel="Info"
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
tabIndex={0}
> >
<IconBase <IconBase
ariaLabel="Info" ariaLabel="Info"
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
styles={[Function]} styles={[Function]}
tabIndex={0}
theme={ theme={
Object { Object {
"disableGlobalClassNames": false, "disableGlobalClassNames": false,
@@ -1612,6 +1619,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="panelInfoIcon root-57" className="panelInfoIcon root-57"
data-icon-name="Info" data-icon-name="Info"
role="img" role="img"
tabIndex={0}
> >
</i> </i>

View File

@@ -17,7 +17,6 @@ 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,6 +1,7 @@
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";
@@ -20,7 +21,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.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg); logConsoleError(msg);
return; return;
} }
@@ -29,7 +30,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.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleInfo(msg); logConsoleInfo(msg);
} }

View File

@@ -1,4 +1,3 @@
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";
@@ -17,6 +16,7 @@ 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 { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem } from "../Juno/JunoClient";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
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";
@@ -35,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, TextFieldProps, useDialog } from "./Controls/Dialog"; import { DialogProps, 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";
@@ -88,12 +88,13 @@ export default class Explorer {
}; };
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
private phoenixClient: PhoenixClient;
constructor() { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.phoenixClient = new PhoenixClient();
useNotebook.subscribe( useNotebook.subscribe(
() => this.refreshCommandBarButtons(), () => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount (state) => state.isNotebooksEnabledForAccount
@@ -344,19 +345,36 @@ export default class Explorer {
return; return;
} }
this._isInitializingNotebooks = true; this._isInitializingNotebooks = true;
if (userContext.features.phoenix) {
const provisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
resourceId: userContext.databaseAccount.id,
dbAccountName: userContext.databaseAccount.name,
aadToken: userContext.authorizationToken,
resourceGroup: userContext.resourceGroup,
subscriptionId: userContext.subscriptionId,
};
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
}
} else {
await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
await this.ensureNotebookWorkspaceRunning(); useNotebook.getState().setNotebookServerInfo({
const connectionInfo = await listConnectionInfo( notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
userContext.subscriptionId, authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
userContext.resourceGroup, });
databaseAccount.name, }
"default"
);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
useNotebook.getState().initializeNotebooksTree(this.notebookManager); useNotebook.getState().initializeNotebooksTree(this.notebookManager);
@@ -365,7 +383,7 @@ export default class Explorer {
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public resetNotebookWorkspace() { public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError( handleError(
"Attempt to reset notebook workspace, but notebook is not enabled", "Attempt to reset notebook workspace, but notebook is not enabled",
@@ -390,7 +408,6 @@ export default class Explorer {
if (!databaseAccount) { if (!databaseAccount) {
return false; return false;
} }
try { try {
const { value: workspaces } = await listByDatabaseAccount( const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId, userContext.subscriptionId,
@@ -538,17 +555,22 @@ export default class Explorer {
} }
} }
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(
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); const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree);
promise promise
.then(() => this.resourceTree.triggerRender()) .then(() => this.resourceTree.triggerRender())
.catch((reason) => this.showOkModalDialog("Unable to upload file", reason)); .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason)));
return promise; return promise;
} }
@@ -614,51 +636,6 @@ 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.
@@ -718,7 +695,7 @@ export default class Explorer {
return true; return true;
} }
public renameNotebook(notebookFile: NotebookContentItem): void { public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): 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");
@@ -732,7 +709,9 @@ 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) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); useDialog
.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",
@@ -749,7 +728,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) this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree)
} }
notebookFile={notebookFile} notebookFile={notebookFile}
/> />
@@ -757,7 +736,7 @@ export default class Explorer {
} }
} }
public onCreateDirectory(parent: NotebookContentItem): void { public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): 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");
@@ -779,7 +758,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) this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree)
} }
notebookFile={parent} notebookFile={parent}
/> />
@@ -848,7 +827,7 @@ export default class Explorer {
} }
}; };
public deleteNotebookFile(item: NotebookContentItem): Promise<void> { public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): 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");
@@ -862,7 +841,9 @@ 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) {
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); useDialog
.getState()
.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
return Promise.reject(); return Promise.reject();
} }
@@ -879,7 +860,7 @@ export default class Explorer {
return Promise.reject(); return Promise.reject();
} }
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).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)}`)
); );
@@ -888,7 +869,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): void { public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): 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");
@@ -903,7 +884,7 @@ export default class Explorer {
}); });
this.notebookManager?.notebookContentClient this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent) .createNewNotebookFile(parent, isGithubTree)
.then((newFile: NotebookContentItem) => { .then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`); logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -943,7 +924,7 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
} }
public openNotebookTerminal(kind: ViewModels.TerminalKind) { public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
let title: string; let title: string;
switch (kind) { switch (kind) {
@@ -1063,7 +1044,10 @@ export default class Explorer {
} }
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { if (
userContext.features.phoenix === false &&
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
) {
this._openSetupNotebooksPaneForQuickstart(); this._openSetupNotebooksPaneForQuickstart();
} }
@@ -1109,10 +1093,13 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(); : this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
const isNotebookEnabled: boolean = let isNotebookEnabled = true;
userContext.authType !== AuthType.ResourceToken && if (!userContext.features.phoenix) {
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || isNotebookEnabled =
userContext.features.enableNotebooks); userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks);
}
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed()); useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
@@ -1121,11 +1108,13 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
if (isNotebookEnabled) { if (!userContext.features.notebooksTemporarilyDown) {
await this.initNotebooks(userContext.databaseAccount); if (isNotebookEnabled) {
} else if (this.notebookToImport) { await this.initNotebooks(userContext.databaseAccount);
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane } else if (this.notebookToImport) {
this._openSetupNotebooksPaneForQuickstart(); // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
this._openSetupNotebooksPaneForQuickstart();
}
} }
} }
} }

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.svg"; import CheckIcon from "../../../../images/check-1.svg";
import DeleteIcon from "../../../../images/delete.svg"; import DeleteIcon from "../../../../images/delete.svg";
import EditIcon from "../../../../images/edit.svg"; import EditIcon from "../../../../images/edit-1.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

@@ -9,6 +9,7 @@ import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
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 { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@@ -54,6 +55,14 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === true &&
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus"));
}
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
} }

View File

@@ -103,19 +103,25 @@ 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();
expect(enableNotebookBtn.disabled).toBe(false); //TODO: modify once notebooks are available
expect(enableNotebookBtn.tooltipText).toBe(""); expect(enableNotebookBtn).toBeUndefined();
//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();
expect(enableNotebookBtn.disabled).toBe(true); //TODO: modify once notebooks are available
expect(enableNotebookBtn.tooltipText).toBe( expect(enableNotebookBtn).toBeUndefined();
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks." //expect(enableNotebookBtn).toBeDefined();
); //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."
//);
}); });
}); });
@@ -192,8 +198,11 @@ 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);
expect(openMongoShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
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", () => {
@@ -203,8 +212,11 @@ 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);
expect(openMongoShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
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", () => {
@@ -290,9 +302,13 @@ 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);
expect(openCassandraShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
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", () => {
@@ -302,8 +318,11 @@ 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);
expect(openCassandraShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
}); });

View File

@@ -67,35 +67,51 @@ export function createStaticCommandBarButtons(
newCollectionBtn.children.push(newDatabaseBtn); newCollectionBtn.children.push(newDatabaseBtn);
} }
buttons.push(createDivider());
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled) {
buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container); const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)]; newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton); notebookButtons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) { if (container.notebookManager?.gitHubOAuthService) {
buttons.push(createManageGitHubAccountButton(container)); notebookButtons.push(createManageGitHubAccountButton(container));
} }
buttons.push(createOpenTerminalButton(container)); notebookButtons.push(createOpenTerminalButton(container));
if (userContext.features.phoenix === false) {
buttons.push(createNotebookWorkspaceResetButton(container)); notebookButtons.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"
) { ) {
buttons.push(createDivider()); notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") { if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container)); notebookButtons.push(createOpenCassandraTerminalButton(container));
} else { } else {
buttons.push(createOpenMongoTerminalButton(container)); notebookButtons.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()) { if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container)); buttons.push(createEnableNotebooksButton(container));
} }
} }
@@ -152,7 +168,9 @@ 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) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); if (!userContext.features.notebooksTemporarilyDown) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
@@ -160,7 +178,13 @@ export function createContextCommandBarButtons(
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo", tooltipText:
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);
} }
@@ -388,6 +412,13 @@ 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

@@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants";
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 { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent"; import { MemoryTracker } from "./MemoryTrackerComponent";
/** /**
@@ -22,6 +23,13 @@ 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(
@@ -37,6 +45,7 @@ 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,
@@ -123,8 +132,12 @@ 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 },
}; };
} }
@@ -189,3 +202,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
onRender: () => <MemoryTracker />, onRender: () => <MemoryTracker />,
}; };
}; };
export const createConnectionStatus = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus />,
};
};

View File

@@ -0,0 +1,79 @@
@import "../../../../less/Common/Constants";
.connectionStatusContainer {
cursor: default;
align-items: center;
margin: 0 9px;
border: 1px;
min-height: 44px;
> span {
padding-right: 12px;
font-size: 13px;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
}
.connectionStatusFailed{
color: #bd1919;
}
.ring-container {
position: relative;
}
.ringringGreen {
border: 3px solid green;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
.ringringYellow{
border: 3px solid #ffbf00;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
.ringringRed{
border: 3px solid #bd1919;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
margin: .4285em 0em 0em 0.07477em;
animation: pulsate 3s ease-out;
animation-iteration-count: infinite;
opacity: 0.0
}
@keyframes pulsate {
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;}
15% {opacity: 0.8;}
25% {opacity: 0.6;}
45% {opacity: 0.4;}
70% {opacity: 0.3;}
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;}
}
.locationGreenDot{
font-size: 20px;
margin-right: 0.07em;
color: green;
}
.locationYellowDot{
font-size: 20px;
margin-right: 0.07em;
color: #ffbf00;
}
.locationRedDot{
font-size: 20px;
margin-right: 0.07em;
color: #bd1919;
}

View File

@@ -0,0 +1,72 @@
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
import * as React from "react";
import { ConnectionStatusType } from "../../../Common/Constants";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
export const ConnectionStatus: React.FC = (): JSX.Element => {
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("locationYellowDot");
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow");
const toolTipContent = "Hosted runtime status.";
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isActive) {
intervalId = setInterval(() => {
const secondCounter = counter % 60;
const minuteCounter = Math.floor(counter / 60);
const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`;
const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`;
setSecond(computedSecond);
setMinute(computedMinute);
setCounter((counter) => counter + 1);
}, 1000);
}
return () => clearInterval(intervalId);
}, [isActive, counter]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
setSecond("00");
setMinute("00");
};
const connectionInfo = useNotebook((state) => state.connectionInfo);
if (!connectionInfo) {
return <></>;
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
setIsActive(true);
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer();
setStatusColor("locationGreenDot");
setStatusColorAnimation("ringringGreen");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer();
setStatusColor("locationRedDot");
setStatusColorAnimation("ringringRed");
}
return (
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<div className="ring-container">
<div className={statusColorAnimation}></div>
<Icon iconName="LocationDot" className={statusColor} />
</div>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
</Stack>
</TooltipHost>
);
};

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.svg"; import ClearIcon from "../../../../images/Clear-1.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,6 +277,10 @@ 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,12 +1,14 @@
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;
} }
@@ -20,7 +22,8 @@ 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
); );
} }
@@ -50,6 +53,7 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
} as VirtualCommandBarComponentProps; } as VirtualCommandBarComponentProps;
} }
@@ -69,6 +73,7 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
onRender: initialProps.onRender, onRender: initialProps.onRender,
}; };
}; };

View File

@@ -38,6 +38,7 @@ 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";
@@ -108,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
const q = params.toString(); const q = params.toString();
const suffix = q !== "" ? `?${q}` : ""; const suffix = q !== "" ? `?${q}` : "";
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`; const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
return url.replace(/^http(s)?/, "ws$1"); return url.replace(/^http(s)?/, "ws$1");
}; };
@@ -686,10 +687,8 @@ const handleKernelConnectionLostEpic = (
logConsoleError(msg); logConsoleError(msg);
logFailureToTelemetry(state, "Kernel restart error", msg); logFailureToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer; useDialog.getState().showOkModalDialog("kernel restarts", msg);
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
return of(EMPTY); return of(EMPTY);
} }
@@ -773,8 +772,7 @@ 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;
const explorer = window.dataExplorer; if (!TextFile.handles(mimetype)) {
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
@@ -783,7 +781,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.`;
explorer.showOkModalDialog("File cannot be rendered", msg); useDialog.getState().showOkModalDialog("File cannot be rendered", msg);
logConsoleError(msg); logConsoleError(msg);
} }
return EMPTY; return EMPTY;
@@ -803,19 +801,16 @@ const closeContentFailedToFetchEpic = (
return action$.pipe( return action$.pipe(
ofType(actions.FETCH_CONTENT_FAILED), ofType(actions.FETCH_CONTENT_FAILED),
mergeMap((action) => { mergeMap((action) => {
const explorer = window.dataExplorer; const filepath = action.payload.filepath;
if (explorer) { // Close tab and show error message
const filepath = action.payload.filepath; useTabs
// Close tab and show error message .getState()
useTabs .closeTabsByComparator(
.getState() (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
.closeTabsByComparator( );
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) const msg = `Failed to load file: ${filepath}.`;
); useDialog.getState().showOkModalDialog("Failure to load", msg);
const msg = `Failed to load file: ${filepath}.`; logConsoleError(msg);
explorer.showOkModalDialog("Failure to load", msg);
logConsoleError(msg);
}
return EMPTY; return EMPTY;
}) })
); );

View File

@@ -56,7 +56,7 @@ export class NotebookContainerClient {
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try { try {
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, { const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: authToken, Authorization: authToken,

View File

@@ -36,7 +36,10 @@ export class NotebookContentClient {
* *
* @param parent parent folder * @param parent parent folder
*/ */
public createNewNotebookFile(parent: NotebookContentItem): Promise<NotebookContentItem> { public async createNewNotebookFile(
parent: NotebookContentItem,
isGithubTree?: boolean
): 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,6 +60,8 @@ 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);
@@ -66,9 +71,9 @@ export class NotebookContentClient {
}); });
} }
public async deleteContentItem(item: NotebookContentItem): Promise<void> { public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
const path = await this.deleteNotebookFile(item.path); const path = await this.deleteNotebookFile(item.path);
useNotebook.getState().deleteNotebookItem(item); useNotebook.getState().deleteNotebookItem(item, isGithubTree);
// TODO: Delete once old resource tree is removed // TODO: Delete once old resource tree is removed
if (!path || path !== item.path) { if (!path || path !== item.path) {
@@ -91,12 +96,12 @@ 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}`);
} }
const filepath = NotebookUtil.getFilePath(parent.path, name); const filepath = NotebookUtil.getFilePath(parent.path, name);
if (await this.checkIfFilepathExists(filepath)) { if (await this.checkIfFilepathExists(filepath)) {
throw new Error(`File already exists: ${filepath}`); throw new Error(`File already exists: ${filepath}`);
@@ -115,6 +120,8 @@ 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);
@@ -137,7 +144,11 @@ export class NotebookContentClient {
* @param sourcePath * @param sourcePath
* @param targetName is not prefixed with path * @param targetName is not prefixed with path
*/ */
public renameNotebook(item: NotebookContentItem, targetName: string): Promise<NotebookContentItem> { public renameNotebook(
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) {
@@ -163,6 +174,9 @@ 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;
}); });
} }
@@ -172,7 +186,11 @@ export class NotebookContentClient {
* @param parent * @param parent
* @param newDirectoryName basename of the new directory * @param newDirectoryName basename of the new directory
*/ */
public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise<NotebookContentItem> { public async createDirectory(
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}`);
} }
@@ -199,8 +217,11 @@ 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,6 +18,7 @@ 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";
@@ -172,31 +173,33 @@ export default class NotebookManager {
if (error.status === HttpStatusCodes.Unauthorized) { if (error.status === HttpStatusCodes.Unauthorized) {
this.gitHubOAuthService.resetToken(); this.gitHubOAuthService.resetToken();
this.params.container.showOkCancelModalDialog( useDialog
undefined, .getState()
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", .showOkCancelModalDialog(
"Connect to GitHub", undefined,
() => "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
useSidePanel "Connect to GitHub",
.getState() () =>
.openSidePanel( useSidePanel
"Connect to GitHub", .getState()
<GitHubReposPanel .openSidePanel(
explorer={this.params.container} "Connect to GitHub",
gitHubClientProp={this.params.container.notebookManager.gitHubClient} <GitHubReposPanel
junoClientProp={this.junoClient} explorer={this.params.container}
/> 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";
this.params.container.showOkCancelModalDialog( useDialog.getState().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,6 +14,7 @@ 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";
@@ -107,6 +108,7 @@ 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,6 +19,12 @@
} }
} }
.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,6 +5,7 @@ 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;
@@ -12,6 +13,7 @@ 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;
} }
@@ -20,6 +22,7 @@ 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;
} }
@@ -47,6 +50,7 @@ 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>
); );
@@ -75,6 +79,7 @@ const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((s
status, status,
executionCount, executionCount,
isHovered, isHovered,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -0,0 +1,27 @@
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,5 +1,6 @@
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";
@@ -23,15 +24,18 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
</div> </div>
); );
} else if (props.isHovered) { } else if (props.isHovered) {
const playButtonText = "Run cell"; const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell";
return ( return (
<IconButton <div className={props.isRunDisabled ? "disabledRunCellButton" : ""}>
className="runCellButton" <IconButton
iconProps={{ iconName: "MSNVideosSolid" }} className="runCellButton"
title={playButtonText} iconProps={{ iconName: "MSNVideosSolid" }}
ariaLabel={playButtonText} title={playButtonText}
onClick={props.runCell} ariaLabel={playButtonText}
/> 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,6 +36,7 @@ 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> {
@@ -43,12 +44,16 @@ 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);
@@ -223,6 +228,7 @@ 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

@@ -0,0 +1,60 @@
// 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,7 +11,6 @@ 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";
@@ -123,9 +122,10 @@ 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: monitor.getItem().id, id: item.id,
destinationId: props.id, destinationId: props.id,
above: hoverUpperHalf, above: hoverUpperHalf,
contentRef: props.contentRef, contentRef: props.contentRef,

View File

@@ -4,6 +4,7 @@ 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;
@@ -14,6 +15,7 @@ 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 {
@@ -59,8 +61,13 @@ 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") {
@@ -125,6 +132,7 @@ 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,4 +1,5 @@
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";
@@ -11,6 +12,8 @@ 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.
*/ */
@@ -153,6 +156,16 @@ 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

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,93 @@
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

@@ -0,0 +1,22 @@
// 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

@@ -28,6 +28,8 @@ interface NotebookState {
myNotebooksContentRoot: NotebookContentItem; myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem;
connectionInfo: DataModels.ContainerConnectionInfo;
notebookFolderName: string;
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;
@@ -36,12 +38,15 @@ interface NotebookState {
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void; setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
setIsShellEnabled: (isShellEnabled: boolean) => void; setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void; setNotebookBasePath: (notebookBasePath: string) => void;
setNotebookFolderName: (notebookFolderName: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>; refreshNotebooksEnabledStateForAccount: () => Promise<void>;
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
updateNotebookItem: (item: NotebookContentItem) => void; insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
deleteNotebookItem: (item: NotebookContentItem) => void; updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -64,6 +69,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
myNotebooksContentRoot: undefined, myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined, galleryContentRoot: undefined,
connectionInfo: undefined,
notebookFolderName: 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) =>
@@ -74,6 +81,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }), setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }), setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }), setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => { refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
const { databaseAccount, authType } = userContext; const { databaseAccount, authType } = userContext;
if ( if (
@@ -141,23 +149,36 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
return undefined; return undefined;
}, },
updateNotebookItem: (item: NotebookContentItem): void => { insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = cloneDeep(get().myNotebooksContentRoot); 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); const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item); parentItem.children.push(item);
item.parent = parentItem; item.parent = parentItem;
set({ myNotebooksContentRoot: root }); isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
}, },
deleteNotebookItem: (item: NotebookContentItem): void => { deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = cloneDeep(get().myNotebooksContentRoot); const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent); const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
set({ myNotebooksContentRoot: root }); isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
}, },
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => { initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = { const myNotebooksContentRoot = {
name: "My Notebooks", name: get().notebookFolderName,
path: get().notebookBasePath, path: get().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -173,6 +194,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
} }
: undefined; : undefined;
set({ set({
myNotebooksContentRoot, myNotebooksContentRoot,
galleryContentRoot, galleryContentRoot,
@@ -216,6 +238,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
path: "PsuedoDir", path: "PsuedoDir",
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
children: [], children: [],
parent: gitHubNotebooksContentRoot,
}; };
pinnedRepo.branches.forEach((branch) => { pinnedRepo.branches.forEach((branch) => {
@@ -223,6 +246,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
name: branch.name, name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
parent: repoTreeItem,
}); });
}); });
@@ -232,4 +256,5 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ gitHubNotebooksContentRoot }); set({ gitHubNotebooksContentRoot });
} }
}, },
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }),
})); }));

View File

@@ -113,11 +113,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: "", collectionId: "",
enableIndexing: true, enableIndexing: true,
isSharded: userContext.apiType !== "Tables", isSharded: userContext.apiType !== "Tables",
partitionKey: partitionKey: this.getPartitionKey(),
(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,
@@ -165,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
true true
).toLocaleLowerCase()}.`} ).toLocaleLowerCase()}.`}
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -214,6 +210,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelTextField" className="panelTextField"
aria-label="New database id" aria-label="New database id"
autoFocus autoFocus
tabIndex={0}
value={this.state.newDatabaseId} value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value }) this.setState({ newDatabaseId: event.target.value })
@@ -240,7 +237,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
true true
).toLocaleLowerCase()} within the database.`} ).toLocaleLowerCase()} within the database.`}
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
)} )}
@@ -283,7 +280,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -366,7 +363,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data." "Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
} }
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -413,7 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getPartitionKeyTooltipText()} content={this.getPartitionKeyTooltipText()}
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -471,7 +468,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
does not count towards the throughput you provisioned for the database. This throughput amount will be does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`} billed in addition to the throughput amount you provisioned at the database level.`}
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
)} )}
@@ -501,7 +498,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
per partition key." per partition key."
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -554,6 +551,72 @@ 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" tabIndex={0} />
</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"
@@ -575,7 +638,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development." content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
> >
<Icon iconName="Info" className="panelInfoIcon" /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -608,72 +671,6 @@ 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>
)} )}
@@ -815,6 +812,19 @@ 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

@@ -5,6 +5,7 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -75,6 +76,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.owner, selectedLocation.owner,
selectedLocation.repo selectedLocation.repo
)} - ${selectedLocation.branch}`; )} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
destination = "My Notebooks Scratch";
} }
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
@@ -98,6 +101,7 @@ 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 = {
@@ -105,21 +109,23 @@ 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: ResourceTreeAdapter.GitHubReposTitle, name: selectedLocation.branch,
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); return container.uploadFile(name, content, parent, isGithubTree);
}; };
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => { const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {

View File

@@ -12,6 +12,7 @@ import {
import React, { FormEvent, FunctionComponent } from "react"; import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient"; import { IPinnedRepo } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
interface Location { interface Location {
@@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
const getDropDownOptions = (): IDropdownOption[] => { const getDropDownOptions = (): IDropdownOption[] => {
const options: IDropdownOption[] = []; const options: IDropdownOption[] = [];
options.push({ options.push({
key: "MyNotebooks-Item", key: "MyNotebooks-Item",
text: ResourceTreeAdapter.MyNotebooksTitle, text: useNotebook.getState().notebookFolderName,
title: ResourceTreeAdapter.MyNotebooksTitle, title: useNotebook.getState().notebookFolderName,
data: { data: {
type: "MyNotebooks", type: "MyNotebooks",
} as Location, } as Location,

View File

@@ -108,6 +108,8 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
submitButtonText: "OK", submitButtonText: "OK",
onSubmit, onSubmit,
}; };
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
@@ -123,6 +125,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setInputCollectionName(newInput); setInputCollectionName(newInput);
}} }}
ariaLabel={confirmContainer}
/> />
</div> </div>
{shouldRecordFeedback() && ( {shouldRecordFeedback() && (
@@ -142,6 +145,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setDeleteCollectionFeedback(newInput); setDeleteCollectionFeedback(newInput);
}} }}
ariaLabel={reasonInfo}
/> />
</div> </div>
)} )}

View File

@@ -40,6 +40,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</span> </span>
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
@@ -53,6 +54,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value="" value=""
> >
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
deferredValidationTime={200} deferredValidationTime={200}
id="confirmCollectionId" id="confirmCollectionId"
@@ -346,6 +348,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-57" className="ms-TextField-field field-57"
id="confirmCollectionId" id="confirmCollectionId"

View File

@@ -118,7 +118,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
message: message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.", "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
}; };
const confirmDatabase = "Confirm by typing the database id";
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?";
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />} {!formError && <PanelInfoErrorComponent {...errorProps} />}
@@ -133,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setDatabaseInput(newInput); setDatabaseInput(newInput);
}} }}
ariaLabel={confirmDatabase}
/> />
</div> </div>
{isLastNonEmptyDatabase() && ( {isLastNonEmptyDatabase() && (
@@ -151,6 +153,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setDatabaseFeedbackInput(newInput); setDatabaseFeedbackInput(newInput);
}} }}
ariaLabel={reasonInfo}
/> />
</div> </div>
)} )}

View File

@@ -153,7 +153,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
selectedKey={paramKeyValue && paramKeyValue.key} selectedKey={paramKeyValue && paramKeyValue.key}
/> />
))} ))}
<Stack horizontal onClick={addNewParamAtLastIndex}> <Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" /> <Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text> <Text className="addNewParamStyle">Add New Param</Text>
</Stack> </Stack>

View File

@@ -59,30 +59,36 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
onChange={onParamKeyChange} onChange={onParamKeyChange}
options={options} options={options}
styles={dropdownStyles} styles={dropdownStyles}
tabIndex={0}
/> />
<TextField <TextField
label={inputLabel && inputLabel} label={inputLabel && inputLabel}
id="confirmCollectionId" id="confirmCollectionId"
autoFocus
value={paramValue} value={paramValue}
onChange={onParamValueChange} onChange={onParamValueChange}
/> />
{isAddRemoveVisible && ( {isAddRemoveVisible && (
<> <>
<Image <div tabIndex={0}>
{...imageProps} <Image
src={EntityCancelIcon} {...imageProps}
alt="Delete param" src={EntityCancelIcon}
id="deleteparam" alt="Delete param"
onClick={onDeleteParamKeyPress} id="deleteparam"
/> role="button"
<Image onClick={onDeleteParamKeyPress}
{...imageProps} />
src={AddPropertyIcon} </div>
alt="Add param" <div tabIndex={0}>
id="addparam" <Image
onClick={onAddNewParamKeyPress} {...imageProps}
/> src={AddPropertyIcon}
alt="Add param"
id="addparam"
role="button"
onClick={onAddNewParamKeyPress}
/>
</div>
</> </>
)} )}
</Stack> </Stack>

View File

@@ -23,6 +23,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -3,13 +3,12 @@
.panelFormWrapper { .panelFormWrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; min-height: 100%;
.panelMainContent { .panelMainContent {
flex-grow: 1; flex-grow: 1;
padding: 0 34px; padding: 0 34px;
margin: 20px 0; margin: 20px 0;
overflow: auto;
& > :not(.collapsibleSection) { & > :not(.collapsibleSection) {
margin-bottom: @DefaultSpace; margin-bottom: @DefaultSpace;

View File

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

View File

@@ -195,7 +195,6 @@ export const SettingsPane: FunctionComponent = () => {
step={1} step={1}
className="textfontclr" className="textfontclr"
role="textbox" role="textbox"
tabIndex={0}
id="max-degree" id="max-degree"
value={"" + maxDegreeOfParallelism} value={"" + maxDegreeOfParallelism}
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)} onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}

View File

@@ -123,7 +123,6 @@ exports[`Settings Pane should render Default properly 1`] = `
onValidate={[Function]} onValidate={[Function]}
role="textbox" role="textbox"
step={1} step={1}
tabIndex={0}
value="6" value="6"
/> />
</div> </div>

View File

@@ -13,6 +13,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],

View File

@@ -91,9 +91,7 @@ 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 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."
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

@@ -363,6 +363,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</span> </span>
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the database id"
autoFocus={true} autoFocus={true}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
@@ -375,6 +376,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
} }
> >
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the database id"
autoFocus={true} autoFocus={true}
deferredValidationTime={200} deferredValidationTime={200}
id="confirmDatabaseId" id="confirmDatabaseId"
@@ -667,6 +669,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Confirm by typing the database id"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-60" className="ms-TextField-field field-60"
id="confirmDatabaseId" id="confirmDatabaseId"
@@ -707,6 +710,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</span> </span>
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
@@ -720,6 +724,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
} }
> >
<TextFieldBase <TextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
deferredValidationTime={200} deferredValidationTime={200}
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
multiline={true} multiline={true}
@@ -1013,6 +1018,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<textarea <textarea
aria-invalid={false} aria-invalid={false}
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
className="ms-TextField-field field-71" className="ms-TextField-field field-71"
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
onBlur={[Function]} onBlur={[Function]}

View File

@@ -83,7 +83,11 @@ 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();
const recentItems = this.createRecentItems(); let 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;
@@ -219,7 +223,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}); });
} }
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) {
heroes.push({ heroes.push({
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
title: "New Notebook", title: "New Notebook",

View File

@@ -1,5 +1,6 @@
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";
@@ -69,19 +70,28 @@ export default class TableCommands {
return null; // Error return null; // Error
} }
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
let deleteMessage: string = "Are you sure you want to delete the selected entities?"; const deleteMessage: string =
if (userContext.apiType === "Cassandra") { userContext.apiType === "Cassandra"
deleteMessage = "Are you sure you want to delete the selected rows?"; ? "Are you sure you want to delete the selected rows?"
} : "Are you sure you want to delete the selected entities?";
if (window.confirm(deleteMessage)) {
viewModel.queryTablesTab.container.tableDataClient useDialog.getState().showOkCancelModalDialog(
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) "Confirm delete",
.then((results: any) => { deleteMessage,
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { "Delete",
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,6 +21,7 @@ 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";
@@ -228,7 +229,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) {
window.alert(getErrorMessage(error)); useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
} }
} }
@@ -252,10 +253,23 @@ export default class ConflictsTab extends TabsBase {
} }
public onAcceptChangesClick = async (): Promise<void> => { public onAcceptChangesClick = async (): Promise<void> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { if (this.isEditorDirty()) {
return; useDialog
.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);
@@ -318,7 +332,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);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.ResolveConflict, Action.ResolveConflict,
{ {
@@ -372,7 +386,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);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteConflict, Action.DeleteConflict,
{ {
@@ -662,11 +676,6 @@ 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,6 +25,7 @@ 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";
@@ -378,7 +379,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) {
window.alert(getErrorMessage(error)); useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
} }
} }
@@ -401,18 +402,29 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
} }
public onNewDocumentClick = (): Q.Promise<any> => { public onNewDocumentClick = (): void => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { if (this.isEditorDirty()) {
return Q(); useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
() => this.initializeNewDocument(),
"Cancel",
undefined
);
} else {
this.initializeNewDocument();
} }
this.selectedDocumentId(null); };
private initializeNewDocument = (): void => {
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> => {
@@ -453,7 +465,7 @@ export default class DocumentsTab extends TabsBase {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateDocument, Action.CreateDocument,
{ {
@@ -516,7 +528,7 @@ export default class DocumentsTab extends TabsBase {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.UpdateDocument, Action.UpdateDocument,
{ {
@@ -546,9 +558,16 @@ 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 ?";
if (window.confirm(msg)) { useDialog
await this._deleteDocument(selectedDocumentId); .getState()
} .showOkCancelModalDialog(
"Confirm delete",
msg,
"Delete",
async () => await this._deleteDocument(selectedDocumentId),
"Cancel",
undefined
);
}; };
public onValidDocumentEdit(): Q.Promise<any> { public onValidDocumentEdit(): Q.Promise<any> {
@@ -617,11 +636,6 @@ 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,6 +16,7 @@ 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";
@@ -111,7 +112,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateDocument, Action.CreateDocument,
{ {
@@ -169,7 +170,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.UpdateDocument, Action.UpdateDocument,
{ {

View File

@@ -17,12 +17,14 @@ 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";
@@ -59,14 +61,16 @@ export default class NotebookTabV2 extends NotebookTabBase {
}; };
if (this.notebookComponentAdapter.isContentDirty()) { if (this.notebookComponentAdapter.isContentDirty()) {
this.container.showOkCancelModalDialog( useDialog
"Close without saving?", .getState()
`File has unsaved changes, close without saving?`, .showOkCancelModalDialog(
"Close", "Close without saving?",
cleanup, `File has unsaved changes, close without saving?`,
"Cancel", "Close",
undefined cleanup,
); "Cancel",
undefined
);
return Q.resolve(null); return Q.resolve(null);
} else { } else {
cleanup(); cleanup();
@@ -84,11 +88,13 @@ 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";
@@ -105,8 +111,6 @@ 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";
@@ -187,9 +191,10 @@ 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: false, disabled: isNotebookUntrusted,
children: [ children: [
{ {
iconSrc: RunIcon, iconSrc: RunIcon,

View File

@@ -1,4 +1,4 @@
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react"; import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
@@ -120,21 +120,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
constructor(props: IQueryTabComponentProps) { constructor(props: IQueryTabComponentProps) {
super(props); super(props);
const columns: IColumn[] = [ const columns: IColumn[] = [
{
key: "column1",
name: "",
minWidth: 16,
maxWidth: 16,
data: String,
fieldName: "toolTip",
onRender: this.onRenderColumnItem,
},
{ {
key: "column2", key: "column2",
name: "METRIC", name: "METRIC",
minWidth: 200, minWidth: 200,
data: String, data: String,
fieldName: "metric", fieldName: "metric",
onRender: this.onRenderColumnItem,
}, },
{ {
key: "column3", key: "column3",
@@ -206,7 +198,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
public onRenderColumnItem(item: IDocument): JSX.Element { public onRenderColumnItem(item: IDocument): JSX.Element {
if (item.toolTip !== "") { if (item.toolTip !== "") {
return <InfoTooltip>{`${item.toolTip}`}</InfoTooltip>; return (
<>
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
</>
);
} else { } else {
return undefined; return undefined;
} }

View File

@@ -1,5 +1,5 @@
import ko from "knockout"; import ko from "knockout";
import React, { useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable"; import { useObservable } from "../../hooks/useObservable";
@@ -32,7 +32,13 @@ export const Tabs = (): JSX.Element => {
function TabNav({ tab, active }: { tab: Tab; active: boolean }) { function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (active && focusTab.current) {
focusTab.current.focus();
}
});
return ( return (
<li <li
onMouseOver={() => setHovering(true)} onMouseOver={() => setHovering(true)}
@@ -46,6 +52,7 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
aria-controls={tab.tabId} aria-controls={tab.tabId}
tabIndex={0} tabIndex={0}
role="tab" role="tab"
ref={focusTab}
> >
<span className="tabNavContentContainer"> <span className="tabNavContentContainer">
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}> <a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>

View File

@@ -291,7 +291,7 @@ export default class UserDefinedFunctionTabContent extends Component<
language={"javascript"} language={"javascript"}
content={udfBody} content={udfBody}
isReadOnly={false} isReadOnly={false}
ariaLabel={"Graph JSON"} ariaLabel={"User defined function body"}
onContentChanged={this.handleUdfBodyChange} onContentChanged={this.handleUdfBodyChange}
/> />
</div> </div>

View File

@@ -1,12 +1,11 @@
import Q from "q"; import { extractPartitionKey } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
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"; 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";
export default class ConflictId { export default class ConflictId {
public container: ConflictsTab; public container: ConflictsTab;
@@ -50,13 +49,20 @@ export default class ConflictId {
} }
public click() { public click() {
if ( if (this.container.isEditorDirty()) {
!this.container.isEditorDirty() || useDialog
window.confirm("Your unsaved changes will be lost. Do you want to continue?") .getState()
) { .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

@@ -1,5 +1,6 @@
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 {
@@ -28,10 +29,20 @@ export default class DocumentId {
} }
public click() { public click() {
if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) { if (this.container.isEditorDirty()) {
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,5 +1,6 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg"; import GalleryIcon from "../../../images/GalleryIcon.svg";
@@ -10,7 +11,7 @@ import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import { Areas } from "../../Common/Constants"; import { Areas, ConnectionStatusType, Notebook } 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";
@@ -24,6 +25,7 @@ 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";
@@ -54,7 +56,16 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
galleryContentRoot, galleryContentRoot,
gitHubNotebooksContentRoot, gitHubNotebooksContentRoot,
updateNotebookItem, updateNotebookItem,
} = useNotebook(); } = useNotebook(
(state) => ({
isNotebookEnabled: state.isNotebookEnabled,
myNotebooksContentRoot: state.myNotebooksContentRoot,
galleryContentRoot: state.galleryContentRoot,
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
updateNotebookItem: state.updateNotebookItem,
}),
shallow
);
const { activeTab, refreshActiveTab } = useTabs(); const { activeTab, refreshActiveTab } = useTabs();
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const pseudoDirPath = "PsuedoDir"; const pseudoDirPath = "PsuedoDir";
@@ -110,23 +121,32 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children: [], children: [],
}; };
if (galleryContentRoot) { if (userContext.features.notebooksTemporarilyDown) {
notebooksTree.children.push(buildGalleryNotebooksTree()); notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} } else {
if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (myNotebooksContentRoot) { if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) {
notebooksTree.children.push(buildMyNotebooksTree()); 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(true));
}
} }
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(buildGitHubNotebooksTree());
}
return notebooksTree; return notebooksTree;
}; };
const buildNotebooksTemporarilyDownTree = (): TreeNode => {
return {
label: Notebook.temporarilyDownMsg,
className: "clickDisabled",
};
};
const buildGalleryNotebooksTree = (): TreeNode => { const buildGalleryNotebooksTree = (): TreeNode => {
return { return {
label: "Gallery", label: "Gallery",
@@ -156,7 +176,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return myNotebooksTree; return myNotebooksTree;
}; };
const buildGitHubNotebooksTree = (): TreeNode => { const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot, gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
@@ -165,10 +185,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
} },
true
); );
const manageGitContextMenu: TreeNodeMenuItem[] = [
gitHubNotebooksTree.contextMenu = [
{ {
label: "Manage GitHub settings", label: "Manage GitHub settings",
onClick: () => onClick: () =>
@@ -193,7 +213,23 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}, },
}, },
]; ];
const connectGitContextMenu: TreeNodeMenuItem[] = [
{
label: "Connect to GitHub",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
];
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
gitHubNotebooksTree.isExpanded = true; gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true; gitHubNotebooksTree.isAlphaSorted = true;
@@ -201,9 +237,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}; };
const buildChildNodes = ( const buildChildNodes = (
container: Explorer,
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode[] => { ): TreeNode[] => {
if (!item || !item.children) { if (!item || !item.children) {
return []; return [];
@@ -211,8 +247,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return item.children.map((item) => { return item.children.map((item) => {
const result = const result =
item.type === NotebookContentItemType.Directory item.type === NotebookContentItemType.Directory
? buildNotebookDirectoryNode(item, onFileClick) ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree)
: buildNotebookFileNode(item, onFileClick); : buildNotebookFileNode(item, onFileClick, isGithubTree);
result.timestamp = item.timestamp; result.timestamp = item.timestamp;
return result; return result;
}); });
@@ -221,7 +257,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const buildNotebookFileNode = ( const buildNotebookFileNode = (
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode => { ): TreeNode => {
return { return {
label: item.name, label: item.name,
@@ -238,30 +275,36 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: createFileContextMenu(container, item), contextMenu: createFileContextMenu(container, item, isGithubTree),
data: item, data: item,
}; };
}; };
const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { const createFileContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [ let items: TreeNodeMenuItem[] = [
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item), onClick: () => container.renameNotebook(item, isGithubTree),
}, },
{ {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
container.showOkCancelModalDialog( useDialog
"Confirm delete", .getState()
`Are you sure you want to delete "${item.name}"`, .showOkCancelModalDialog(
"Delete", "Confirm delete",
() => container.deleteNotebookFile(item), `Are you sure you want to delete "${item.name}"`,
"Cancel", "Delete",
undefined () => container.deleteNotebookFile(item, isGithubTree),
); "Cancel",
undefined
);
}, },
}, },
{ {
@@ -308,41 +351,47 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
} }
}; };
const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { const createDirectoryContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [ let items: TreeNodeMenuItem[] = [
{ {
label: "Refresh", label: "Refresh",
iconSrc: RefreshIcon, iconSrc: RefreshIcon,
onClick: () => loadSubitems(item), onClick: () => loadSubitems(item, isGithubTree),
}, },
{ {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
container.showOkCancelModalDialog( useDialog
"Confirm delete", .getState()
`Are you sure you want to delete "${item.name}?"`, .showOkCancelModalDialog(
"Delete", "Confirm delete",
() => container.deleteNotebookFile(item), `Are you sure you want to delete "${item.name}?"`,
"Cancel", "Delete",
undefined () => container.deleteNotebookFile(item, isGithubTree),
); "Cancel",
undefined
);
}, },
}, },
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item), onClick: () => container.renameNotebook(item, isGithubTree),
}, },
{ {
label: "New Directory", label: "New Directory",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => container.onCreateDirectory(item), onClick: () => container.onCreateDirectory(item, isGithubTree),
}, },
{ {
label: "New Notebook", label: "New Notebook",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => container.onNewNotebookClicked(item), onClick: () => container.onNewNotebookClicked(item, isGithubTree),
}, },
{ {
label: "Upload File", label: "Upload File",
@@ -367,7 +416,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const buildNotebookDirectoryNode = ( const buildNotebookDirectoryNode = (
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode => { ): TreeNode => {
return { return {
label: item.name, label: item.name,
@@ -377,7 +427,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
isLeavesParentsSeparate: true, isLeavesParentsSeparate: true,
onClick: () => { onClick: () => {
if (!item.children) { if (!item.children) {
loadSubitems(item); loadSubitems(item, isGithubTree);
} }
}, },
isSelected: () => { isSelected: () => {
@@ -390,9 +440,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined, contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined,
data: item, data: item,
children: buildChildNodes(container, item, onFileClick), children: buildChildNodes(item, onFileClick, isGithubTree),
}; };
}; };
@@ -477,7 +527,12 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
}); });
if (isNotebookEnabled && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed()) { if (
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
!userContext.features.notebooksTemporarilyDown
) {
children.push({ children.push({
label: "Schema (Preview)", label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection), onClick: collection.onSchemaAnalyzerClick.bind(collection),
@@ -694,9 +749,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return traverse(schema); return traverse(schema);
}; };
const loadSubitems = async (item: NotebookContentItem): Promise<void> => { const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise<void> => {
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
updateNotebookItem(updatedItem); updateNotebookItem(updatedItem, isGithubTree);
}; };
const dataRootNode = buildDataTree(); const dataRootNode = buildDataTree();

View File

@@ -27,6 +27,7 @@ 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";
@@ -44,6 +45,7 @@ import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter { export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks"; public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
public static readonly GitHubReposTitle = "GitHub repos"; public static readonly GitHubReposTitle = "GitHub repos";
private static readonly DataTitle = "DATA"; private static readonly DataTitle = "DATA";
@@ -129,9 +131,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
path: "Gallery", path: "Gallery",
type: NotebookContentItemType.File, type: NotebookContentItemType.File,
}; };
this.myNotebooksContentRoot = { this.myNotebooksContentRoot = {
name: ResourceTreeAdapter.MyNotebooksTitle, name: useNotebook.getState().notebookFolderName,
path: useNotebook.getState().notebookBasePath, path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -145,16 +146,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
}) })
); );
} }
this.gitHubNotebooksContentRoot = {
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { name: ResourceTreeAdapter.GitHubReposTitle,
this.gitHubNotebooksContentRoot = { path: ResourceTreeAdapter.PseudoDirPath,
name: ResourceTreeAdapter.GitHubReposTitle, type: NotebookContentItemType.Directory,
path: ResourceTreeAdapter.PseudoDirPath, };
type: NotebookContentItemType.Directory,
};
} else {
this.gitHubNotebooksContentRoot = undefined;
}
return Promise.all(refreshTasks); return Promise.all(refreshTasks);
} }
@@ -712,14 +708,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
this.container.showOkCancelModalDialog( useDialog
"Confirm delete", .getState()
`Are you sure you want to delete "${item.name}"`, .showOkCancelModalDialog(
"Delete", "Confirm delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), `Are you sure you want to delete "${item.name}"`,
"Cancel", "Delete",
undefined () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
); "Cancel",
undefined
);
}, },
}, },
{ {
@@ -777,14 +775,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
this.container.showOkCancelModalDialog( useDialog
"Confirm delete", .getState()
`Are you sure you want to delete "${item.name}?"`, .showOkCancelModalDialog(
"Delete", "Confirm delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), `Are you sure you want to delete "${item.name}?"`,
"Cancel", "Delete",
undefined () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
); "Cancel",
undefined
);
}, },
}, },
{ {

View File

@@ -8,6 +8,7 @@ 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";
@@ -138,16 +139,21 @@ export default class StoredProcedure {
} }
}; };
public delete() { public delete() {
if (!window.confirm("Are you sure you want to delete the stored procedure?")) { useDialog.getState().showOkCancelModalDialog(
return; "Confirm delete",
} "Are you sure you want to delete the stored procedure?",
"Delete",
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
this.collection.children.remove(this); () => {
useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}
);
}, },
(reason) => {} "Cancel",
undefined
); );
} }

View File

@@ -6,6 +6,7 @@ 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";
@@ -99,16 +100,21 @@ export default class Trigger {
}; };
public delete() { public delete() {
if (!window.confirm("Are you sure you want to delete the trigger?")) { useDialog.getState().showOkCancelModalDialog(
return; "Confirm delete",
} "Are you sure you want to delete the trigger?",
"Delete",
deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
this.collection.children.remove(this); () => {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}
);
}, },
(reason) => {} "Cancel",
undefined
); );
} }
} }

View File

@@ -6,6 +6,7 @@ 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";
@@ -95,18 +96,23 @@ export default class UserDefinedFunction {
} }
public delete() { public delete() {
if (!window.confirm("Are you sure you want to delete the user defined function?")) { useDialog.getState().showOkCancelModalDialog(
return; "Confirm delete",
} "Are you sure you want to delete the user defined function?",
"Delete",
deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
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

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

View File

@@ -61,7 +61,6 @@ 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;
@@ -359,7 +358,6 @@ 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>> {
@@ -370,7 +368,6 @@ 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

@@ -0,0 +1,57 @@
{
"GraphAPIDescription": "Provision a Graph API Compute for your Azure Cosmos DB account.",
"GraphAPICompute": "Graph GraphAPI Compute",
"Provisioned": "Provisioned",
"Deprovisioned": "Deprovisioned",
"Compute": "Compute",
"GremlinV2": "GremlinV2",
"LearnAboutCompute": "Learn more about GraphAPI Compute.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the GraphAPI Compute.",
"ComputePricing": "Learn more about GraphAPI Compute pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Graph GraphAPI Compute resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "GraphAPI Compute resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "GraphAPI Compute resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "GraphAPI Compute resource provisioning failed.",
"UpdateMessage": "GraphAPI Compute resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "GraphAPI Compute resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "GraphAPI Compute resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "GraphAPI Compute resource updation failed.",
"DeleteMessage": "GraphAPI Compute resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "GraphAPI Compute resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "GraphAPI Compute resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "GraphAPI Compute resource deletion failed.",
"CannotSave": "Cannot save the changes to the GraphAPI Compute resource at the moment.",
"GraphAccountEndpoint": "Graph Account Endpoint",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"ApproximateCost": "Approximate Cost Per Hour",
"CostText": "Hourly cost of the GraphAPI Compute resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the GraphAPI Compute, use the connection string shown in ",
"KeysBlade": "the keys blade.",
"MetricsString": "Metrics",
"MetricsText": "Monitor the CPU and memory usage for the GraphAPI Compute instances in ",
"MetricsBlade": "the metrics blade.",
"MonitorUsage": "Monitor Usage",
"ResizingDecisionText": "Number of instances has to be 1 during provisioning. Instances can only be incremented by 1 at once. ",
"ResizingDecisionLink": "Learn more about GraphAPI Compute sizing.",
"WarningBannerOnUpdate": "Adding or modifying GraphAPI Compute instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the GraphAPI Compute, you will not be able to connect to the Graph API account."
}

View File

@@ -37,6 +37,7 @@ import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";

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