Compare commits
74 Commits
users/artr
...
eslint/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a1e9f7231 | ||
|
|
f968f57543 | ||
|
|
6ca8e3c6f4 | ||
|
|
f7e7240010 | ||
|
|
acc095a482 | ||
|
|
288e6410f3 | ||
|
|
9ac3392271 | ||
|
|
9a908dde9a | ||
|
|
ddd2e63fe7 | ||
|
|
34c59b4872 | ||
|
|
7d28af4fc7 | ||
|
|
50b99ceb42 | ||
|
|
15a26d6500 | ||
|
|
a8150af269 | ||
|
|
6a9a0156a3 | ||
|
|
ead28f043f | ||
|
|
b05e5a2145 | ||
|
|
5e8aa491ba | ||
|
|
a730c08292 | ||
|
|
3dce5cd129 | ||
|
|
7c186c3ef2 | ||
|
|
d8840a0dfd | ||
|
|
f853c4ec2f | ||
|
|
9bf5f48165 | ||
|
|
0b2a204b70 | ||
|
|
c28593b752 | ||
|
|
4cbbef9574 | ||
|
|
300c952a9b | ||
|
|
38c3761260 | ||
|
|
3032f689b6 | ||
|
|
8b30af3d9e | ||
|
|
e10240bd7a | ||
|
|
ae9c27795e | ||
|
|
d7997d716e | ||
|
|
af0dc3094b | ||
|
|
665270296f | ||
|
|
2d945c8231 | ||
|
|
8866976bb4 | ||
|
|
d10f3c69f1 | ||
|
|
7e4f030547 | ||
|
|
05f46dd635 | ||
|
|
65882ea831 | ||
|
|
95c9b7ee31 | ||
|
|
39dd293fc1 | ||
|
|
8eeda41021 | ||
|
|
960cd9fc55 | ||
|
|
9ec0ac9f54 | ||
|
|
b66aeb814a | ||
|
|
410f582378 | ||
|
|
678ca51c77 | ||
|
|
2dfd90ca0f | ||
|
|
4110be10bd | ||
|
|
6e55e397b3 | ||
|
|
24d0a16123 | ||
|
|
f8ac36962b | ||
|
|
51f3f9a718 | ||
|
|
ee4404c439 | ||
|
|
b65456f754 | ||
|
|
56699ccb1b | ||
|
|
042f980b89 | ||
|
|
7e0c4b7290 | ||
|
|
a66fc06dad | ||
|
|
a8bc821dec | ||
|
|
1394aae944 | ||
|
|
fecac5625a | ||
|
|
dc21032d69 | ||
|
|
39a67dbc98 | ||
|
|
ed9cf01b50 | ||
|
|
e443d17b2e | ||
|
|
401660ae15 | ||
|
|
c5e4ee9c2b | ||
|
|
913fec4e69 | ||
|
|
6d46e48490 | ||
|
|
afacde4041 |
15
.env.example
@@ -1,16 +1 @@
|
||||
PORTAL_RUNNER_USERNAME=
|
||||
PORTAL_RUNNER_PASSWORD=
|
||||
PORTAL_RUNNER_SUBSCRIPTION=
|
||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT=
|
||||
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY=
|
||||
PORTAL_RUNNER_CONNECTION_STRING=
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||
CASSANDRA_CONNECTION_STRING=
|
||||
MONGO_CONNECTION_STRING=
|
||||
TABLES_CONNECTION_STRING=
|
||||
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
|
||||
@@ -21,16 +21,8 @@ src/Common/MongoUtility.ts
|
||||
src/Common/NotificationsClientBase.ts
|
||||
src/Common/QueriesClient.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.ts
|
||||
src/Controls/Heatmap/HeatmapDatatypes.ts
|
||||
src/Definitions/datatables.d.ts
|
||||
src/Definitions/gif.d.ts
|
||||
src/Definitions/globals.d.ts
|
||||
@@ -44,35 +36,14 @@ src/Definitions/png.d.ts
|
||||
src/Definitions/svg.d.ts
|
||||
src/Explorer/ComponentRegisterer.test.ts
|
||||
src/Explorer/ComponentRegisterer.ts
|
||||
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.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/ErrorDisplayComponent/ErrorDisplayComponent.ts
|
||||
src/Explorer/Controls/InputTypeahead/InputTypeahead.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.ts
|
||||
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
||||
src/Explorer/DataSamples/DataSamplesUtil.ts
|
||||
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
|
||||
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
|
||||
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
|
||||
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts
|
||||
src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts
|
||||
@@ -191,4 +162,4 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
|
||||
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||
__mocks__/monaco-editor.ts
|
||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||
src/Explorer/Tree/ResourceTree.tsx
|
||||
2
.github/workflows/ci.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
||||
- ./test/mongo/container.spec.ts
|
||||
- ./test/mongo/container32.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/tables/container.spec.ts
|
||||
steps:
|
||||
|
||||
|
Before Width: | Height: | Size: 842 B After Width: | Height: | Size: 842 B |
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 371 B |
@@ -1,16 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path
|
||||
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
|
||||
transform="scale(0.5)"
|
||||
fill="#000"
|
||||
stroke="#CCC"
|
||||
>
|
||||
transform="scale(0.5)" fill="#000" stroke="#000">
|
||||
</path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 449 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -37,8 +37,8 @@ module.exports = {
|
||||
global: {
|
||||
branches: 25,
|
||||
functions: 25,
|
||||
lines: 30,
|
||||
statements: 30,
|
||||
lines: 29.5,
|
||||
statements: 29.5,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
@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;
|
||||
@GrayScale: "grayscale()";
|
||||
|
||||
@xSmallFontSize: 4px;
|
||||
@smallFontSize: 8px;
|
||||
|
||||
@@ -2357,6 +2357,8 @@ a:link {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -2832,6 +2834,8 @@ a:link {
|
||||
|
||||
#explorerNotificationConsole {
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.uniqueIndexesContainer {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.dataResourceTree {
|
||||
margin-left: @MediumSpace;
|
||||
overflow: auto;
|
||||
|
||||
.databaseHeader {
|
||||
font-size: 14px;
|
||||
@@ -18,6 +19,10 @@
|
||||
.notebookHeader {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clickDisabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
@import "./Common/Constants";
|
||||
|
||||
|
||||
.resourceTree {
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
flex: 0 0 auto;
|
||||
.main {
|
||||
height: 100%;
|
||||
@@ -46,20 +44,19 @@
|
||||
}
|
||||
|
||||
.contextmenushowing {
|
||||
background-color: #EEE;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.collectionstree {
|
||||
width: 100%;
|
||||
margin-top: @DefaultSpace;
|
||||
|
||||
|
||||
.databaseList {
|
||||
list-style-type: none;
|
||||
padding-left: 0px;
|
||||
|
||||
.collectionList {
|
||||
padding-left:(2 * @MediumSpace);
|
||||
padding-left: (2 * @MediumSpace);
|
||||
}
|
||||
|
||||
.collectionChildList {
|
||||
@@ -85,7 +82,7 @@
|
||||
left: 0px;
|
||||
float: right;
|
||||
display: none;
|
||||
padding-left: 6px!important;
|
||||
padding-left: 6px !important;
|
||||
line-height: @TreeLineHeight;
|
||||
}
|
||||
|
||||
@@ -192,7 +189,7 @@ img.collectionsTreeCollapseExpand {
|
||||
}
|
||||
|
||||
.expanded::before {
|
||||
content: '\23F7';
|
||||
content: "\23F7";
|
||||
margin-left: 0px;
|
||||
font-size: 15px;
|
||||
}
|
||||
@@ -211,7 +208,7 @@ img.collectionsTreeCollapseExpand {
|
||||
transform: rotate(-90deg) translateX(-100%);
|
||||
-webkit-transform: rotate(-90deg) translateX(-100%);
|
||||
-ms-transform: rotate(-90deg) translateX(-100%);
|
||||
border-bottom: 1px solid #CCC;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.main-nav-img {
|
||||
@@ -257,11 +254,11 @@ ul.nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.highlight:hover>.contextmenubutton {
|
||||
.highlight:hover > .contextmenubutton {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
.highlight:hover>.contextmenubutton::after {
|
||||
.highlight:hover > .contextmenubutton::after {
|
||||
content: "\2026";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
4642
package-lock.json
generated
41
package.json
@@ -42,14 +42,15 @@
|
||||
"@octokit/rest": "17.9.2",
|
||||
"@phosphor/widgets": "1.9.3",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@types/lodash": "4.14.171",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"applicationinsights": "1.8.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "file:./canvas",
|
||||
"clean-webpack-plugin": "0.1.19",
|
||||
"clean-webpack-plugin": "3.0.0",
|
||||
"clipboard-copy": "4.0.1",
|
||||
"copy-webpack-plugin": "6.0.2",
|
||||
"copy-webpack-plugin": "9.0.1",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"d3": "6.1.1",
|
||||
@@ -80,10 +81,10 @@
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
"react": "16.13.1",
|
||||
"react": "16.14.0",
|
||||
"react-animate-height": "2.0.8",
|
||||
"react-dnd": "9.4.0",
|
||||
"react-dnd-html5-backend": "9.4.0",
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-hotkeys": "2.0.0",
|
||||
"react-i18next": "11.8.5",
|
||||
@@ -97,7 +98,7 @@
|
||||
"sanitize-html": "2.3.3",
|
||||
"styled-components": "4.3.2",
|
||||
"swr": "0.4.0",
|
||||
"terser-webpack-plugin": "3.1.0",
|
||||
"terser-webpack-plugin": "5.1.4",
|
||||
"underscore": "1.9.1",
|
||||
"utility-types": "3.10.0",
|
||||
"zustand": "3.5.0"
|
||||
@@ -131,6 +132,7 @@
|
||||
"@types/underscore": "1.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "4.22.0",
|
||||
"@typescript-eslint/parser": "4.22.0",
|
||||
"@webpack-cli/serve": "1.5.2",
|
||||
"babel-jest": "24.9.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"buffer": "5.1.0",
|
||||
@@ -152,44 +154,45 @@
|
||||
"html-inline-css-webpack-plugin": "1.11.0",
|
||||
"html-loader": "0.5.5",
|
||||
"html-loader-jest": "0.2.1",
|
||||
"html-webpack-plugin": "4.5.2",
|
||||
"jest": "25.5.4",
|
||||
"jest-canvas-mock": "2.1.0",
|
||||
"html-webpack-plugin": "5.3.2",
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"jest-playwright-preset": "1.5.1",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.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",
|
||||
"node-fetch": "2.6.1",
|
||||
"playwright": "1.10.0",
|
||||
"playwright": "1.13.0",
|
||||
"prettier": "2.2.1",
|
||||
"process": "0.11.10",
|
||||
"raw-loader": "0.5.1",
|
||||
"react-dev-utils": "11.0.4",
|
||||
"rimraf": "3.0.0",
|
||||
"sinon": "3.2.1",
|
||||
"style-loader": "0.23.0",
|
||||
"ts-loader": "6.2.2",
|
||||
"ts-loader": "9.2.4",
|
||||
"tslint": "5.11.0",
|
||||
"tslint-microsoft-contrib": "6.0.0",
|
||||
"typedoc": "0.20.36",
|
||||
"typescript": "4.3.4",
|
||||
"url-loader": "1.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "4.46.0",
|
||||
"webpack-bundle-analyzer": "3.6.1",
|
||||
"webpack-cli": "3.3.10",
|
||||
"webpack-dev-server": "3.11.0"
|
||||
"webpack": "5.47.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "4.7.2",
|
||||
"webpack-dev-server": "3.11.2"
|
||||
},
|
||||
"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",
|
||||
"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: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:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
|
||||
"pack:prod": "webpack --mode production",
|
||||
"pack:fast": "webpack --mode development --progress",
|
||||
"copyToConsumers": "node copyToConsumers",
|
||||
"test": "rimraf coverage && jest",
|
||||
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import { userContext } from "../UserContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface CollapsedResourceTreeProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
@@ -11,6 +12,21 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
toggleLeftPaneExpanded,
|
||||
isLeftPaneExpanded,
|
||||
}: 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 (
|
||||
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
||||
<div className="main-nav nav">
|
||||
@@ -21,11 +37,14 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
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" />
|
||||
</span>
|
||||
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
|
||||
<span className="collectionCollapsed">
|
||||
<span>{userContext.apiType} API</span>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -95,6 +95,8 @@ export class Flights {
|
||||
public static readonly MongoIndexing = "mongoindexing";
|
||||
public static readonly AutoscaleTest = "autoscaletest";
|
||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||
public static readonly Phoenix = "phoenix";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
@@ -336,6 +338,14 @@ export enum ConflictOperationType {
|
||||
Delete = "delete",
|
||||
}
|
||||
|
||||
export enum ConnectionStatusType {
|
||||
Connect = "Connect",
|
||||
Connecting = "Connecting",
|
||||
Connected = "Connected",
|
||||
Failed = "Connection Failed",
|
||||
ReConnect = "Reconnect",
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
@@ -345,10 +355,32 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 5000;
|
||||
public static readonly heartbeatDelayMs = 60000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
public static readonly memoryGuageToGB = 1048576;
|
||||
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.";
|
||||
public static saveNotebookModalTitle = "Save Notebook in temporary workspace";
|
||||
public static saveNotebookModalContent =
|
||||
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
|
||||
public static newNotebookModalTitle = "Create Notebook in temporary workspace";
|
||||
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
|
||||
public static newNotebookModalContent1 =
|
||||
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
|
||||
public static newNotebookModalContent2 =
|
||||
"To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends. ";
|
||||
public static galleryNotebookDownloadContent1 =
|
||||
"To download, run, and make changes to this sample notebook, a temporary workspace will be created. When the session expires, any notebooks in the workspace will be removed.";
|
||||
public static galleryNotebookDownloadContent2 =
|
||||
"To save your work permanently, save your notebooks to a GitHub repository or download the Notebooks to your local machine before the session ends. ";
|
||||
public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
|
||||
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
|
||||
public static learnMore = "Learn more.";
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
|
||||
@@ -3,8 +3,16 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@@ -246,4 +254,31 @@ describe("MongoProxyClient", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
@@ -78,7 +79,7 @@ export function queryDocuments(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint() || "";
|
||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -141,7 +142,8 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "GET",
|
||||
@@ -181,7 +183,7 @@ export function createDocument(
|
||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||
@@ -225,7 +227,7 @@ export function updateDocument(
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy(
|
||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
|
||||
return window
|
||||
.fetch(
|
||||
@@ -333,8 +335,15 @@ export function createMongoCollectionWithProxy(
|
||||
});
|
||||
}
|
||||
|
||||
export function getEndpoint(): string {
|
||||
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
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) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||
import { AuthType } from "../AuthType";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||
import { userContext } from "../UserContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface ResourceTreeProps {
|
||||
export interface ResourceTreeContainerProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
isLeftPaneExpanded: boolean;
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
|
||||
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
|
||||
toggleLeftPaneExpanded,
|
||||
isLeftPaneExpanded,
|
||||
}: ResourceTreeProps): JSX.Element => {
|
||||
container,
|
||||
}: 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 (
|
||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||
{/* Collections Window - - Start */}
|
||||
@@ -38,9 +60,11 @@ export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
|
||||
id="expandToggleLeftPaneButton"
|
||||
role="button"
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
tabIndex={0}
|
||||
aria-label="Collapse Tree"
|
||||
title="Collapse Tree"
|
||||
ref={focusButton}
|
||||
>
|
||||
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
||||
</span>
|
||||
@@ -48,9 +72,11 @@ export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
{userContext.authType === AuthType.ResourceToken ? (
|
||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
|
||||
) : (
|
||||
<ResourceTokenTree />
|
||||
) : userContext.features.enableKoResourceTree ? (
|
||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||
) : (
|
||||
<ResourceTree container={container} />
|
||||
)}
|
||||
</div>
|
||||
{/* Collections Window - End */}
|
||||
@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
||||
return (
|
||||
<span>
|
||||
<TooltipHost content={children}>
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
import ko from "knockout";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { Database } from "../../Contracts/ViewModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { client } from "../CosmosClient";
|
||||
@@ -23,6 +26,15 @@ describe("createCollection", () => {
|
||||
} as DatabaseAccount,
|
||||
apiType: "SQL",
|
||||
});
|
||||
useDatabases.setState({
|
||||
databases: [
|
||||
{
|
||||
id: ko.observable("testDatabase"),
|
||||
loadCollections: () => undefined,
|
||||
collections: ko.observableArray([]),
|
||||
} as Database,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should call ARM if logged in with AAD", async () => {
|
||||
|
||||
@@ -4,20 +4,16 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import {
|
||||
createUpdateCassandraTable,
|
||||
getCassandraTable,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import {
|
||||
createUpdateMongoDBCollection,
|
||||
getMongoDBCollection,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
@@ -59,6 +55,16 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
};
|
||||
|
||||
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
if (!params.createNewDatabase) {
|
||||
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
|
||||
if (!isValid) {
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
throw new Error(
|
||||
`Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { apiType } = userContext;
|
||||
switch (apiType) {
|
||||
case "SQL":
|
||||
@@ -77,23 +83,6 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams
|
||||
};
|
||||
|
||||
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getSqlContainer(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.SqlContainerResource = {
|
||||
id: params.collectionId,
|
||||
@@ -131,23 +120,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||
|
||||
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }];
|
||||
try {
|
||||
const getResponse = await getMongoDBCollection(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.MongoDBCollectionResource = {
|
||||
id: params.collectionId,
|
||||
@@ -189,23 +161,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
|
||||
};
|
||||
|
||||
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getCassandraTable(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.CassandraTableResource = {
|
||||
id: params.collectionId,
|
||||
@@ -233,23 +188,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams):
|
||||
};
|
||||
|
||||
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getGremlinGraph(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.GremlinGraphResource = {
|
||||
id: params.collectionId,
|
||||
@@ -284,22 +222,6 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise<D
|
||||
};
|
||||
|
||||
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getTable(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.TableResource = {
|
||||
id: params.collectionId,
|
||||
|
||||
@@ -2,20 +2,13 @@ import { DatabaseResponse } from "@azure/cosmos";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
import { userContext } from "../../UserContext";
|
||||
import {
|
||||
createUpdateCassandraKeyspace,
|
||||
getCassandraKeyspace,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import {
|
||||
createUpdateGremlinDatabase,
|
||||
getGremlinDatabase,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import {
|
||||
createUpdateMongoDBDatabase,
|
||||
getMongoDBDatabase,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { getDatabaseName } from "../../Utils/APITypeUtils";
|
||||
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import {
|
||||
CassandraKeyspaceCreateUpdateParameters,
|
||||
CreateUpdateOptions,
|
||||
@@ -48,6 +41,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
|
||||
}
|
||||
|
||||
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
|
||||
const databaseName = getDatabaseName().toLocaleLowerCase();
|
||||
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
|
||||
}
|
||||
|
||||
const { apiType } = userContext;
|
||||
|
||||
switch (apiType) {
|
||||
@@ -65,22 +63,6 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P
|
||||
}
|
||||
|
||||
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
try {
|
||||
const getResponse = await getSqlDatabase(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
@@ -101,22 +83,6 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
|
||||
}
|
||||
|
||||
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
try {
|
||||
const getResponse = await getMongoDBDatabase(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||
const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
@@ -137,22 +103,6 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
|
||||
}
|
||||
|
||||
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
try {
|
||||
const getResponse = await getCassandraKeyspace(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||
const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
|
||||
properties: {
|
||||
@@ -173,22 +123,6 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
|
||||
}
|
||||
|
||||
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
try {
|
||||
const getResponse = await getGremlinDatabase(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||
const rpPayload: GremlinDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ConnectionStatusType } from "../Common/Constants";
|
||||
|
||||
export interface DatabaseAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -496,3 +498,8 @@ export interface MemoryUsageInfo {
|
||||
freeKB: number;
|
||||
totalKB: number;
|
||||
}
|
||||
|
||||
export interface ContainerConnectionInfo {
|
||||
status: ConnectionStatusType;
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("The Heatmap Control", () => {
|
||||
};
|
||||
|
||||
let heatmap: Heatmap;
|
||||
let theme: PortalTheme = 1;
|
||||
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
|
||||
const theme: PortalTheme = 1;
|
||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||
|
||||
describe("drawHeatmap rendering", () => {
|
||||
beforeEach(() => {
|
||||
@@ -100,7 +100,7 @@ describe("iframe rendering when there is no data", () => {
|
||||
});
|
||||
|
||||
it("should show a no data message with a dark theme", () => {
|
||||
let data = {
|
||||
const data = {
|
||||
data: {
|
||||
signature: "pcIframe",
|
||||
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;
|
||||
|
||||
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", () => {
|
||||
let data = {
|
||||
const data = {
|
||||
data: {
|
||||
signature: "pcIframe",
|
||||
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;
|
||||
|
||||
handleMessage(data as MessageEvent);
|
||||
|
||||
@@ -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 {
|
||||
family: StyleConstants.DataExplorerFont,
|
||||
size,
|
||||
@@ -78,9 +78,9 @@ export class Heatmap {
|
||||
// go thru all rows and create 2d matrix for heatmap...
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
output.yAxisPoints.push(rows[i]);
|
||||
let dataPoints: number[] = [];
|
||||
const dataPoints: number[] = [];
|
||||
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"]);
|
||||
}
|
||||
output.dataPoints.push(dataPoints);
|
||||
@@ -193,7 +193,7 @@ export class Heatmap {
|
||||
this._getLayoutSettings(),
|
||||
this._getChartDisplaySettings()
|
||||
);
|
||||
let plotDiv: any = document.getElementById(Heatmap.elementId);
|
||||
const plotDiv: any = document.getElementById(Heatmap.elementId);
|
||||
plotDiv.on("plotly_click", (data: any) => {
|
||||
let timeSelected: string = data.points[0].x;
|
||||
timeSelected = timeSelected.replace(" ", "T");
|
||||
@@ -205,7 +205,7 @@ export class Heatmap {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let output = [];
|
||||
const output = [];
|
||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
||||
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ export const createCollectionContextMenuButton = (
|
||||
|
||||
items.push({
|
||||
iconSrc: HostedTerminalIcon,
|
||||
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Icon, Label, Stack } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
|
||||
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 {
|
||||
return (
|
||||
<>
|
||||
@@ -39,6 +47,11 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
verticalAlign="center"
|
||||
tokens={accordionStackTokens}
|
||||
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"} />
|
||||
<Label>{this.props.title}</Label>
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
exports[`CollapsibleSectionComponent renders 1`] = `
|
||||
<Fragment>
|
||||
<Stack
|
||||
aria-expanded={true}
|
||||
aria-name="Advanced"
|
||||
className="collapsibleSection"
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Link,
|
||||
PrimaryButton,
|
||||
ProgressIndicator,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import React, { FC } from "react";
|
||||
@@ -23,13 +24,78 @@ export interface DialogState {
|
||||
dialogProps?: DialogProps;
|
||||
openDialog: (props: DialogProps) => void;
|
||||
closeDialog: () => void;
|
||||
showOkCancelModalDialog: (
|
||||
title: string,
|
||||
subText: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
contentHtml?: JSX.Element,
|
||||
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,
|
||||
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
|
||||
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,
|
||||
contentHtml?: JSX.Element,
|
||||
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();
|
||||
},
|
||||
contentHtml,
|
||||
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 {
|
||||
@@ -62,6 +128,7 @@ export interface DialogProps {
|
||||
type?: DialogType;
|
||||
showCloseButton?: boolean;
|
||||
onDismiss?: () => void;
|
||||
contentHtml?: JSX.Element;
|
||||
}
|
||||
|
||||
const DIALOG_MIN_WIDTH = "400px";
|
||||
@@ -88,6 +155,7 @@ export const Dialog: FC = () => {
|
||||
type,
|
||||
showCloseButton,
|
||||
onDismiss,
|
||||
contentHtml,
|
||||
} = props || {};
|
||||
|
||||
const dialogProps: IDialogProps = {
|
||||
@@ -119,8 +187,7 @@ export const Dialog: FC = () => {
|
||||
text: secondaryButtonText,
|
||||
onClick: onSecondaryButtonClick,
|
||||
}
|
||||
: {};
|
||||
|
||||
: undefined;
|
||||
return visible ? (
|
||||
<FluentDialog {...dialogProps}>
|
||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||
@@ -130,6 +197,7 @@ export const Dialog: FC = () => {
|
||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
)}
|
||||
{contentHtml && <Text>{contentHtml}</Text>}
|
||||
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||
<DialogFooter>
|
||||
<PrimaryButton {...primaryButtonProps} />
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.input-type-head-text-field {
|
||||
width: 100%;
|
||||
}
|
||||
.input-query-form {
|
||||
width: 100%;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
@@ -21,4 +27,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-typeahead-chocies-container {
|
||||
border: 1px solid lightgrey;
|
||||
padding: 5px 10px 5px 10px;
|
||||
cursor: pointer;
|
||||
.choice-caption{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,13 @@
|
||||
* typeaheadOverrideOptions: { dynamic:false }
|
||||
*
|
||||
*/
|
||||
import "jquery-typeahead";
|
||||
import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import "./InputTypeahead.less";
|
||||
|
||||
export interface Item {
|
||||
caption: string;
|
||||
value: any;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,170 +74,128 @@ export interface InputTypeaheadComponentProps {
|
||||
useTextarea?: boolean;
|
||||
}
|
||||
|
||||
interface OnClickItem {
|
||||
matchedKey: string;
|
||||
value: any;
|
||||
caption: string;
|
||||
group: string;
|
||||
interface InputTypeaheadComponentState {
|
||||
isSuggestionVisible: boolean;
|
||||
selectedChoice: Item;
|
||||
filteredChoices: Item[];
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
inputValue: string;
|
||||
selection: Item;
|
||||
}
|
||||
|
||||
interface InputTypeaheadComponentState {}
|
||||
|
||||
export class InputTypeaheadComponent extends React.Component<
|
||||
InputTypeaheadComponentProps,
|
||||
InputTypeaheadComponentState
|
||||
> {
|
||||
private inputElt: HTMLElement;
|
||||
private containerElt: HTMLElement;
|
||||
|
||||
private cache: Cache;
|
||||
private inputValue: string;
|
||||
private selection: Item;
|
||||
|
||||
public constructor(props: InputTypeaheadComponentProps) {
|
||||
constructor(props: InputTypeaheadComponentProps) {
|
||||
super(props);
|
||||
this.cache = {
|
||||
inputValue: null,
|
||||
selection: null,
|
||||
this.state = {
|
||||
isSuggestionVisible: false,
|
||||
filteredChoices: [],
|
||||
selectedChoice: {
|
||||
caption: "",
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private onRenderCell = (item: Item): JSX.Element => {
|
||||
return (
|
||||
<span className="input-typeahead-container">
|
||||
<div
|
||||
className="input-typehead"
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(event)}
|
||||
>
|
||||
<div className="typeahead__container" ref={(input) => (this.containerElt = input)}>
|
||||
<div className="typeahead__field">
|
||||
<span className="typeahead__query">
|
||||
{this.props.useTextarea ? (
|
||||
<textarea
|
||||
rows={1}
|
||||
name="q"
|
||||
autoComplete="off"
|
||||
aria-label="Input query"
|
||||
ref={(input) => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
name="q"
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
aria-label="Input query"
|
||||
ref={(input) => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{this.props.showSearchButton && (
|
||||
<span className="typeahead__button">
|
||||
<button type="submit">
|
||||
<span className="typeahead__search-icon" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
<div className="input-typeahead-chocies-container" onClick={() => this.onChoiceClick(item)}>
|
||||
<p className="choice-caption">{item.caption}</p>
|
||||
<span>{item.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
private onChoiceClick = (item: Item): void => {
|
||||
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) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.submitFct(this.cache.inputValue, this.cache.selection);
|
||||
$(this.containerElt).children(".typeahead__result").hide();
|
||||
this.props.submitFct(this.props.defaultValue, this.state.selectedChoice);
|
||||
this.setState({ isSuggestionVisible: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must execute once ko is rendered, so that it can find the input element by id
|
||||
*/
|
||||
private initializeTypeahead(): void {
|
||||
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) {
|
||||
props.onSelected(item);
|
||||
}
|
||||
},
|
||||
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
|
||||
cache.inputValue = query;
|
||||
if (props.onNewValue) {
|
||||
props.onNewValue(query);
|
||||
}
|
||||
},
|
||||
},
|
||||
template: (query: string, item: any) => {
|
||||
// Don't display id if caption *IS* the id
|
||||
return item.caption === item.value
|
||||
? "<span>{{caption}}</span>"
|
||||
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
|
||||
},
|
||||
dynamic: true,
|
||||
};
|
||||
|
||||
// Override options
|
||||
if (props.typeaheadOverrideOptions) {
|
||||
for (const p in props.typeaheadOverrideOptions) {
|
||||
options[p] = props.typeaheadOverrideOptions[p];
|
||||
}
|
||||
}
|
||||
private filterChoiceByValue = (choices: Item[], searchKeyword: string): Item[] => {
|
||||
return choices.filter((choice) =>
|
||||
// @ts-ignore
|
||||
Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase()))
|
||||
);
|
||||
};
|
||||
|
||||
if (props.hasOwnProperty("showCancelButton")) {
|
||||
options.cancelButton = props.showCancelButton;
|
||||
}
|
||||
public render(): JSX.Element {
|
||||
const { defaultValue, useTextarea, placeholder, onNewValue } = this.props;
|
||||
const { isSuggestionVisible, selectedChoice, filteredChoices } = this.state;
|
||||
const theme = getTheme();
|
||||
|
||||
$(this.inputElt).typeahead(options);
|
||||
const iconButtonStyles = {
|
||||
root: {
|
||||
color: theme.palette.neutralPrimary,
|
||||
marginLeft: "10px !important",
|
||||
marginTop: "0px",
|
||||
marginRight: "2px",
|
||||
width: "42px",
|
||||
},
|
||||
rootHovered: {
|
||||
color: theme.palette.neutralDark,
|
||||
},
|
||||
};
|
||||
const cancelIcon: IIconProps = { iconName: "cancel" };
|
||||
const searchIcon: IIconProps = { iconName: "Search" };
|
||||
|
||||
return (
|
||||
<div className="input-typeahead-container">
|
||||
<Stack horizontal>
|
||||
<form aria-labelledby="input" className="input-query-form">
|
||||
<TextField
|
||||
multiline={useTextarea}
|
||||
rows={1}
|
||||
id="input"
|
||||
defaultValue={defaultValue}
|
||||
ariaLabel="Input query"
|
||||
placeholder={placeholder}
|
||||
className="input-type-head-text-field"
|
||||
value={defaultValue}
|
||||
onKeyDown={this.onSubmit}
|
||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||
/>
|
||||
</form>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,55 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`inputTypeahead renders <input /> 1`] = `
|
||||
<span
|
||||
<div
|
||||
className="input-typeahead-container"
|
||||
>
|
||||
<div
|
||||
className="input-typehead"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<form
|
||||
aria-labelledby="input"
|
||||
className="input-query-form"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
className="typeahead__container"
|
||||
>
|
||||
<div
|
||||
className="typeahead__field"
|
||||
>
|
||||
<span
|
||||
className="typeahead__query"
|
||||
>
|
||||
<input
|
||||
aria-label="Input query"
|
||||
autoComplete="off"
|
||||
name="q"
|
||||
type="search"
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</form>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`inputTypeahead renders <textarea /> 1`] = `
|
||||
<span
|
||||
<div
|
||||
className="input-typeahead-container"
|
||||
>
|
||||
<div
|
||||
className="input-typehead"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<form
|
||||
aria-labelledby="input"
|
||||
className="input-query-form"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
className="typeahead__container"
|
||||
>
|
||||
<div
|
||||
className="typeahead__field"
|
||||
>
|
||||
<span
|
||||
className="typeahead__query"
|
||||
>
|
||||
<textarea
|
||||
aria-label="Input query"
|
||||
autoComplete="off"
|
||||
name="q"
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</form>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -17,6 +17,8 @@ import Explorer from "../../Explorer";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
@@ -146,7 +148,9 @@ export class NotebookViewerComponent
|
||||
<NotebookMetadataComponent
|
||||
data={this.state.galleryItem}
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={this.props.container && "Download to my notebooks"}
|
||||
downloadButtonText={
|
||||
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
|
||||
}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { useDialog } from "../Dialog";
|
||||
|
||||
const title = "Open Saved Queries";
|
||||
|
||||
@@ -222,7 +223,11 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
|
||||
key: "Delete",
|
||||
text: "Delete query",
|
||||
onClick: async () => {
|
||||
if (window.confirm("Are you sure you want to delete this query?")) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
"Are you sure you want to delete this query?",
|
||||
"Delete",
|
||||
async () => {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
@@ -250,7 +255,10 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
|
||||
);
|
||||
}
|
||||
await this.fetchSavedQueries(); // get latest state
|
||||
}
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
getPriceCurrency,
|
||||
getCurrencySign,
|
||||
getAutoscalePricePerRu,
|
||||
getMultimasterMultiplier,
|
||||
computeRUUsagePriceHourly,
|
||||
getPricePerRu,
|
||||
estimatedCostDisclaimer,
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
ITextFieldStyles,
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
DetailsRow,
|
||||
ICheckboxStyles,
|
||||
IStackProps,
|
||||
IStackTokens,
|
||||
IChoiceGroupStyles,
|
||||
Link,
|
||||
Text,
|
||||
IMessageBarStyles,
|
||||
ITextStyles,
|
||||
IDetailsRowStyles,
|
||||
IStackStyles,
|
||||
IColumn,
|
||||
IDetailsColumnStyles,
|
||||
IDetailsListStyles,
|
||||
IDetailsRowProps,
|
||||
IDetailsRowStyles,
|
||||
IDropdownStyles,
|
||||
IMessageBarStyles,
|
||||
ISeparatorStyles,
|
||||
IStackProps,
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
ITextFieldStyles,
|
||||
ITextStyles,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Stack,
|
||||
SelectionMode,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles,
|
||||
Stack,
|
||||
Text,
|
||||
} 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 {
|
||||
costType: JSX.Element;
|
||||
@@ -65,7 +65,7 @@ export interface PriceBreakdown {
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
@@ -223,14 +223,15 @@ export const getRuPriceBreakdown = (
|
||||
multimasterEnabled: isMultimaster,
|
||||
isAutoscale: isAutoscale,
|
||||
});
|
||||
const basePricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
||||
: getPricePerRu(serverId);
|
||||
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
|
||||
const pricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
|
||||
: getPricePerRu(serverId, multimasterMultiplier);
|
||||
return {
|
||||
hourlyPrice: hourlyPrice,
|
||||
hourlyPrice,
|
||||
dailyPrice: hourlyPrice * 24,
|
||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
||||
pricePerRu,
|
||||
currency: getPriceCurrency(serverId),
|
||||
currencySign: getCurrencySign(serverId),
|
||||
};
|
||||
@@ -271,7 +272,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
||||
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
|
||||
<a href={Urls.autoscaleMigration}>Learn more</a>
|
||||
<Link href={Urls.autoscaleMigration}>Learn more</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -39,6 +40,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -73,6 +75,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -80,11 +83,11 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
>
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
|
||||
|
||||
<a
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</StyledMessageBar>
|
||||
<StyledChoiceGroup
|
||||
@@ -186,6 +189,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -460,6 +464,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -412,6 +413,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -952,6 +954,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -1228,6 +1231,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -42,19 +43,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"gitHubOAuthService": GitHubOAuthService {
|
||||
"junoClient": JunoClient {
|
||||
"cachedPinnedRepos": [Function],
|
||||
},
|
||||
"token": [Function],
|
||||
},
|
||||
"junoClient": JunoClient {
|
||||
"cachedPinnedRepos": [Function],
|
||||
},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
@@ -114,6 +102,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -122,19 +111,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"gitHubOAuthService": GitHubOAuthService {
|
||||
"junoClient": JunoClient {
|
||||
"cachedPinnedRepos": [Function],
|
||||
},
|
||||
"token": [Function],
|
||||
},
|
||||
"junoClient": JunoClient {
|
||||
"cachedPinnedRepos": [Function],
|
||||
},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -159,6 +159,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -166,16 +167,17 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
>
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
|
||||
|
||||
<a
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -195,6 +197,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -207,6 +210,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -219,6 +223,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -230,6 +235,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -249,6 +255,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -264,6 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -278,6 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -291,6 +300,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -302,6 +312,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -321,6 +332,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -363,6 +375,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -378,6 +391,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -394,6 +408,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.tabComponentContainer {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext";
|
||||
import {
|
||||
calculateEstimateNumber,
|
||||
computeRUUsagePriceHourly,
|
||||
estimatedCostDisclaimer,
|
||||
getAutoscalePricePerRu,
|
||||
getCurrencySign,
|
||||
getMultimasterMultiplier,
|
||||
@@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
|
||||
: getPricePerRu(serverId) * multiplier;
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||
|
||||
if (isAutoscale) {
|
||||
return (
|
||||
|
||||
@@ -118,6 +118,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
<input
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={isAutoscaleSelected}
|
||||
type="radio"
|
||||
role="radio"
|
||||
@@ -131,6 +132,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
aria-label="Manual mode"
|
||||
checked={!isAutoscaleSelected}
|
||||
type="radio"
|
||||
aria-required={true}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||
|
||||
@@ -345,12 +345,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -630,6 +632,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
@@ -651,6 +654,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.0"
|
||||
@@ -667,6 +671,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</span>
|
||||
<input
|
||||
aria-label="Manual mode"
|
||||
aria-required={true}
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.2"
|
||||
@@ -1327,12 +1332,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -1612,6 +1619,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
|
||||
@@ -243,6 +243,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||
<IconButton
|
||||
name="More"
|
||||
title="More"
|
||||
className="treeMenuEllipsis"
|
||||
ariaLabel={menuItemLabel}
|
||||
menuIconProps={{
|
||||
|
||||
@@ -211,6 +211,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,6 +424,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ describe("DataSampleUtils", () => {
|
||||
collections: ko.observableArray<Collection>([collection]),
|
||||
} as Database;
|
||||
const explorer = {} as Explorer;
|
||||
explorer.showOkModalDialog = () => {};
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
const dataSamplesUtil = new DataSamplesUtil(explorer);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
@@ -20,7 +21,7 @@ export class DataSamplesUtil {
|
||||
const containerName = generator.getCollectionId();
|
||||
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
|
||||
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
|
||||
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
logConsoleError(msg);
|
||||
return;
|
||||
}
|
||||
@@ -29,7 +30,7 @@ export class DataSamplesUtil {
|
||||
.createSampleContainerAsync()
|
||||
.catch((error) => logConsoleError(`Error creating sample container: ${error}`));
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IChoiceGroupProps } from "@fluentui/react";
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
|
||||
@@ -12,11 +13,13 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -35,7 +38,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
|
||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
@@ -59,7 +62,6 @@ import TerminalTab from "./Tabs/TerminalTab";
|
||||
import Database from "./Tree/Database";
|
||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
||||
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import { useDatabases } from "./useDatabases";
|
||||
import { useSelectedNode } from "./useSelectedNode";
|
||||
@@ -74,9 +76,6 @@ export default class Explorer {
|
||||
// Resource Tree
|
||||
private resourceTree: ResourceTreeAdapter;
|
||||
|
||||
// Resource Token
|
||||
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
|
||||
|
||||
// Tabs
|
||||
public isTabsContentExpanded: ko.Observable<boolean>;
|
||||
|
||||
@@ -92,12 +91,13 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||
|
||||
private phoenixClient: PhoenixClient;
|
||||
constructor() {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
this._isInitializingNotebooks = false;
|
||||
this.phoenixClient = new PhoenixClient();
|
||||
useNotebook.subscribe(
|
||||
() => this.refreshCommandBarButtons(),
|
||||
(state) => state.isNotebooksEnabledForAccount
|
||||
@@ -166,27 +166,13 @@ export default class Explorer {
|
||||
|
||||
useNotebook.subscribe(
|
||||
async () => {
|
||||
if (!this.notebookManager) {
|
||||
const NotebookManager = await (
|
||||
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
|
||||
).default;
|
||||
this.notebookManager = new NotebookManager();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList(),
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
this.initiateAndRefreshNotebookList();
|
||||
useNotebook.getState().setIsRefreshed(false);
|
||||
},
|
||||
(state) => state.isNotebookEnabled
|
||||
(state) => state.isNotebookEnabled || state.isRefreshed
|
||||
);
|
||||
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
|
||||
|
||||
// Override notebook server parameters from URL parameters
|
||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||
@@ -216,6 +202,23 @@ export default class Explorer {
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
public async initiateAndRefreshNotebookList(): Promise<void> {
|
||||
if (!this.notebookManager) {
|
||||
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
|
||||
.default;
|
||||
this.notebookManager = new NotebookManager();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList(),
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
}
|
||||
|
||||
public openEnableSynapseLinkDialog(): void {
|
||||
const addSynapseLinkDialogProps: DialogProps = {
|
||||
linkProps: {
|
||||
@@ -349,7 +352,7 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
this._isInitializingNotebooks = true;
|
||||
|
||||
if (userContext.features.phoenix === false) {
|
||||
await this.ensureNotebookWorkspaceRunning();
|
||||
const connectionInfo = await listConnectionInfo(
|
||||
userContext.subscriptionId,
|
||||
@@ -362,12 +365,62 @@ export default class Explorer {
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshNotebookList();
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace() {
|
||||
public async allocateContainer(): Promise<void> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
const isAllocating = useNotebook.getState().isAllocating;
|
||||
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
|
||||
const provisionData = {
|
||||
aadToken: userContext.authorizationToken,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
try {
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
||||
if (
|
||||
connectionInfo.status === HttpStatusCodes.OK &&
|
||||
connectionInfo.data &&
|
||||
connectionInfo.data.notebookServerUrl
|
||||
) {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
});
|
||||
this.notebookManager?.notebookClient
|
||||
.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
} else {
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
throw error;
|
||||
}
|
||||
this.refreshNotebookList();
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace(): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||
handleError(
|
||||
"Attempt to reset notebook workspace, but notebook is not enabled",
|
||||
@@ -392,7 +445,6 @@ export default class Explorer {
|
||||
if (!databaseAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { value: workspaces } = await listByDatabaseAccount(
|
||||
userContext.subscriptionId,
|
||||
@@ -540,17 +592,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) {
|
||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||
handleError(error, "Explorer/uploadFile");
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree);
|
||||
promise
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -616,51 +673,6 @@ export default class Explorer {
|
||||
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.
|
||||
* Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder.
|
||||
@@ -679,6 +691,9 @@ export default class Explorer {
|
||||
if (!notebookContentItem || !notebookContentItem.path) {
|
||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||
}
|
||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
|
||||
this.allocateContainer();
|
||||
}
|
||||
|
||||
const notebookTabs = useTabs
|
||||
.getState()
|
||||
@@ -720,7 +735,7 @@ export default class Explorer {
|
||||
return true;
|
||||
}
|
||||
|
||||
public renameNotebook(notebookFile: NotebookContentItem): void {
|
||||
public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to rename notebook, but notebook is not enabled";
|
||||
handleError(error, "Explorer/renameNotebook");
|
||||
@@ -734,7 +749,9 @@ export default class Explorer {
|
||||
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
|
||||
});
|
||||
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 {
|
||||
useSidePanel.getState().openSidePanel(
|
||||
"Rename Notebook",
|
||||
@@ -751,7 +768,7 @@ export default class Explorer {
|
||||
paneTitle="Rename Notebook"
|
||||
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
|
||||
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
|
||||
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
|
||||
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree)
|
||||
}
|
||||
notebookFile={notebookFile}
|
||||
/>
|
||||
@@ -759,7 +776,7 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public onCreateDirectory(parent: NotebookContentItem): void {
|
||||
public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create notebook directory, but notebook is not enabled";
|
||||
handleError(error, "Explorer/onCreateDirectory");
|
||||
@@ -781,7 +798,7 @@ export default class Explorer {
|
||||
submitButtonLabel="Create"
|
||||
defaultInput=""
|
||||
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
|
||||
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input)
|
||||
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree)
|
||||
}
|
||||
notebookFile={parent}
|
||||
/>
|
||||
@@ -842,13 +859,15 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
await this.resourceTree.initialize();
|
||||
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||
|
||||
this.notebookManager?.refreshPinnedRepos();
|
||||
if (this.notebookToImport) {
|
||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||
}
|
||||
};
|
||||
|
||||
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
|
||||
public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to delete notebook file, but notebook is not enabled";
|
||||
handleError(error, "Explorer/deleteNotebookFile");
|
||||
@@ -862,7 +881,9 @@ export default class Explorer {
|
||||
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -879,7 +900,7 @@ export default class Explorer {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then(
|
||||
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
|
||||
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
|
||||
);
|
||||
@@ -888,22 +909,64 @@ export default class Explorer {
|
||||
/**
|
||||
* 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) {
|
||||
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||
handleError(error, "Explorer/onNewNotebookClicked");
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
|
||||
if (isPhoenixEnabled) {
|
||||
if (isGithubTree) {
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
};
|
||||
} else {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookModalTitle,
|
||||
undefined,
|
||||
"Create",
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
}
|
||||
}
|
||||
|
||||
private getNewNoteWarningText(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.newNotebookModalContent1}</p>
|
||||
<br />
|
||||
<p>
|
||||
{Notebook.newNotebookModalContent2}
|
||||
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
||||
{Notebook.learnMore}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
|
||||
this.notebookManager?.notebookContentClient
|
||||
.createNewNotebookFile(parent)
|
||||
.createNewNotebookFile(parent, isGithubTree)
|
||||
.then((newFile: NotebookContentItem) => {
|
||||
logConsoleInfo(`Successfully created: ${newFile.name}`);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
@@ -932,17 +995,37 @@ export default class Explorer {
|
||||
.finally(clearInProgressMessage);
|
||||
}
|
||||
|
||||
public refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
||||
handleError(error, "Explorer/refreshContentItem");
|
||||
return Promise.reject(new Error(error));
|
||||
}
|
||||
|
||||
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
|
||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||
}
|
||||
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
await this.allocateContainer();
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Failed to Connect",
|
||||
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
}
|
||||
}
|
||||
|
||||
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||
let title: string;
|
||||
|
||||
switch (kind) {
|
||||
@@ -993,7 +1076,7 @@ export default class Explorer {
|
||||
notebookUrl?: string,
|
||||
galleryItem?: IGalleryItem,
|
||||
isFavorite?: boolean
|
||||
) {
|
||||
): Promise<void> {
|
||||
const title = "Gallery";
|
||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||
const galleryTab = useTabs
|
||||
@@ -1062,7 +1145,10 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1094,7 +1180,27 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookUploadModalTitle,
|
||||
undefined,
|
||||
"Upload",
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.uploadFilePanel(parent);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText()
|
||||
);
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.uploadFilePanel(parent);
|
||||
}
|
||||
}
|
||||
|
||||
private uploadFilePanel(parent?: NotebookContentItem): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
@@ -1103,15 +1209,36 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.galleryNotebookDownloadContent1}</p>
|
||||
<br />
|
||||
<p>
|
||||
{Notebook.galleryNotebookDownloadContent2}
|
||||
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
|
||||
{Notebook.learnMore}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
|
||||
}
|
||||
|
||||
public async refreshExplorer(): Promise<void> {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
const isNotebookEnabled: boolean =
|
||||
let isNotebookEnabled = true;
|
||||
if (!userContext.features.phoenix) {
|
||||
isNotebookEnabled =
|
||||
userContext.authType !== AuthType.ResourceToken &&
|
||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
||||
userContext.features.enableNotebooks);
|
||||
}
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
|
||||
|
||||
@@ -1120,6 +1247,7 @@ export default class Explorer {
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
|
||||
if (!userContext.features.notebooksTemporarilyDown) {
|
||||
if (isNotebookEnabled) {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
} else if (this.notebookToImport) {
|
||||
@@ -1127,4 +1255,5 @@ export default class Explorer {
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ describe("Cache arrays by key", () => {
|
||||
const key = "key";
|
||||
cache.insert(key, 1, 0);
|
||||
cache.clear();
|
||||
expect(cache.retrieve(key, 0, 1)).toBe(null);
|
||||
expect(cache.retrieve(key, 0, 1)).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should invalidate oldest key to keep cache size under maximum", () => {
|
||||
@@ -21,7 +21,7 @@ describe("Cache arrays by key", () => {
|
||||
cache.insert(key1, 2, 4);
|
||||
|
||||
expect(cache.retrieve(key1, 0, 3)).toEqual([0, 2, 4]);
|
||||
expect(cache.retrieve(key2, 1, 1)).toEqual(null);
|
||||
expect(cache.retrieve(key2, 1, 1)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should cache and retrieve cached page within boundaries", () => {
|
||||
@@ -39,7 +39,7 @@ describe("Cache arrays by key", () => {
|
||||
const key = "key";
|
||||
cache.insert(key, 0, 0);
|
||||
cache.insert(key, 1, 1);
|
||||
expect(cache.retrieve(key, 2, 1)).toEqual(null);
|
||||
expect(cache.retrieve(key, 2, 1)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should not retrieve cached page overlapping boundaries", () => {
|
||||
@@ -48,7 +48,7 @@ describe("Cache arrays by key", () => {
|
||||
cache.insert(key, 0, 0);
|
||||
cache.insert(key, 1, 1);
|
||||
cache.insert(key, 2, 2);
|
||||
expect(cache.retrieve(key, 2, 4)).toEqual(null);
|
||||
expect(cache.retrieve(key, 2, 4)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should not insert non-contiguous element", () => {
|
||||
@@ -57,7 +57,7 @@ describe("Cache arrays by key", () => {
|
||||
cache.insert(key, 0, 0);
|
||||
cache.insert(key, 1, 1);
|
||||
cache.insert(key, 3, 3);
|
||||
expect(cache.retrieve(key, 3, 1)).toEqual(null);
|
||||
expect(cache.retrieve(key, 3, 1)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should cache multiple keys", () => {
|
||||
|
||||
@@ -61,12 +61,12 @@ export class ArraysByKeyCache<T> {
|
||||
* @param pageSize
|
||||
*/
|
||||
public retrieve(key: string, startIndex: number, pageSize: number): T[] | null {
|
||||
if (!this.cache.hasOwnProperty(key)) {
|
||||
return null;
|
||||
if (!Object.prototype.hasOwnProperty.call(this.cache, key)) {
|
||||
return undefined;
|
||||
}
|
||||
const elements = this.cache[key];
|
||||
if (startIndex + pageSize > elements.length) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return elements.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
|
||||
import ExpandIcon from "../../../../images/Expand_14x14.svg";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||
|
||||
interface MiddlePaneComponentProps {
|
||||
isTabsContentExpanded: boolean;
|
||||
@@ -17,7 +17,14 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
|
||||
<div className="middlePane">
|
||||
<div className="graphTitle">
|
||||
<span className="paneTitle">Graph</span>
|
||||
<span className="graphExpandCollapseBtn pull-right" onClick={this.props.toggleExpandGraph}>
|
||||
<span
|
||||
className="graphExpandCollapseBtn pull-right"
|
||||
onClick={this.props.toggleExpandGraph}
|
||||
role="button"
|
||||
aria-expanded={this.props.isTabsContentExpanded}
|
||||
aria-name="View graph in full screen"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
||||
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
import * as React from "react";
|
||||
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 EditIcon from "../../../../images/edit.svg";
|
||||
import EditIcon from "../../../../images/edit-1.svg";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
|
||||
.graphExplorerContainer {
|
||||
margin-left: @SmallSpace;
|
||||
overflow: hidden;
|
||||
height:100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
.loadGraphHelper {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
height:50%;
|
||||
height: 50%;
|
||||
|
||||
img {
|
||||
width: @LoadGraphHelperWidth;
|
||||
@@ -72,7 +73,7 @@
|
||||
margin: @DefaultSpace;
|
||||
}
|
||||
|
||||
.loadGraphBtn{
|
||||
.loadGraphBtn {
|
||||
margin-top: @LargeSpace;
|
||||
}
|
||||
}
|
||||
@@ -94,17 +95,17 @@
|
||||
}
|
||||
|
||||
/* Override bootstrap's nav pills */
|
||||
.nav-pills>li.active>a,
|
||||
.nav-pills>li.active>a:focus,
|
||||
.nav-pills>li.active>a:hover {
|
||||
.nav-pills > li.active > a,
|
||||
.nav-pills > li.active > a:focus,
|
||||
.nav-pills > li.active > a:hover {
|
||||
background-color: @AccentMediumHigh;
|
||||
}
|
||||
|
||||
.nav-pills>li>a {
|
||||
.nav-pills > li > a {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.nav>li>a {
|
||||
.nav > li > a {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
@@ -114,16 +115,16 @@
|
||||
|
||||
.typeahead__cancel-button {
|
||||
top: 5px !important;
|
||||
right: .4em !important;
|
||||
right: 0.4em !important;
|
||||
}
|
||||
|
||||
|
||||
.queryMetricsSummary {
|
||||
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
|
||||
table-layout: fixed;
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 100px;
|
||||
|
||||
.queryMetricsSummaryHead {
|
||||
.flex-display();
|
||||
@@ -144,7 +145,8 @@
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
.flex-display();
|
||||
th, td {
|
||||
th,
|
||||
td {
|
||||
padding: @DefaultSpace;
|
||||
|
||||
&:nth-child(1) {
|
||||
@@ -168,7 +170,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.graphContainer {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
@@ -176,7 +177,6 @@
|
||||
padding-top: (2 * @MediumSpace);
|
||||
.flex-display();
|
||||
|
||||
|
||||
.leftPane {
|
||||
width: 200px;
|
||||
padding: 0px 0px 0px @DefaultSpace;
|
||||
@@ -224,8 +224,8 @@
|
||||
.graphTitle {
|
||||
margin-top: @DefaultSpace;
|
||||
|
||||
.graphExpandCollapseBtn{
|
||||
padding:2px 6px 5px 6px;
|
||||
.graphExpandCollapseBtn {
|
||||
padding: 2px 6px 5px 6px;
|
||||
margin-right: @SmallSpace;
|
||||
|
||||
&:hover {
|
||||
@@ -236,7 +236,7 @@
|
||||
.active();
|
||||
}
|
||||
|
||||
img{
|
||||
img {
|
||||
.dataExplorerIcons();
|
||||
}
|
||||
}
|
||||
@@ -249,7 +249,7 @@
|
||||
height: 100%;
|
||||
|
||||
.graphModal {
|
||||
background-color: rgba(255, 255, 255, .7);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
@@ -282,7 +282,6 @@
|
||||
min-height: 0px;
|
||||
margin-top: 0px;
|
||||
|
||||
|
||||
.rightPaneHeader {
|
||||
/* TODO: Hack to align the trashbox with the header for now. */
|
||||
margin-top: -28px;
|
||||
@@ -406,16 +405,16 @@
|
||||
|
||||
.propertyTableHeader {
|
||||
font-weight: bold;
|
||||
color: @DefaultFontColor ;
|
||||
color: @DefaultFontColor;
|
||||
border-bottom: 1px solid @BaseMediumHigh;
|
||||
}
|
||||
|
||||
/* Override autocomplete stuff */
|
||||
.typeahead__container {
|
||||
font: @mediumFontSize 'Segoe UI' !important;
|
||||
font: @mediumFontSize "Segoe UI" !important;
|
||||
|
||||
input {
|
||||
font: @mediumFontSize 'Segoe UI' !important;
|
||||
font: @mediumFontSize "Segoe UI" !important;
|
||||
padding: 0px @SmallSpace !important;
|
||||
}
|
||||
}
|
||||
@@ -426,12 +425,12 @@
|
||||
|
||||
.typeahead__cancel-button {
|
||||
top: 5px !important;
|
||||
right: .4em !important;
|
||||
right: 0.4em !important;
|
||||
}
|
||||
|
||||
.rightPaneAddPropertyBtn {
|
||||
padding: @DefaultSpace;
|
||||
margin-left: -8px;/* TODO: Hack to align the addproperty button with the section content for now. */
|
||||
margin-left: -8px; /* TODO: Hack to align the addproperty button with the section content for now. */
|
||||
|
||||
img {
|
||||
margin: 0px @SmallSpace @SmallSpace 0px;
|
||||
@@ -450,6 +449,8 @@
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.paneTitle {
|
||||
@@ -586,7 +587,7 @@
|
||||
|
||||
.loadmore {
|
||||
use.loadMoreIcon {
|
||||
fill:@GraphNodeDefaultColor;
|
||||
fill: @GraphNodeDefaultColor;
|
||||
|
||||
&.active {
|
||||
fill: @AccentMediumHigh;
|
||||
@@ -608,8 +609,6 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* scroll for leftpane, rightpane and newvertex pane*/
|
||||
|
||||
@@ -9,8 +9,10 @@ import create, { UseStore } from "zustand";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
@@ -54,7 +56,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
|
||||
}
|
||||
|
||||
if (
|
||||
userContext.features.phoenix === false &&
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
|
||||
) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||
}
|
||||
|
||||
|
||||
@@ -103,19 +103,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(false);
|
||||
expect(enableNotebookBtn.tooltipText).toBe("");
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
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", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
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."
|
||||
);
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
//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 openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
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", () => {
|
||||
@@ -203,8 +212,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
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", () => {
|
||||
@@ -290,9 +302,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
|
||||
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", () => {
|
||||
@@ -302,8 +318,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
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("");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -67,35 +67,51 @@ export function createStaticCommandBarButtons(
|
||||
newCollectionBtn.children.push(newDatabaseBtn);
|
||||
}
|
||||
|
||||
buttons.push(createDivider());
|
||||
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
buttons.push(createDivider());
|
||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const newNotebookButton = createNewNotebookButton(container);
|
||||
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
|
||||
buttons.push(newNotebookButton);
|
||||
notebookButtons.push(newNotebookButton);
|
||||
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
buttons.push(createManageGitHubAccountButton(container));
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
|
||||
buttons.push(createOpenTerminalButton(container));
|
||||
|
||||
buttons.push(createNotebookWorkspaceResetButton(container));
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
if (userContext.features.phoenix === false) {
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
}
|
||||
if (
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
buttons.push(createDivider());
|
||||
notebookButtons.push(createDivider());
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
buttons.push(createOpenCassandraTerminalButton(container));
|
||||
notebookButtons.push(createOpenCassandraTerminalButton(container));
|
||||
} 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 {
|
||||
if (!isRunningOnNationalCloud()) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
@@ -152,7 +168,9 @@ export function createContextCommandBarButtons(
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
if (!userContext.features.notebooksTemporarilyDown) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
}
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
@@ -160,7 +178,13 @@ export function createContextCommandBarButtons(
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
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);
|
||||
}
|
||||
@@ -388,6 +412,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
|
||||
if (!buttonProps.isDivider) {
|
||||
buttonProps.disabled = true;
|
||||
buttonProps.tooltipText = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
@@ -565,7 +596,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
onCommandClick: () => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
@@ -575,7 +606,8 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>
|
||||
),
|
||||
);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
|
||||
@@ -13,6 +13,8 @@ import { StyleConstants } from "../../../Common/Constants";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { ConnectionStatus } from "./ConnectionStatusComponent";
|
||||
import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
|
||||
/**
|
||||
@@ -22,6 +24,13 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
|
||||
const getFilter = (isDisabled: boolean): string => {
|
||||
if (isDisabled) {
|
||||
return StyleConstants.GrayScale;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return btns
|
||||
.filter((btn) => btn)
|
||||
.map(
|
||||
@@ -37,6 +46,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined,
|
||||
filter: getFilter(btn.disabled),
|
||||
},
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName,
|
||||
@@ -123,8 +133,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
width: 12,
|
||||
paddingLeft: 1,
|
||||
paddingTop: 6,
|
||||
filter: getFilter(btn.disabled),
|
||||
},
|
||||
imageProps: {
|
||||
src: ChevronDownIcon,
|
||||
alt: btn.iconAlt,
|
||||
},
|
||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,3 +203,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
||||
onRender: () => <MemoryTracker />,
|
||||
};
|
||||
};
|
||||
|
||||
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <ConnectionStatus container={container} />,
|
||||
};
|
||||
};
|
||||
|
||||
184
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
Normal file
@@ -0,0 +1,184 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.connectionStatusContainer {
|
||||
cursor: default;
|
||||
align-items: center;
|
||||
border: 1px;
|
||||
min-height: 44px;
|
||||
|
||||
> span {
|
||||
padding-right: 12px;
|
||||
font-size: 12px;
|
||||
font-family: @DataExplorerFont;
|
||||
color: @DefaultFontColor;
|
||||
}
|
||||
&:focus{
|
||||
outline: 0px;
|
||||
}
|
||||
}
|
||||
.commandReactBtn {
|
||||
&:hover {
|
||||
background-color: rgb(238, 247, 255);
|
||||
color: rgb(32, 31, 30);
|
||||
cursor: pointer;
|
||||
}
|
||||
&:focus{
|
||||
outline: 1px dashed #605e5c;
|
||||
}
|
||||
}
|
||||
.connectedReactBtn {
|
||||
&:hover {
|
||||
background-color: rgb(238, 247, 255);
|
||||
color: rgb(32, 31, 30);
|
||||
cursor: pointer;
|
||||
}
|
||||
&:focus{
|
||||
outline: 0px;
|
||||
}
|
||||
}
|
||||
.connectIcon{
|
||||
margin: 0px 4px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
color: rgb(0, 120, 212);
|
||||
}
|
||||
.status {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-right: 8px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 9px!important;
|
||||
padding: 0px!important;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.status::before,
|
||||
.status::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.status::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-color: rgba(#fff, 0.1);
|
||||
border-radius: 100%;
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale(0);
|
||||
}
|
||||
|
||||
.connected{
|
||||
background-color: green;
|
||||
box-shadow:
|
||||
0 0 0 0em rgba(green, 0),
|
||||
0em 0.05em 0.1em rgba(#000000, 0.2);
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
.connecting{
|
||||
background-color:#ffbf00;
|
||||
box-shadow:
|
||||
0 0 0 0em rgba(#ffbf00, 0),
|
||||
0em 0.05em 0.1em rgba(#000000, 0.2);
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
.failed{
|
||||
background-color:#bd1919;
|
||||
box-shadow:
|
||||
0 0 0 0em rgba(#bd1919, 0),
|
||||
0em 0.05em 0.1em rgba(#000000, 0.2);
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
|
||||
.status.connecting.is-animating {
|
||||
animation: status-outer-connecting 3000ms infinite;
|
||||
}
|
||||
.status.failed.is-animating {
|
||||
animation: status-outer-failed 3000ms infinite;
|
||||
}
|
||||
.status.connected.is-animating {
|
||||
animation: status-outer-connected 3000ms infinite;
|
||||
}
|
||||
@keyframes status-outer-connected {
|
||||
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
20% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
40% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
60% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
80% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
85% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes status-outer-failed {
|
||||
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
20% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
40% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
60% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
80% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
85% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes status-outer-connecting {
|
||||
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
20% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
40% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
60% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
80% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
85% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
107
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
|
||||
import { ActionButton } from "@fluentui/react/lib/Button";
|
||||
import * as React from "react";
|
||||
import "../../../../less/hostedexplorer.less";
|
||||
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import "../CommandBar/ConnectionStatusComponent.less";
|
||||
interface Props {
|
||||
container: Explorer;
|
||||
}
|
||||
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): 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("");
|
||||
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
|
||||
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);
|
||||
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
||||
|
||||
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
|
||||
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
|
||||
|
||||
if (
|
||||
connectionInfo &&
|
||||
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
|
||||
) {
|
||||
return (
|
||||
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
||||
<TooltipHost content={toolTipContent}>
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
|
||||
<span>{connectionInfo.status}</span>
|
||||
</Stack>
|
||||
</TooltipHost>
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
|
||||
setIsActive(true);
|
||||
setStatusColor("status connecting is-animating");
|
||||
setToolTipContent("Connecting to temporary workspace.");
|
||||
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
|
||||
stopTimer();
|
||||
setStatusColor("status connected is-animating");
|
||||
setToolTipContent("Connected to temporary workspace.");
|
||||
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
|
||||
stopTimer();
|
||||
setStatusColor("status failed is-animating");
|
||||
setToolTipContent("Click here to Reconnect to temporary workspace.");
|
||||
}
|
||||
return (
|
||||
<ActionButton
|
||||
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
||||
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
|
||||
}
|
||||
>
|
||||
<TooltipHost content={toolTipContent}>
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<i className={statusColor}></i>
|
||||
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
||||
{connectionInfo.status}
|
||||
</span>
|
||||
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
|
||||
<ProgressIndicator description={minute + ":" + second} />
|
||||
)}
|
||||
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
|
||||
<ProgressIndicator
|
||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||
percentComplete={usedGB / totalGB}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</TooltipHost>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { Dropdown, IDropdownOption } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
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 ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
@@ -129,7 +129,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<img
|
||||
@@ -205,7 +205,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
||||
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
||||
<span className="date">{item.date}</span>
|
||||
<span className="message">{item.message}</span>
|
||||
<span className="message" role="alert" aria-live="assertive">
|
||||
{item.message}
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button expanded"
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -236,7 +236,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button expanded"
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -340,7 +340,9 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
date
|
||||
</span>
|
||||
<span
|
||||
aria-live="assertive"
|
||||
className="message"
|
||||
role="alert"
|
||||
>
|
||||
message
|
||||
</span>
|
||||
|
||||
@@ -277,6 +277,10 @@ export class NotebookComponentBootstrapper {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import * as NteractUtil from "../NTeractUtil";
|
||||
|
||||
interface VirtualCommandBarComponentProps {
|
||||
kernelSpecName: string;
|
||||
kernelStatus: string;
|
||||
currentCellType: string;
|
||||
isNotebookUntrusted: boolean;
|
||||
onRender: () => void;
|
||||
}
|
||||
|
||||
@@ -20,7 +22,8 @@ class VirtualCommandBarComponent extends React.Component<VirtualCommandBarCompon
|
||||
return (
|
||||
this.props.kernelStatus !== nextProps.kernelStatus ||
|
||||
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,
|
||||
kernelSpecName,
|
||||
currentCellType,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
} as VirtualCommandBarComponentProps;
|
||||
}
|
||||
|
||||
@@ -69,6 +73,7 @@ const makeMapStateToProps = (
|
||||
kernelStatus,
|
||||
kernelSpecName,
|
||||
currentCellType,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
onRender: initialProps.onRender,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useDialog } from "../../Controls/Dialog";
|
||||
import * as FileSystemUtil from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
@@ -108,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
|
||||
const q = params.toString();
|
||||
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");
|
||||
};
|
||||
@@ -686,10 +687,8 @@ const handleKernelConnectionLostEpic = (
|
||||
logConsoleError(msg);
|
||||
logFailureToTelemetry(state, "Kernel restart error", msg);
|
||||
|
||||
const explorer = window.dataExplorer;
|
||||
if (explorer) {
|
||||
explorer.showOkModalDialog("kernel restarts", msg);
|
||||
}
|
||||
useDialog.getState().showOkModalDialog("kernel restarts", msg);
|
||||
|
||||
return of(EMPTY);
|
||||
}
|
||||
|
||||
@@ -773,8 +772,7 @@ const closeUnsupportedMimetypesEpic = (
|
||||
ofType(actions.FETCH_CONTENT_FULFILLED),
|
||||
mergeMap((action) => {
|
||||
const mimetype = action.payload.model.mimetype;
|
||||
const explorer = window.dataExplorer;
|
||||
if (explorer && !TextFile.handles(mimetype)) {
|
||||
if (!TextFile.handles(mimetype)) {
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
useTabs
|
||||
@@ -783,7 +781,7 @@ const closeUnsupportedMimetypesEpic = (
|
||||
(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.`;
|
||||
explorer.showOkModalDialog("File cannot be rendered", msg);
|
||||
useDialog.getState().showOkModalDialog("File cannot be rendered", msg);
|
||||
logConsoleError(msg);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -803,8 +801,6 @@ const closeContentFailedToFetchEpic = (
|
||||
return action$.pipe(
|
||||
ofType(actions.FETCH_CONTENT_FAILED),
|
||||
mergeMap((action) => {
|
||||
const explorer = window.dataExplorer;
|
||||
if (explorer) {
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
useTabs
|
||||
@@ -813,9 +809,8 @@ const closeContentFailedToFetchEpic = (
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
const msg = `Failed to load file: ${filepath}.`;
|
||||
explorer.showOkModalDialog("Failure to load", msg);
|
||||
useDialog.getState().showOkModalDialog("Failure to load", msg);
|
||||
logConsoleError(msg);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
export class NotebookContainerClient {
|
||||
@@ -42,7 +45,7 @@ export class NotebookContainerClient {
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
@@ -56,7 +59,7 @@ export class NotebookContainerClient {
|
||||
|
||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||
try {
|
||||
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
|
||||
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: authToken,
|
||||
@@ -75,6 +78,12 @@ export class NotebookContainerClient {
|
||||
freeKB: memoryUsageInfo.free,
|
||||
};
|
||||
}
|
||||
} else if (NotebookUtil.isPhoenixEnabled()) {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.ReConnect,
|
||||
};
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(true);
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
@@ -84,6 +93,13 @@ export class NotebookContainerClient {
|
||||
"Connection lost with Notebook server. Attempting to reconnect..."
|
||||
);
|
||||
}
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(true);
|
||||
}
|
||||
this.onConnectionLost();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { stringifyNotebook } from "@nteract/commutable";
|
||||
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import * as StringUtils from "../../Utils/StringUtils";
|
||||
import * as FileSystemUtil from "./FileSystemUtil";
|
||||
@@ -14,7 +15,17 @@ export class NotebookContentClient {
|
||||
* This updates the item and points all the children's parent to this item
|
||||
* @param item
|
||||
*/
|
||||
public updateItemChildren(item: NotebookContentItem): Promise<void> {
|
||||
public async updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
|
||||
const subItems = await this.fetchNotebookFiles(item.path);
|
||||
const clonedItem = cloneDeep(item);
|
||||
subItems.forEach((subItem) => (subItem.parent = clonedItem));
|
||||
clonedItem.children = subItems;
|
||||
|
||||
return clonedItem;
|
||||
}
|
||||
|
||||
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||
public async updateItemChildrenInPlace(item: NotebookContentItem): Promise<void> {
|
||||
return this.fetchNotebookFiles(item.path).then((subItems) => {
|
||||
item.children = subItems;
|
||||
subItems.forEach((subItem) => (subItem.parent = item));
|
||||
@@ -25,7 +36,10 @@ export class NotebookContentClient {
|
||||
*
|
||||
* @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) {
|
||||
throw new Error(`Parent must be a directory: ${parent}`);
|
||||
}
|
||||
@@ -46,6 +60,8 @@ export class NotebookContentClient {
|
||||
const notebookFile = xhr.response;
|
||||
|
||||
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) {
|
||||
item.parent = parent;
|
||||
parent.children.push(item);
|
||||
@@ -55,8 +71,11 @@ export class NotebookContentClient {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteContentItem(item: NotebookContentItem): Promise<void> {
|
||||
return this.deleteNotebookFile(item.path).then((path: string) => {
|
||||
public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||
const path = await this.deleteNotebookFile(item.path);
|
||||
useNotebook.getState().deleteNotebookItem(item, isGithubTree);
|
||||
|
||||
// TODO: Delete once old resource tree is removed
|
||||
if (!path || path !== item.path) {
|
||||
throw new Error("No path provided");
|
||||
}
|
||||
@@ -66,7 +85,6 @@ export class NotebookContentClient {
|
||||
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
||||
item.parent.children = newChildren;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,12 +96,12 @@ export class NotebookContentClient {
|
||||
public async uploadFileAsync(
|
||||
name: string,
|
||||
content: string,
|
||||
parent: NotebookContentItem
|
||||
parent: NotebookContentItem,
|
||||
isGithubTree?: boolean
|
||||
): Promise<NotebookContentItem> {
|
||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||
throw new Error(`Parent must be a directory: ${parent}`);
|
||||
}
|
||||
|
||||
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
||||
if (await this.checkIfFilepathExists(filepath)) {
|
||||
throw new Error(`File already exists: ${filepath}`);
|
||||
@@ -102,6 +120,8 @@ export class NotebookContentClient {
|
||||
.then((xhr: AjaxResponse) => {
|
||||
const notebookFile = xhr.response;
|
||||
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) {
|
||||
item.parent = parent;
|
||||
parent.children.push(item);
|
||||
@@ -124,7 +144,11 @@ export class NotebookContentClient {
|
||||
* @param sourcePath
|
||||
* @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;
|
||||
// Match extension
|
||||
if (sourcePath.indexOf(".") !== -1) {
|
||||
@@ -150,6 +174,9 @@ export class NotebookContentClient {
|
||||
item.name = notebookFile.name;
|
||||
item.path = notebookFile.path;
|
||||
item.timestamp = NotebookUtil.getCurrentTimestamp();
|
||||
|
||||
useNotebook.getState().updateNotebookItem(item, isGithubTree);
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
@@ -159,7 +186,11 @@ export class NotebookContentClient {
|
||||
* @param parent
|
||||
* @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) {
|
||||
throw new Error(`Parent is not a directory: ${parent.path}`);
|
||||
}
|
||||
@@ -186,8 +217,11 @@ export class NotebookContentClient {
|
||||
|
||||
const dir = xhr.response;
|
||||
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;
|
||||
parent.children?.push(item);
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import Explorer from "../Explorer";
|
||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
@@ -29,6 +30,7 @@ import { SnapshotRequest } from "./NotebookComponent/types";
|
||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||
import { NotebookContentClient } from "./NotebookContentClient";
|
||||
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
type NotebookPaneContent = string | ImmutableNotebook;
|
||||
|
||||
@@ -110,6 +112,7 @@ export default class NotebookManager {
|
||||
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
|
||||
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
|
||||
this.params.resourceTree.triggerRender();
|
||||
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
|
||||
});
|
||||
this.refreshPinnedRepos();
|
||||
}
|
||||
@@ -141,6 +144,7 @@ export default class NotebookManager {
|
||||
notebookContentRef={notebookContentRef}
|
||||
onTakeSnapshot={onTakeSnapshot}
|
||||
/>,
|
||||
"440px",
|
||||
onClosePanel
|
||||
);
|
||||
}
|
||||
@@ -169,7 +173,9 @@ export default class NotebookManager {
|
||||
if (error.status === HttpStatusCodes.Unauthorized) {
|
||||
this.gitHubOAuthService.resetToken();
|
||||
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
@@ -193,7 +199,7 @@ export default class NotebookManager {
|
||||
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
@@ -206,6 +212,7 @@ export default class NotebookManager {
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
|
||||
@@ -6,7 +6,7 @@ import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
import * as React from "react";
|
||||
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 { Dispatch } from "redux";
|
||||
import { userContext } from "../../../UserContext";
|
||||
@@ -14,6 +14,7 @@ import * as cdbActions from "../NotebookComponent/actions";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
|
||||
import { AzureTheme } from "./AzureTheme";
|
||||
import "./base.css";
|
||||
import CellCreator from "./decorators/CellCreator";
|
||||
@@ -107,6 +108,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
return (
|
||||
<>
|
||||
<div className="NotebookRendererContainer">
|
||||
<SecurityWarningBar contentRef={this.props.contentRef} />
|
||||
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<KeyboardShortcuts contentRef={this.props.contentRef}>
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.disabledRunCellButton {
|
||||
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
|
||||
color: @BaseMediumHigh;
|
||||
}
|
||||
}
|
||||
|
||||
.greyStopButton {
|
||||
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
|
||||
color: @BaseMediumHigh;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Dispatch } from "redux";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { CdbAppState } from "../NotebookComponent/types";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
|
||||
export interface PassedPromptProps {
|
||||
id: string;
|
||||
@@ -12,6 +13,7 @@ export interface PassedPromptProps {
|
||||
status?: string;
|
||||
executionCount?: number;
|
||||
isHovered?: boolean;
|
||||
isRunDisabled?: boolean;
|
||||
runCell?: () => void;
|
||||
stopCell?: () => void;
|
||||
}
|
||||
@@ -20,6 +22,7 @@ interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
isHovered?: boolean;
|
||||
isNotebookUntrusted?: boolean;
|
||||
children: (props: PassedPromptProps) => React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -47,6 +50,7 @@ export class PromptPure extends React.Component<Props> {
|
||||
runCell: this.props.executeCell,
|
||||
stopCell: this.props.stopExecution,
|
||||
isHovered: this.props.isHovered,
|
||||
isRunDisabled: this.props.isNotebookUntrusted,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -75,6 +79,7 @@ const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((s
|
||||
status,
|
||||
executionCount,
|
||||
isHovered,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IconButton, Spinner, SpinnerSize } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import { PassedPromptProps } from "./Prompt";
|
||||
import "./Prompt.less";
|
||||
|
||||
@@ -23,15 +24,18 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
||||
</div>
|
||||
);
|
||||
} else if (props.isHovered) {
|
||||
const playButtonText = "Run cell";
|
||||
const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell";
|
||||
return (
|
||||
<div className={props.isRunDisabled ? "disabledRunCellButton" : ""}>
|
||||
<IconButton
|
||||
className="runCellButton"
|
||||
iconProps={{ iconName: "MSNVideosSolid" }}
|
||||
title={playButtonText}
|
||||
ariaLabel={playButtonText}
|
||||
disabled={props.isRunDisabled}
|
||||
onClick={props.runCell}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;
|
||||
|
||||
@@ -36,6 +36,7 @@ interface StateProps {
|
||||
cellIdAbove: CellId;
|
||||
cellIdBelow: CellId;
|
||||
hasCodeOutput: boolean;
|
||||
isNotebookUntrusted: boolean;
|
||||
}
|
||||
|
||||
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
|
||||
@@ -43,12 +44,16 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
|
||||
render(): JSX.Element {
|
||||
let items: IContextualMenuItem[] = [];
|
||||
const isNotebookUntrusted = this.props.isNotebookUntrusted;
|
||||
const runTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
|
||||
|
||||
if (this.props.cellType === "code") {
|
||||
items = items.concat([
|
||||
{
|
||||
key: "Run",
|
||||
text: "Run",
|
||||
title: runTooltip,
|
||||
disabled: isNotebookUntrusted,
|
||||
onClick: () => {
|
||||
this.props.executeCell();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
|
||||
@@ -223,6 +228,7 @@ const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state
|
||||
cellIdAbove,
|
||||
cellIdBelow,
|
||||
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, ownProps.contentRef),
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
DropTargetConnector,
|
||||
DropTargetMonitor,
|
||||
} from "react-dnd";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import styled, { StyledComponent } from "styled-components";
|
||||
@@ -123,9 +122,10 @@ export const cellTarget = {
|
||||
drop(props: Props, monitor: DropTargetMonitor, component: any): void {
|
||||
if (monitor) {
|
||||
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.
|
||||
props.moveCell({
|
||||
id: monitor.getItem().id,
|
||||
id: item.id,
|
||||
destinationId: props.id,
|
||||
above: hoverUpperHalf,
|
||||
contentRef: props.contentRef,
|
||||
|
||||
@@ -4,6 +4,7 @@ import Immutable from "immutable";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { NotebookUtil } from "../../../NotebookUtil";
|
||||
|
||||
interface ComponentProps {
|
||||
contentRef: ContentRef;
|
||||
@@ -14,6 +15,7 @@ interface StateProps {
|
||||
cellMap: Immutable.Map<string, any>;
|
||||
cellOrder: Immutable.List<string>;
|
||||
focusedCell?: string | null;
|
||||
isNotebookUntrusted: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
@@ -59,8 +61,13 @@ export class KeyboardShortcuts extends React.Component<Props> {
|
||||
cellOrder,
|
||||
focusedCell,
|
||||
cellMap,
|
||||
isNotebookUntrusted,
|
||||
} = this.props;
|
||||
|
||||
if (isNotebookUntrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ctrlKeyPressed = e.ctrlKey;
|
||||
// Allow cmd + enter (macOS) to operate like ctrl + enter
|
||||
if (process.platform === "darwin") {
|
||||
@@ -125,6 +132,7 @@ export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps)
|
||||
cellOrder,
|
||||
cellMap,
|
||||
focusedCell,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable";
|
||||
import { AppState, selectors } from "@nteract/core";
|
||||
import domtoimage from "dom-to-image";
|
||||
import Html2Canvas from "html2canvas";
|
||||
import path from "path";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import * as StringUtils from "../../Utils/StringUtils";
|
||||
import { SnapshotFragment } from "./NotebookComponent/types";
|
||||
@@ -11,6 +13,8 @@ import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentI
|
||||
export type FileType = "directory" | "file" | "notebook";
|
||||
// Utilities for notebooks
|
||||
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.
|
||||
*/
|
||||
@@ -153,6 +157,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
|
||||
* @param notebookObject
|
||||
@@ -315,4 +329,16 @@ export class NotebookUtil {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
public static getNotebookBtnTitle(fileName: string): string {
|
||||
if (this.isPhoenixEnabled()) {
|
||||
return `Download to ${fileName}`;
|
||||
} else {
|
||||
return `Download to my notebooks`;
|
||||
}
|
||||
}
|
||||
|
||||
public static isPhoenixEnabled(): boolean {
|
||||
return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -1,12 +1,22 @@
|
||||
import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import NotebookManager from "./NotebookManager";
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
|
||||
interface NotebookState {
|
||||
isNotebookEnabled: boolean;
|
||||
@@ -18,6 +28,13 @@ interface NotebookState {
|
||||
isShellEnabled: boolean;
|
||||
notebookBasePath: string;
|
||||
isInitializingNotebooks: boolean;
|
||||
myNotebooksContentRoot: NotebookContentItem;
|
||||
gitHubNotebooksContentRoot: NotebookContentItem;
|
||||
galleryContentRoot: NotebookContentItem;
|
||||
connectionInfo: ContainerConnectionInfo;
|
||||
notebookFolderName: string;
|
||||
isAllocating: boolean;
|
||||
isRefreshed: boolean;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
@@ -26,10 +43,21 @@ interface NotebookState {
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
||||
setNotebookBasePath: (notebookBasePath: string) => void;
|
||||
setNotebookFolderName: (notebookFolderName: string) => void;
|
||||
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
||||
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
|
||||
setIsAllocating: (isAllocating: boolean) => void;
|
||||
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
|
||||
setIsRefreshed: (isAllocating: boolean) => void;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
isNotebookEnabled: false,
|
||||
isNotebooksEnabledForAccount: false,
|
||||
notebookServerInfo: {
|
||||
@@ -46,6 +74,15 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
||||
isShellEnabled: false,
|
||||
notebookBasePath: Constants.Notebook.defaultBasePath,
|
||||
isInitializingNotebooks: false,
|
||||
myNotebooksContentRoot: undefined,
|
||||
gitHubNotebooksContentRoot: undefined,
|
||||
galleryContentRoot: undefined,
|
||||
connectionInfo: {
|
||||
status: ConnectionStatusType.Connect,
|
||||
},
|
||||
notebookFolderName: undefined,
|
||||
isAllocating: false,
|
||||
isRefreshed: false,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
@@ -56,6 +93,7 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||
const { databaseAccount, authType } = userContext;
|
||||
if (
|
||||
@@ -103,4 +141,142 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
||||
set({ isNotebooksEnabledForAccount: false });
|
||||
}
|
||||
},
|
||||
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
|
||||
const currentItem = root || get().myNotebooksContentRoot;
|
||||
|
||||
if (currentItem) {
|
||||
if (currentItem.path === item.path && currentItem.name === item.name) {
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
if (currentItem.children) {
|
||||
for (const childItem of currentItem.children) {
|
||||
const result = get().findItem(childItem, item);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, parent);
|
||||
item.parent = parentItem;
|
||||
if (parentItem.children) {
|
||||
parentItem.children.push(item);
|
||||
} else {
|
||||
parentItem.children = [item];
|
||||
}
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, item.parent);
|
||||
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||
parentItem.children.push(item);
|
||||
item.parent = parentItem;
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, item.parent);
|
||||
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||
const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
|
||||
set({ notebookFolderName });
|
||||
const myNotebooksContentRoot = {
|
||||
name: get().notebookFolderName,
|
||||
path: get().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
const galleryContentRoot = {
|
||||
name: "Gallery",
|
||||
path: "Gallery",
|
||||
type: NotebookContentItemType.File,
|
||||
};
|
||||
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
|
||||
? {
|
||||
name: "GitHub repos",
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
set({
|
||||
myNotebooksContentRoot,
|
||||
galleryContentRoot,
|
||||
gitHubNotebooksContentRoot,
|
||||
});
|
||||
|
||||
if (get().notebookServerInfo?.notebookServerEndpoint) {
|
||||
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
|
||||
set({ myNotebooksContentRoot: updatedRoot });
|
||||
|
||||
if (updatedRoot?.children) {
|
||||
// Count 1st generation children (tree is lazy-loaded)
|
||||
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
|
||||
updatedRoot.children.forEach((notebookItem) => {
|
||||
switch (notebookItem.type) {
|
||||
case NotebookContentItemType.File:
|
||||
nodeCounts.files++;
|
||||
break;
|
||||
case NotebookContentItemType.Directory:
|
||||
nodeCounts.directories++;
|
||||
break;
|
||||
case NotebookContentItemType.Notebook:
|
||||
nodeCounts.notebooks++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
|
||||
}
|
||||
}
|
||||
},
|
||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
|
||||
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
|
||||
if (gitHubNotebooksContentRoot) {
|
||||
gitHubNotebooksContentRoot.children = [];
|
||||
pinnedRepos?.forEach((pinnedRepo) => {
|
||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||
const repoTreeItem: NotebookContentItem = {
|
||||
name: repoFullName,
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
children: [],
|
||||
parent: gitHubNotebooksContentRoot,
|
||||
};
|
||||
|
||||
pinnedRepo.branches.forEach((branch) => {
|
||||
repoTreeItem.children.push({
|
||||
name: branch.name,
|
||||
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
|
||||
type: NotebookContentItemType.Directory,
|
||||
parent: repoTreeItem,
|
||||
});
|
||||
});
|
||||
|
||||
gitHubNotebooksContentRoot.children.push(repoTreeItem);
|
||||
});
|
||||
|
||||
set({ gitHubNotebooksContentRoot });
|
||||
}
|
||||
},
|
||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
||||
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
||||
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: undefined,
|
||||
authToken: undefined,
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
},
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
}));
|
||||
|
||||
@@ -113,11 +113,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
collectionId: "",
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey:
|
||||
(userContext.features.partitionKeyDefault && userContext.apiType === "SQL") ||
|
||||
(userContext.features.partitionKeyDefault && userContext.apiType === "Mongo")
|
||||
? "/id"
|
||||
: "",
|
||||
partitionKey: this.getPartitionKey(),
|
||||
enableDedicatedThroughput: false,
|
||||
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
|
||||
useHashV2: false,
|
||||
@@ -165,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()}.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -214,6 +210,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
className="panelTextField"
|
||||
aria-label="New database id"
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ newDatabaseId: event.target.value })
|
||||
@@ -240,7 +237,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -283,7 +280,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
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>
|
||||
</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."
|
||||
}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -413,7 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getPartitionKeyTooltipText()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</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
|
||||
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>
|
||||
</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
|
||||
per partition key."
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -554,61 +551,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||
this.scrollToAdvancedSection();
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
||||
{isCapabilityEnabled("EnableMongo") && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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."
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Checkbox
|
||||
label="Create a Wildcard Index on all fields"
|
||||
checked={this.state.createMongoWildCardIndex}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ createMongoWildCardIndex: isChecked })
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Checkbox
|
||||
label="My partition key is larger than 100 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ useHashV2: isChecked })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
@@ -619,7 +561,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -674,6 +616,61 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||
this.scrollToAdvancedSection();
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
||||
{isCapabilityEnabled("EnableMongo") && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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."
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Checkbox
|
||||
label="Create a Wildcard Index on all fields"
|
||||
checked={this.state.createMongoWildCardIndex}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ createMongoWildCardIndex: isChecked })
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Checkbox
|
||||
label="My partition key is larger than 100 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ useHashV2: isChecked })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
)}
|
||||
@@ -815,6 +812,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
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 {
|
||||
if (
|
||||
userContext.features.partitionKeyDefault &&
|
||||
|
||||
@@ -23,10 +23,12 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
||||
|
||||
export interface AddDatabasePaneProps {
|
||||
explorer: Explorer;
|
||||
buttonElement?: HTMLElement;
|
||||
}
|
||||
|
||||
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
explorer: container,
|
||||
buttonElement,
|
||||
}: AddDatabasePaneProps) => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
let throughput: number;
|
||||
@@ -77,6 +79,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
};
|
||||
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
||||
buttonElement.focus();
|
||||
}, []);
|
||||
|
||||
const onSubmit = () => {
|
||||
|
||||
@@ -198,6 +198,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<Stack className="panelGroupSpacing">
|
||||
<TextField
|
||||
aria-required="true"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
styles={getTextFieldStyles()}
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
@@ -285,6 +286,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
underlined
|
||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||
aria-required="true"
|
||||
required={true}
|
||||
ariaLabel="addCollection-tableId"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -75,6 +76,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
selectedLocation.owner,
|
||||
selectedLocation.repo
|
||||
)} - ${selectedLocation.branch}`;
|
||||
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
|
||||
destination = useNotebook.getState().notebookFolderName;
|
||||
}
|
||||
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
||||
@@ -98,6 +101,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
|
||||
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
|
||||
let parent: NotebookContentItem;
|
||||
let isGithubTree: boolean;
|
||||
switch (location.type) {
|
||||
case "MyNotebooks":
|
||||
parent = {
|
||||
@@ -105,21 +109,23 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
isGithubTree = false;
|
||||
break;
|
||||
|
||||
case "GitHub":
|
||||
parent = {
|
||||
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||
name: selectedLocation.branch,
|
||||
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
isGithubTree = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
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 => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import React, { FormEvent, FunctionComponent } from "react";
|
||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||
|
||||
interface Location {
|
||||
@@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
|
||||
const getDropDownOptions = (): IDropdownOption[] => {
|
||||
const options: IDropdownOption[] = [];
|
||||
|
||||
options.push({
|
||||
key: "MyNotebooks-Item",
|
||||
text: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
title: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
text: useNotebook.getState().notebookFolderName,
|
||||
title: useNotebook.getState().notebookFolderName,
|
||||
data: {
|
||||
type: "MyNotebooks",
|
||||
} as Location,
|
||||
|
||||
@@ -108,6 +108,8 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
submitButtonText: "OK",
|
||||
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 (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
@@ -123,6 +125,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
onChange={(event, newInput?: string) => {
|
||||
setInputCollectionName(newInput);
|
||||
}}
|
||||
ariaLabel={confirmContainer}
|
||||
/>
|
||||
</div>
|
||||
{shouldRecordFeedback() && (
|
||||
@@ -142,6 +145,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDeleteCollectionFeedback(newInput);
|
||||
}}
|
||||
ariaLabel={reasonInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -40,6 +40,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Confirm by typing the container id"
|
||||
autoFocus={true}
|
||||
id="confirmCollectionId"
|
||||
onChange={[Function]}
|
||||
@@ -53,6 +54,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
value=""
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Confirm by typing the container id"
|
||||
autoFocus={true}
|
||||
deferredValidationTime={200}
|
||||
id="confirmCollectionId"
|
||||
@@ -346,6 +348,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Confirm by typing the container id"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-57"
|
||||
id="confirmCollectionId"
|
||||
|
||||
@@ -118,7 +118,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
message:
|
||||
"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 (
|
||||
<RightPaneForm {...props}>
|
||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||
@@ -133,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDatabaseInput(newInput);
|
||||
}}
|
||||
ariaLabel={confirmDatabase}
|
||||
/>
|
||||
</div>
|
||||
{isLastNonEmptyDatabase() && (
|
||||
@@ -151,6 +153,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDatabaseFeedbackInput(newInput);
|
||||
}}
|
||||
ariaLabel={reasonInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -153,7 +153,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
selectedKey={paramKeyValue && paramKeyValue.key}
|
||||
/>
|
||||
))}
|
||||
<Stack horizontal onClick={addNewParamAtLastIndex}>
|
||||
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||
<Text className="addNewParamStyle">Add New Param</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -59,30 +59,36 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
onChange={onParamKeyChange}
|
||||
options={options}
|
||||
styles={dropdownStyles}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<TextField
|
||||
label={inputLabel && inputLabel}
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
value={paramValue}
|
||||
onChange={onParamValueChange}
|
||||
/>
|
||||
{isAddRemoveVisible && (
|
||||
<>
|
||||
<div tabIndex={0}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EntityCancelIcon}
|
||||
alt="Delete param"
|
||||
id="deleteparam"
|
||||
role="button"
|
||||
onClick={onDeleteParamKeyPress}
|
||||
/>
|
||||
</div>
|
||||
<div tabIndex={0}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={AddPropertyIcon}
|
||||
alt="Add param"
|
||||
id="addparam"
|
||||
role="button"
|
||||
onClick={onAddNewParamKeyPress}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -345,6 +345,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
},
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<DropdownBase
|
||||
label="Key"
|
||||
@@ -363,6 +364,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
selectedKey="string"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -664,6 +666,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
responsiveMode={3}
|
||||
selectedKey="string"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -1562,14 +1565,12 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</DropdownBase>
|
||||
</Dropdown>
|
||||
<StyledTextFieldBase
|
||||
autoFocus={true}
|
||||
id="confirmCollectionId"
|
||||
key=".0:$.1"
|
||||
label="Value"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<TextFieldBase
|
||||
autoFocus={true}
|
||||
deferredValidationTime={200}
|
||||
id="confirmCollectionId"
|
||||
label="Value"
|
||||
@@ -2155,7 +2156,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-labelledby="TextFieldLabel3"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-77"
|
||||
id="confirmCollectionId"
|
||||
onBlur={[Function]}
|
||||
@@ -2500,6 +2500,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
},
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<DropdownBase
|
||||
label="Key"
|
||||
@@ -2518,6 +2519,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
selectedKey="string"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -2819,6 +2821,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
responsiveMode={3}
|
||||
selectedKey="string"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -3717,7 +3720,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</DropdownBase>
|
||||
</Dropdown>
|
||||
<StyledTextFieldBase
|
||||
autoFocus={true}
|
||||
id="confirmCollectionId"
|
||||
key=".0:$.1"
|
||||
label="Param"
|
||||
@@ -3725,7 +3727,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
value=""
|
||||
>
|
||||
<TextFieldBase
|
||||
autoFocus={true}
|
||||
deferredValidationTime={200}
|
||||
id="confirmCollectionId"
|
||||
label="Param"
|
||||
@@ -4312,7 +4313,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-labelledby="TextFieldLabel7"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-77"
|
||||
id="confirmCollectionId"
|
||||
onBlur={[Function]}
|
||||
@@ -4327,12 +4327,16 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</TextFieldBase>
|
||||
</StyledTextFieldBase>
|
||||
<div
|
||||
tabIndex={0}
|
||||
>
|
||||
<StyledImageBase
|
||||
alt="Delete param"
|
||||
className="addRemoveIconLabel"
|
||||
height={30}
|
||||
id="deleteparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
width={20}
|
||||
>
|
||||
@@ -4342,6 +4346,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
height={30}
|
||||
id="deleteparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -4636,17 +4641,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
onClick={[Function]}
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</ImageBase>
|
||||
</StyledImageBase>
|
||||
</div>
|
||||
<div
|
||||
tabIndex={0}
|
||||
>
|
||||
<StyledImageBase
|
||||
alt="Add param"
|
||||
className="addRemoveIconLabel"
|
||||
height={30}
|
||||
id="addparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
width={20}
|
||||
>
|
||||
@@ -4656,6 +4667,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
height={30}
|
||||
id="addparam"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -4950,21 +4962,25 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
onClick={[Function]}
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</ImageBase>
|
||||
</StyledImageBase>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</InputParameter>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="ms-Stack css-54"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<StyledImageBase
|
||||
alt="Add param"
|
||||
|
||||
@@ -23,6 +23,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -31,19 +32,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"gitHubOAuthService": GitHubOAuthService {
|
||||
"junoClient": JunoClient {
|
||||
"cachedPinnedRepos": [Function],
|
||||
},
|
||||
"token": [Function],
|
||||
},
|
||||
"junoClient": JunoClient {
|
||||
"cachedPinnedRepos": [Function],
|
||||
},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
.panelFormWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.panelMainContent {
|
||||
flex-grow: 1;
|
||||
padding: 0 34px;
|
||||
margin: 20px 0;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
& > :not(.collapsibleSection) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
@@ -150,9 +150,6 @@
|
||||
.backImageIcon {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entityValueTextField {
|
||||
margin: 24px;
|
||||
}
|
||||
.addEntityDatePicker {
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
@@ -85,11 +85,12 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
|
||||
export const SidePanel: React.FC = () => {
|
||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||
const { isOpen, panelContent, headerText } = useSidePanel((state) => {
|
||||
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||
return {
|
||||
isOpen: state.isOpen,
|
||||
panelContent: state.panelContent,
|
||||
headerText: state.headerText,
|
||||
panelWidth: state.panelWidth,
|
||||
};
|
||||
});
|
||||
// TODO Refactor PanelContainerComponent into a functional component and remove this wrapper
|
||||
@@ -100,6 +101,7 @@ export const SidePanel: React.FC = () => {
|
||||
panelContent={panelContent}
|
||||
headerText={headerText}
|
||||
isConsoleExpanded={isConsoleExpanded}
|
||||
panelWidth={panelWidth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,13 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||
{icon}
|
||||
<span className="panelWarningErrorDetailsLinkContainer">
|
||||
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
|
||||
<Text
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-label={message}
|
||||
className="panelWarningErrorMessage"
|
||||
variant="small"
|
||||
>
|
||||
{message}
|
||||
{link && linkText && (
|
||||
<Link target="_blank" href={link}>
|
||||
|
||||
@@ -105,7 +105,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
|
||||
notebookName,
|
||||
notebookDescription,
|
||||
notebookTags?.split(","),
|
||||
author,
|
||||
imageSrc,
|
||||
content
|
||||
);
|
||||
|
||||