mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
103 Commits
fixed-ts-s
...
fix_a11y_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac39b7a852 | ||
|
|
b7f0548cca | ||
|
|
4728dc48d7 | ||
|
|
9358fd5889 | ||
|
|
f5da8bb276 | ||
|
|
de5df90f75 | ||
|
|
66421ad276 | ||
|
|
e70fa01a8b | ||
|
|
79b6f3cf2f | ||
|
|
b765cae088 | ||
|
|
591782195d | ||
|
|
c7ceda3a3e | ||
|
|
b19144f792 | ||
|
|
e61f9f2a38 | ||
|
|
025d5010b4 | ||
|
|
be28eb387b | ||
|
|
529202ba7e | ||
|
|
de58f570cd | ||
|
|
6351e2bcd2 | ||
|
|
d97b991378 | ||
|
|
b7daadee20 | ||
|
|
b327bfd0d6 | ||
|
|
469cd866e0 | ||
|
|
ada95eae1f | ||
|
|
8a8c023d7b | ||
|
|
667b1e1486 | ||
|
|
203c2ac246 | ||
|
|
5d235038ad | ||
|
|
6b4d6f986e | ||
|
|
e575b94ffa | ||
|
|
42bdcaf8d1 | ||
|
|
94a03e5b03 | ||
|
|
1155557af1 | ||
|
|
27a49e9aa9 | ||
|
|
fa8be2bc0f | ||
|
|
3aa4bbe266 | ||
|
|
2dfabf3c69 | ||
|
|
a3d88af175 | ||
|
|
5597a1e8b6 | ||
|
|
e3d5ad2ce8 | ||
|
|
64f36e2d28 | ||
|
|
4ce1252e58 | ||
|
|
7d9faec81e | ||
|
|
22da3b90ef | ||
|
|
361ac45e52 | ||
|
|
8aa764079a | ||
|
|
55837db65b | ||
|
|
9f27cb95b9 | ||
|
|
271256bffb | ||
|
|
aff7133095 | ||
|
|
bfd4948fb9 | ||
|
|
1c54459708 | ||
|
|
df3b18d585 | ||
|
|
882f0e1554 | ||
|
|
b67b76cc87 | ||
|
|
734ee1e436 | ||
|
|
ff498b51e2 | ||
|
|
ed1ffb692f | ||
|
|
f7fa3f7c09 | ||
|
|
6ebf19c0c9 | ||
|
|
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 |
@@ -81,17 +81,9 @@ src/Explorer/Tables/DataTable/DataTableBindingManager.ts
|
|||||||
src/Explorer/Tables/DataTable/DataTableBuilder.ts
|
src/Explorer/Tables/DataTable/DataTableBuilder.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
|
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
|
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableOperations.ts
|
|
||||||
src/Explorer/Tables/DataTable/DataTableViewModel.ts
|
src/Explorer/Tables/DataTable/DataTableViewModel.ts
|
||||||
src/Explorer/Tables/DataTable/TableCommands.ts
|
|
||||||
src/Explorer/Tables/DataTable/TableEntityCache.ts
|
|
||||||
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
|
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
|
||||||
src/Explorer/Tables/Entities.ts
|
|
||||||
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
|
|
||||||
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
|
|
||||||
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
|
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
|
||||||
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
|
|
||||||
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
|
|
||||||
src/Explorer/Tables/TableDataClient.ts
|
src/Explorer/Tables/TableDataClient.ts
|
||||||
src/Explorer/Tables/TableEntityProcessor.ts
|
src/Explorer/Tables/TableEntityProcessor.ts
|
||||||
src/Explorer/Tables/Utilities.ts
|
src/Explorer/Tables/Utilities.ts
|
||||||
@@ -115,15 +107,10 @@ src/Explorer/Tree/ObjectId.ts
|
|||||||
src/Explorer/Tree/ResourceTokenCollection.ts
|
src/Explorer/Tree/ResourceTokenCollection.ts
|
||||||
src/Explorer/Tree/StoredProcedure.ts
|
src/Explorer/Tree/StoredProcedure.ts
|
||||||
src/Explorer/Tree/TreeComponents.ts
|
src/Explorer/Tree/TreeComponents.ts
|
||||||
src/Explorer/Tree/Trigger.ts
|
|
||||||
src/Explorer/WaitsForTemplateViewModel.ts
|
src/Explorer/WaitsForTemplateViewModel.ts
|
||||||
src/GitHub/GitHubClient.test.ts
|
src/GitHub/GitHubClient.test.ts
|
||||||
src/GitHub/GitHubClient.ts
|
src/GitHub/GitHubClient.ts
|
||||||
src/GitHub/GitHubConnector.ts
|
|
||||||
src/GitHub/GitHubOAuthService.ts
|
|
||||||
src/Index.ts
|
src/Index.ts
|
||||||
src/Juno/JunoClient.test.ts
|
|
||||||
src/Juno/JunoClient.ts
|
|
||||||
src/Platform/Hosted/Authorization.ts
|
src/Platform/Hosted/Authorization.ts
|
||||||
src/ReactDevTools.ts
|
src/ReactDevTools.ts
|
||||||
src/Shared/Constants.ts
|
src/Shared/Constants.ts
|
||||||
@@ -143,20 +130,13 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
|||||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||||
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
|
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
|
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
|
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
|
||||||
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
|
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
|
||||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
|
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
|
||||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
|
|
||||||
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
|
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
|
||||||
src/Explorer/Notebook/NotebookComponent/contents/index.tsx
|
src/Explorer/Notebook/NotebookComponent/contents/index.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
|
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
|
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx
|
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ module.exports = {
|
|||||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
"@typescript-eslint/no-extraneous-class": "error",
|
"@typescript-eslint/no-extraneous-class": "error",
|
||||||
"no-null/no-null": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
|
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
|
||||||
eqeqeq: "error",
|
eqeqeq: "error",
|
||||||
|
|||||||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -12,7 +12,8 @@
|
|||||||
"--inspect-brk",
|
"--inspect-brk",
|
||||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||||
"--runInBand",
|
"--runInBand",
|
||||||
"--coverage", "false"
|
"--coverage",
|
||||||
|
"false"
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen",
|
"internalConsoleOptions": "neverOpen",
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
"--inspect-brk",
|
"--inspect-brk",
|
||||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||||
"${fileBasenameNoExtension}",
|
"${fileBasenameNoExtension}",
|
||||||
"--coverage", "false",
|
"--coverage",
|
||||||
|
"false",
|
||||||
// "--watch",
|
// "--watch",
|
||||||
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
||||||
// // https://github.com/facebook/jest/issues/6683
|
// // https://github.com/facebook/jest/issues/6683
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -22,5 +22,6 @@
|
|||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true,
|
"source.fixAll.eslint": true,
|
||||||
"source.organizeImports": true
|
"source.organizeImports": true
|
||||||
}
|
},
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||||
|
"isTerminalEnabled" : true
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com"
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
|
"isTerminalEnabled" : false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg
|
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/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">
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<path
|
<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"
|
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)"
|
transform="scale(0.5)" fill="#000" stroke="#000">
|
||||||
fill="#000"
|
|
||||||
stroke="#CCC"
|
|
||||||
>
|
|
||||||
</path>
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 449 B |
@@ -37,8 +37,8 @@ module.exports = {
|
|||||||
global: {
|
global: {
|
||||||
branches: 25,
|
branches: 25,
|
||||||
functions: 25,
|
functions: 25,
|
||||||
lines: 30,
|
lines: 29,
|
||||||
statements: 30,
|
statements: 29,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -129,6 +129,8 @@ module.exports = {
|
|||||||
// The test environment that will be used for testing
|
// The test environment that will be used for testing
|
||||||
// testEnvironment: "jest-environment-jsdom",
|
// testEnvironment: "jest-environment-jsdom",
|
||||||
|
|
||||||
|
modulePaths: ["node_modules", "<rootDir>/src"],
|
||||||
|
|
||||||
// Options that will be passed to the testEnvironment
|
// Options that will be passed to the testEnvironment
|
||||||
// testEnvironmentOptions: {},
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
|||||||
@@ -2077,7 +2077,7 @@ a:link {
|
|||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-x: auto;
|
overflow-x: clip;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -2245,7 +2245,7 @@ a:link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refreshColHeader {
|
.refreshColHeader {
|
||||||
padding: 3px 6px 6px 6px;
|
padding: 3px 6px 10px 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refreshColHeader:hover {
|
.refreshColHeader:hover {
|
||||||
@@ -2357,6 +2357,8 @@ a:link {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -2832,6 +2834,8 @@ a:link {
|
|||||||
|
|
||||||
#explorerNotificationConsole {
|
#explorerNotificationConsole {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uniqueIndexesContainer {
|
.uniqueIndexesContainer {
|
||||||
@@ -2865,14 +2869,14 @@ a:link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings-pane {
|
.settingsSection {
|
||||||
.settingsSection {
|
|
||||||
border-bottom: 1px solid @BaseMedium;
|
border-bottom: 1px solid @BaseMedium;
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
padding: @MediumSpace 0px;
|
padding: @MediumSpace 0px;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -2885,11 +2889,19 @@ settings-pane {
|
|||||||
|
|
||||||
.settingsSectionLabel {
|
.settingsSectionLabel {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageOptionsPart {
|
.pageOptionsPart {
|
||||||
padding-bottom: @MediumSpace;
|
padding-bottom: @MediumSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legendLabel {
|
||||||
|
border-bottom: 0px;
|
||||||
|
width: auto;
|
||||||
|
font-size: @mediumFontSize;
|
||||||
|
display: inline !important;
|
||||||
|
float: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { NormalizedEventKey } from "./Constants";
|
||||||
|
|
||||||
export interface CollapsedResourceTreeProps {
|
export interface CollapsedResourceTreeProps {
|
||||||
toggleLeftPaneExpanded: () => void;
|
toggleLeftPaneExpanded: () => void;
|
||||||
@@ -11,6 +12,21 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
|||||||
toggleLeftPaneExpanded,
|
toggleLeftPaneExpanded,
|
||||||
isLeftPaneExpanded,
|
isLeftPaneExpanded,
|
||||||
}: CollapsedResourceTreeProps): JSX.Element => {
|
}: CollapsedResourceTreeProps): JSX.Element => {
|
||||||
|
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusButton.current) {
|
||||||
|
focusButton.current.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||||
|
toggleLeftPaneExpanded();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
||||||
<div className="main-nav nav">
|
<div className="main-nav nav">
|
||||||
@@ -21,11 +37,14 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="Expand Tree"
|
aria-label="Expand Tree"
|
||||||
|
onClick={toggleLeftPaneExpanded}
|
||||||
|
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||||
|
ref={focusButton}
|
||||||
>
|
>
|
||||||
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}>
|
<span className="leftarrowCollapsed">
|
||||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||||
</span>
|
</span>
|
||||||
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
|
<span className="collectionCollapsed">
|
||||||
<span>{userContext.apiType} API</span>
|
<span>{userContext.apiType} API</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export class Flights {
|
|||||||
public static readonly AutoscaleTest = "autoscaletest";
|
public static readonly AutoscaleTest = "autoscaletest";
|
||||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||||
|
public static readonly PhoenixNotebooks = "phoenixnotebooks";
|
||||||
|
public static readonly PhoenixFeatures = "phoenixfeatures";
|
||||||
|
public static readonly NotebooksDownBanner = "notebooksdownbanner";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AfecFeatures {
|
export class AfecFeatures {
|
||||||
@@ -337,6 +340,19 @@ export enum ConflictOperationType {
|
|||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ConnectionStatusType {
|
||||||
|
Connect = "Connect",
|
||||||
|
Connecting = "Connecting",
|
||||||
|
Connected = "Connected",
|
||||||
|
Failed = "Connection Failed",
|
||||||
|
Reconnect = "Reconnect",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ContainerStatusType {
|
||||||
|
Active = "Active",
|
||||||
|
Disconnected = "Disconnected",
|
||||||
|
}
|
||||||
|
|
||||||
export const EmulatorMasterKey =
|
export const EmulatorMasterKey =
|
||||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||||
@@ -346,15 +362,37 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
|
|||||||
|
|
||||||
export class Notebook {
|
export class Notebook {
|
||||||
public static readonly defaultBasePath = "./notebooks";
|
public static readonly defaultBasePath = "./notebooks";
|
||||||
public static readonly heartbeatDelayMs = 5000;
|
public static readonly heartbeatDelayMs = 60000;
|
||||||
|
public static readonly containerStatusHeartbeatDelayMs = 30000;
|
||||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||||
public static readonly autoSaveIntervalMs = 120000;
|
public static readonly autoSaveIntervalMs = 300000;
|
||||||
|
public static readonly memoryGuageToGB = 1048576;
|
||||||
|
public static readonly lowMemoryThreshold = 0.8;
|
||||||
|
public static readonly remainingTimeForAlert = 10;
|
||||||
|
public static readonly retryAttempts = 3;
|
||||||
|
public static readonly retryAttemptDelayMs = 5000;
|
||||||
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
||||||
public static readonly mongoShellTemporarilyDownMsg =
|
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.";
|
"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 =
|
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.";
|
"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.";
|
||||||
|
public static newNotebookModalTitle = "Create notebook in temporary workspace";
|
||||||
|
public static newNotebookUploadModalTitle = "Upload notebook to 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 {
|
export class SparkLibrary {
|
||||||
@@ -375,3 +413,11 @@ export class TerminalQueryParams {
|
|||||||
public static readonly SubscriptionId = "subscriptionId";
|
public static readonly SubscriptionId = "subscriptionId";
|
||||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class JunoEndpoints {
|
||||||
|
public static readonly Test = "https://juno-test.documents-dev.windows-int.net";
|
||||||
|
public static readonly Test2 = "https://juno-test2.documents-dev.windows-int.net";
|
||||||
|
public static readonly Test3 = "https://juno-test3.documents-dev.windows-int.net";
|
||||||
|
public static readonly Prod = "https://tools.cosmos.azure.com";
|
||||||
|
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||||
|
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
import { configContext, Platform } from "../ConfigContext";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
@@ -77,10 +78,21 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||||
|
enum SDKSupportedCapabilities {
|
||||||
|
None = 0,
|
||||||
|
PartitionMerge = 1 << 0,
|
||||||
|
}
|
||||||
|
|
||||||
let _client: Cosmos.CosmosClient;
|
let _client: Cosmos.CosmosClient;
|
||||||
|
|
||||||
export function client(): Cosmos.CosmosClient {
|
export function client(): Cosmos.CosmosClient {
|
||||||
if (_client) return _client;
|
if (_client) return _client;
|
||||||
|
|
||||||
|
let _defaultHeaders: CosmosHeaders = {};
|
||||||
|
_defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] =
|
||||||
|
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||||
|
|
||||||
const options: Cosmos.CosmosClientOptions = {
|
const options: Cosmos.CosmosClientOptions = {
|
||||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||||
key: userContext.masterKey,
|
key: userContext.masterKey,
|
||||||
@@ -89,6 +101,7 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
enableEndpointDiscovery: false,
|
enableEndpointDiscovery: false,
|
||||||
},
|
},
|
||||||
userAgentSuffix: "Azure Portal",
|
userAgentSuffix: "Azure Portal",
|
||||||
|
defaultHeaders: _defaultHeaders,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (configContext.PROXY_PATH !== undefined) {
|
if (configContext.PROXY_PATH !== undefined) {
|
||||||
|
|||||||
@@ -3,8 +3,16 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
|||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
import {
|
||||||
|
deleteDocument,
|
||||||
|
getEndpoint,
|
||||||
|
getFeatureEndpointOrDefault,
|
||||||
|
queryDocuments,
|
||||||
|
readDocument,
|
||||||
|
updateDocument,
|
||||||
|
} from "./MongoProxyClient";
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
|
|
||||||
@@ -228,13 +236,12 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
it("returns a production endpoint", () => {
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a development endpoint", () => {
|
it("returns a development endpoint", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
const endpoint = getEndpoint("https://localhost:1234");
|
||||||
const endpoint = getEndpoint();
|
|
||||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,8 +249,35 @@ describe("MongoProxyClient", () => {
|
|||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.EncryptedToken,
|
authType: AuthType.EncryptedToken,
|
||||||
});
|
});
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("getFeatureEndpointOrDefault", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
|
});
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
||||||
|
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||||
|
});
|
||||||
|
const features = extractFeatures(params);
|
||||||
|
updateUserContext({
|
||||||
|
authType: AuthType.AAD,
|
||||||
|
features: features,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a local endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a production endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import queryString from "querystring";
|
import queryString from "querystring";
|
||||||
|
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
@@ -78,7 +80,7 @@ export function queryDocuments(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint() || "";
|
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -141,7 +143,8 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -181,7 +184,7 @@ export function createDocument(
|
|||||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||||
@@ -225,7 +228,7 @@ export function updateDocument(
|
|||||||
? documentId.partitionKeyProperty
|
? documentId.partitionKeyProperty
|
||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
@@ -266,7 +269,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
|||||||
? documentId.partitionKeyProperty
|
? documentId.partitionKeyProperty
|
||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
@@ -309,7 +312,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(
|
.fetch(
|
||||||
@@ -333,8 +336,18 @@ export function createMongoCollectionWithProxy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEndpoint(): string {
|
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||||
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
|
const endpoint =
|
||||||
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
|
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||||
|
? userContext.features.mongoProxyEndpoint
|
||||||
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
|
|
||||||
|
return getEndpoint(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEndpoint(endpoint: string): string {
|
||||||
|
let url = endpoint + "/api/mongo/explorer";
|
||||||
|
|
||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
url = url.replace("api/mongo", "api/guest/mongo");
|
url = url.replace("api/mongo", "api/guest/mongo");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
@@ -6,6 +6,7 @@ import Explorer from "../Explorer/Explorer";
|
|||||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { NormalizedEventKey } from "./Constants";
|
||||||
|
|
||||||
export interface ResourceTreeContainerProps {
|
export interface ResourceTreeContainerProps {
|
||||||
toggleLeftPaneExpanded: () => void;
|
toggleLeftPaneExpanded: () => void;
|
||||||
@@ -18,6 +19,22 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
|||||||
isLeftPaneExpanded,
|
isLeftPaneExpanded,
|
||||||
container,
|
container,
|
||||||
}: ResourceTreeContainerProps): JSX.Element => {
|
}: ResourceTreeContainerProps): JSX.Element => {
|
||||||
|
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLeftPaneExpanded) {
|
||||||
|
if (focusButton.current) {
|
||||||
|
focusButton.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||||
|
toggleLeftPaneExpanded();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||||
{/* Collections Window - - Start */}
|
{/* Collections Window - - Start */}
|
||||||
@@ -43,9 +60,11 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
|||||||
id="expandToggleLeftPaneButton"
|
id="expandToggleLeftPaneButton"
|
||||||
role="button"
|
role="button"
|
||||||
onClick={toggleLeftPaneExpanded}
|
onClick={toggleLeftPaneExpanded}
|
||||||
|
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="Collapse Tree"
|
aria-label="Collapse Tree"
|
||||||
title="Collapse Tree"
|
title="Collapse Tree"
|
||||||
|
ref={focusButton}
|
||||||
>
|
>
|
||||||
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<TooltipHost content={children}>
|
<TooltipHost content={children}>
|
||||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
import {
|
||||||
|
allowedAadEndpoints,
|
||||||
|
allowedArcadiaEndpoints,
|
||||||
|
allowedArmEndpoints,
|
||||||
|
allowedBackendEndpoints,
|
||||||
|
allowedEmulatorEndpoints,
|
||||||
|
allowedGraphEndpoints,
|
||||||
|
allowedHostedExplorerEndpoints,
|
||||||
|
allowedJunoOrigins,
|
||||||
|
allowedMongoBackendEndpoints,
|
||||||
|
allowedMsalRedirectEndpoints,
|
||||||
|
validateEndpoint,
|
||||||
|
} from "Utils/EndpointValidation";
|
||||||
|
|
||||||
export enum Platform {
|
export enum Platform {
|
||||||
Portal = "Portal",
|
Portal = "Portal",
|
||||||
Hosted = "Hosted",
|
Hosted = "Hosted",
|
||||||
@@ -6,7 +20,7 @@ export enum Platform {
|
|||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedParentFrameOrigins: string[];
|
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
AAD_ENDPOINT: string;
|
AAD_ENDPOINT: string;
|
||||||
@@ -23,10 +37,11 @@ export interface ConfigContext {
|
|||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
|
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
|
isTerminalEnabled: boolean;
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
allowedJunoOrigins: string[];
|
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +55,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
|
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
|
||||||
],
|
], // Webpack injects this at build time
|
||||||
// Webpack injects this at build time
|
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||||
@@ -52,16 +66,11 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||||
|
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
allowedJunoOrigins: [
|
isTerminalEnabled: false,
|
||||||
"https://juno-test.documents-dev.windows-int.net",
|
|
||||||
"https://juno-test2.documents-dev.windows-int.net",
|
|
||||||
"https://tools.cosmos.azure.com",
|
|
||||||
"https://tools-staging.cosmos.azure.com",
|
|
||||||
"https://localhost",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
@@ -72,6 +81,50 @@ export function resetConfigContext(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||||
|
if (!newContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
|
||||||
|
delete newContext.ARM_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
||||||
|
delete newContext.AAD_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||||
|
delete newContext.EMULATOR_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
||||||
|
delete newContext.GRAPH_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.ARCADIA_ENDPOINT, allowedArcadiaEndpoints)) {
|
||||||
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
|
||||||
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||||
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||||
|
delete newContext.JUNO_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.hostedExplorerURL, allowedHostedExplorerEndpoints)) {
|
||||||
|
delete newContext.hostedExplorerURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.msalRedirectURI, allowedMsalRedirectEndpoints)) {
|
||||||
|
delete newContext.msalRedirectURI;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(configContext, newContext);
|
Object.assign(configContext, newContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,18 +148,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
|||||||
});
|
});
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
try {
|
try {
|
||||||
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
|
const { ...externalConfig } = await response.json();
|
||||||
Object.assign(configContext, externalConfig);
|
updateConfigContext(externalConfig);
|
||||||
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
|
||||||
updateConfigContext({
|
|
||||||
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
|
|
||||||
updateConfigContext({
|
|
||||||
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to parse json in config file");
|
console.error("Unable to parse json in config file");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||||
|
|
||||||
export interface DatabaseAccount {
|
export interface DatabaseAccount {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -24,6 +26,8 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
isVirtualNetworkFilterEnabled?: boolean;
|
isVirtualNetworkFilterEnabled?: boolean;
|
||||||
ipRules?: IpRule[];
|
ipRules?: IpRule[];
|
||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
|
capacity?: { totalThroughputLimit: number };
|
||||||
|
locations?: DatabaseAccountResponseLocation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountResponseLocation {
|
export interface DatabaseAccountResponseLocation {
|
||||||
@@ -424,6 +428,32 @@ export interface OperationStatus {
|
|||||||
export interface NotebookWorkspaceConnectionInfo {
|
export interface NotebookWorkspaceConnectionInfo {
|
||||||
authToken: string;
|
authToken: string;
|
||||||
notebookServerEndpoint: string;
|
notebookServerEndpoint: string;
|
||||||
|
forwardingId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerInfo {
|
||||||
|
durationLeftInMinutes: number;
|
||||||
|
notebookServerInfo: NotebookWorkspaceConnectionInfo;
|
||||||
|
status: ContainerStatusType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProvisionData {
|
||||||
|
cosmosEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IContainerData {
|
||||||
|
forwardingId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResponse<T> {
|
||||||
|
status: number;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPhoenixConnectionInfoResult {
|
||||||
|
readonly notebookAuthToken?: string;
|
||||||
|
readonly notebookServerUrl?: string;
|
||||||
|
readonly forwardingId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotebookWorkspaceFeedResponse {
|
export interface NotebookWorkspaceFeedResponse {
|
||||||
@@ -496,3 +526,8 @@ export interface MemoryUsageInfo {
|
|||||||
freeKB: number;
|
freeKB: number;
|
||||||
totalKB: number;
|
totalKB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContainerConnectionInfo {
|
||||||
|
status: ConnectionStatusType;
|
||||||
|
//need to add ram and rom info
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export enum MessageTypes {
|
|||||||
CreateWorkspace,
|
CreateWorkspace,
|
||||||
CreateSparkPool,
|
CreateSparkPool,
|
||||||
RefreshDatabaseAccount,
|
RefreshDatabaseAccount,
|
||||||
|
CloseTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ export const createCollectionContextMenuButton = (
|
|||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Icon, Label, Stack } from "@fluentui/react";
|
import { Icon, Label, Stack } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
|
|
||||||
export interface CollapsibleSectionProps {
|
export interface CollapsibleSectionProps {
|
||||||
@@ -30,6 +31,13 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onKeyPress = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||||
|
this.toggleCollapsed();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -39,6 +47,11 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
tokens={accordionStackTokens}
|
tokens={accordionStackTokens}
|
||||||
onClick={this.toggleCollapsed}
|
onClick={this.toggleCollapsed}
|
||||||
|
onKeyPress={this.onKeyPress}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-name="Advanced"
|
||||||
|
role="button"
|
||||||
|
aria-expanded={this.state.isExpanded}
|
||||||
>
|
>
|
||||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||||
<Label>{this.props.title}</Label>
|
<Label>{this.props.title}</Label>
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
exports[`CollapsibleSectionComponent renders 1`] = `
|
exports[`CollapsibleSectionComponent renders 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Stack
|
<Stack
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-name="Advanced"
|
||||||
className="collapsibleSection"
|
className="collapsibleSection"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 10,
|
"childrenGap": 10,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface DialogState {
|
|||||||
onOk: () => void,
|
onOk: () => void,
|
||||||
cancelLabel: string,
|
cancelLabel: string,
|
||||||
onCancel: () => void,
|
onCancel: () => void,
|
||||||
|
contentHtml?: JSX.Element,
|
||||||
choiceGroupProps?: IChoiceGroupProps,
|
choiceGroupProps?: IChoiceGroupProps,
|
||||||
textFieldProps?: TextFieldProps,
|
textFieldProps?: TextFieldProps,
|
||||||
primaryButtonDisabled?: boolean
|
primaryButtonDisabled?: boolean
|
||||||
@@ -58,6 +59,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
onOk: () => void,
|
onOk: () => void,
|
||||||
cancelLabel: string,
|
cancelLabel: string,
|
||||||
onCancel: () => void,
|
onCancel: () => void,
|
||||||
|
contentHtml?: JSX.Element,
|
||||||
choiceGroupProps?: IChoiceGroupProps,
|
choiceGroupProps?: IChoiceGroupProps,
|
||||||
textFieldProps?: TextFieldProps,
|
textFieldProps?: TextFieldProps,
|
||||||
primaryButtonDisabled?: boolean
|
primaryButtonDisabled?: boolean
|
||||||
@@ -76,6 +78,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
get().closeDialog();
|
get().closeDialog();
|
||||||
onCancel && onCancel();
|
onCancel && onCancel();
|
||||||
},
|
},
|
||||||
|
contentHtml,
|
||||||
choiceGroupProps,
|
choiceGroupProps,
|
||||||
textFieldProps,
|
textFieldProps,
|
||||||
primaryButtonDisabled,
|
primaryButtonDisabled,
|
||||||
@@ -124,6 +127,7 @@ export interface DialogProps {
|
|||||||
type?: DialogType;
|
type?: DialogType;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
|
contentHtml?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIALOG_MIN_WIDTH = "400px";
|
const DIALOG_MIN_WIDTH = "400px";
|
||||||
@@ -150,6 +154,7 @@ export const Dialog: FC = () => {
|
|||||||
type,
|
type,
|
||||||
showCloseButton,
|
showCloseButton,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
contentHtml,
|
||||||
} = props || {};
|
} = props || {};
|
||||||
|
|
||||||
const dialogProps: IDialogProps = {
|
const dialogProps: IDialogProps = {
|
||||||
@@ -181,8 +186,7 @@ export const Dialog: FC = () => {
|
|||||||
text: secondaryButtonText,
|
text: secondaryButtonText,
|
||||||
onClick: onSecondaryButtonClick,
|
onClick: onSecondaryButtonClick,
|
||||||
}
|
}
|
||||||
: {};
|
: undefined;
|
||||||
|
|
||||||
return visible ? (
|
return visible ? (
|
||||||
<FluentDialog {...dialogProps}>
|
<FluentDialog {...dialogProps}>
|
||||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||||
@@ -192,6 +196,7 @@ export const Dialog: FC = () => {
|
|||||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
{contentHtml}
|
||||||
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<PrimaryButton {...primaryButtonProps} />
|
<PrimaryButton {...primaryButtonProps} />
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
|
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
|
import * as UrlUtility from "../../../Common/UrlUtility";
|
||||||
|
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import { RepoListItem } from "./GitHubReposComponent";
|
import { RepoListItem } from "./GitHubReposComponent";
|
||||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
|
||||||
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import * as UrlUtility from "../../../Common/UrlUtility";
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
|
|
||||||
export interface AddRepoComponentProps {
|
export interface AddRepoComponentProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
@@ -27,7 +27,6 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
|||||||
private static readonly ButtonText = "Add";
|
private static readonly ButtonText = "Add";
|
||||||
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
|
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
|
||||||
private static readonly TextFieldErrorMessage = "Invalid url";
|
private static readonly TextFieldErrorMessage = "Invalid url";
|
||||||
private static readonly DefaultBranchName = "master";
|
|
||||||
|
|
||||||
constructor(props: AddRepoComponentProps) {
|
constructor(props: AddRepoComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -78,7 +77,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
|||||||
});
|
});
|
||||||
let enteredUrl = this.state.textFieldValue;
|
let enteredUrl = this.state.textFieldValue;
|
||||||
if (enteredUrl.indexOf("/tree/") === -1) {
|
if (enteredUrl.indexOf("/tree/") === -1) {
|
||||||
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`);
|
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
|
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
|
||||||
@@ -93,11 +92,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
|||||||
const item: RepoListItem = {
|
const item: RepoListItem = {
|
||||||
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
||||||
repo,
|
repo,
|
||||||
branches: [
|
branches: repoInfo.branch ? [{ name: repoInfo.branch }] : [],
|
||||||
{
|
|
||||||
name: repoInfo.branch,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import { RepoListItem } from "./GitHubReposComponent";
|
|||||||
import {
|
import {
|
||||||
BranchesDropdownCheckboxStyles,
|
BranchesDropdownCheckboxStyles,
|
||||||
BranchesDropdownOptionContainerStyle,
|
BranchesDropdownOptionContainerStyle,
|
||||||
|
BranchesDropdownStyles,
|
||||||
|
BranchesDropdownWidth,
|
||||||
|
ReposListBranchesColumnWidth,
|
||||||
ReposListCheckboxStyles,
|
ReposListCheckboxStyles,
|
||||||
ReposListRepoColumnMinWidth,
|
ReposListRepoColumnMinWidth,
|
||||||
ReposListBranchesColumnWidth,
|
|
||||||
BranchesDropdownWidth,
|
|
||||||
BranchesDropdownStyles,
|
|
||||||
} from "./GitHubStyleConstants";
|
} from "./GitHubStyleConstants";
|
||||||
|
|
||||||
export interface ReposListComponentProps {
|
export interface ReposListComponentProps {
|
||||||
@@ -44,6 +44,7 @@ export interface BranchesProps {
|
|||||||
lastPageInfo?: IGitHubPageInfo;
|
lastPageInfo?: IGitHubPageInfo;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
defaultBranchName: string;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
private static readonly BranchesColumnName = "Branches";
|
private static readonly BranchesColumnName = "Branches";
|
||||||
private static readonly LoadingText = "Loading...";
|
private static readonly LoadingText = "Loading...";
|
||||||
private static readonly LoadMoreText = "Load more";
|
private static readonly LoadMoreText = "Load more";
|
||||||
private static readonly DefaultBranchName = "master";
|
private static readonly DefaultBranchNames = "master/main";
|
||||||
private static readonly FooterIndex = -1;
|
private static readonly FooterIndex = -1;
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
@@ -155,6 +156,10 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
||||||
|
if (item.branches.length === 0 && branchesProps.defaultBranchName) {
|
||||||
|
item.branches = [{ name: branchesProps.defaultBranchName }];
|
||||||
|
}
|
||||||
|
|
||||||
const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({
|
const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({
|
||||||
key: branch.name,
|
key: branch.name,
|
||||||
text: branch.name,
|
text: branch.name,
|
||||||
@@ -198,7 +203,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
const dropdownProps: IDropdownProps = {
|
const dropdownProps: IDropdownProps = {
|
||||||
styles: BranchesDropdownStyles,
|
styles: BranchesDropdownStyles,
|
||||||
options: [],
|
options: [],
|
||||||
placeholder: ReposListComponent.DefaultBranchName,
|
placeholder: ReposListComponent.DefaultBranchNames,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,7 +277,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
styles: ReposListCheckboxStyles,
|
styles: ReposListCheckboxStyles,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
const repoListItem = { ...item };
|
const repoListItem = { ...item };
|
||||||
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
|
repoListItem.branches = [];
|
||||||
this.props.pinRepo(repoListItem);
|
this.props.pinRepo(repoListItem);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
.input-type-head-text-field {
|
.input-type-head-text-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.input-query-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -160,9 +160,11 @@ export class InputTypeaheadComponent extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<div className="input-typeahead-container">
|
<div className="input-typeahead-container">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
|
<form aria-labelledby="input" className="input-query-form">
|
||||||
<TextField
|
<TextField
|
||||||
multiline={useTextarea}
|
multiline={useTextarea}
|
||||||
rows={1}
|
rows={1}
|
||||||
|
id="input"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
ariaLabel="Input query"
|
ariaLabel="Input query"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -172,6 +174,7 @@ export class InputTypeaheadComponent extends React.Component<
|
|||||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||||
/>
|
/>
|
||||||
|
</form>
|
||||||
{this.props.showCancelButton && (
|
{this.props.showCancelButton && (
|
||||||
<IconButton
|
<IconButton
|
||||||
styles={iconButtonStyles}
|
styles={iconButtonStyles}
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ exports[`inputTypeahead renders <input /> 1`] = `
|
|||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
aria-labelledby="input"
|
||||||
|
className="input-query-form"
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Input query"
|
ariaLabel="Input query"
|
||||||
className="input-type-head-text-field"
|
className="input-type-head-text-field"
|
||||||
|
id="input"
|
||||||
multiline={false}
|
multiline={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
@@ -17,6 +22,7 @@ exports[`inputTypeahead renders <input /> 1`] = `
|
|||||||
placeholder="placeholder"
|
placeholder="placeholder"
|
||||||
rows={1}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
|
</form>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -27,10 +33,15 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
|
|||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
aria-labelledby="input"
|
||||||
|
className="input-query-form"
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Input query"
|
ariaLabel="Input query"
|
||||||
className="input-type-head-text-field"
|
className="input-type-head-text-field"
|
||||||
|
id="input"
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
@@ -38,6 +49,7 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
|
|||||||
placeholder="placeholder"
|
placeholder="placeholder"
|
||||||
rows={1}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
|
</form>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -35,16 +35,19 @@ const testCassandraAccount: DataModels.DatabaseAccount = {
|
|||||||
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
authToken: "authToken",
|
authToken: "authToken",
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
||||||
|
forwardingId: "Id",
|
||||||
};
|
};
|
||||||
|
|
||||||
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
authToken: "authToken",
|
authToken: "authToken",
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||||
|
forwardingId: "Id",
|
||||||
};
|
};
|
||||||
|
|
||||||
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
authToken: "authToken",
|
authToken: "authToken",
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||||
|
forwardingId: "Id",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("NotebookTerminalComponent", () => {
|
describe("NotebookTerminalComponent", () => {
|
||||||
@@ -52,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
|
|||||||
const props: NotebookTerminalComponentProps = {
|
const props: NotebookTerminalComponentProps = {
|
||||||
databaseAccount: testAccount,
|
databaseAccount: testAccount,
|
||||||
notebookServerInfo: testNotebookServerInfo,
|
notebookServerInfo: testNotebookServerInfo,
|
||||||
|
tabId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
@@ -62,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
|
|||||||
const props: NotebookTerminalComponentProps = {
|
const props: NotebookTerminalComponentProps = {
|
||||||
databaseAccount: testMongo32Account,
|
databaseAccount: testMongo32Account,
|
||||||
notebookServerInfo: testMongoNotebookServerInfo,
|
notebookServerInfo: testMongoNotebookServerInfo,
|
||||||
|
tabId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
@@ -72,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
|
|||||||
const props: NotebookTerminalComponentProps = {
|
const props: NotebookTerminalComponentProps = {
|
||||||
databaseAccount: testMongo36Account,
|
databaseAccount: testMongo36Account,
|
||||||
notebookServerInfo: testMongoNotebookServerInfo,
|
notebookServerInfo: testMongoNotebookServerInfo,
|
||||||
|
tabId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
@@ -82,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
|
|||||||
const props: NotebookTerminalComponentProps = {
|
const props: NotebookTerminalComponentProps = {
|
||||||
databaseAccount: testCassandraAccount,
|
databaseAccount: testCassandraAccount,
|
||||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||||
|
tabId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils";
|
|||||||
export interface NotebookTerminalComponentProps {
|
export interface NotebookTerminalComponentProps {
|
||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
|
tabId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||||
@@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
apiType: userContext.apiType,
|
apiType: userContext.apiType,
|
||||||
authType: userContext.authType,
|
authType: userContext.authType,
|
||||||
databaseAccount: userContext.databaseAccount,
|
databaseAccount: userContext.databaseAccount,
|
||||||
|
tabId: this.props.tabId,
|
||||||
};
|
};
|
||||||
|
|
||||||
postRobot.send(this.terminalWindow, "props", props, {
|
postRobot.send(this.terminalWindow, "props", props, {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import Explorer from "../../Explorer";
|
|||||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||||
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
|
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
|
||||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||||
import "./NotebookViewerComponent.less";
|
import "./NotebookViewerComponent.less";
|
||||||
@@ -51,7 +52,7 @@ export class NotebookViewerComponent
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.clientManager = new NotebookClientV2({
|
this.clientManager = new NotebookClientV2({
|
||||||
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined },
|
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined, forwardingId: undefined },
|
||||||
databaseAccountName: undefined,
|
databaseAccountName: undefined,
|
||||||
defaultExperience: "NotebookViewer",
|
defaultExperience: "NotebookViewer",
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
@@ -146,7 +147,7 @@ export class NotebookViewerComponent
|
|||||||
<NotebookMetadataComponent
|
<NotebookMetadataComponent
|
||||||
data={this.state.galleryItem}
|
data={this.state.galleryItem}
|
||||||
isFavorite={this.state.isFavorite}
|
isFavorite={this.state.isFavorite}
|
||||||
downloadButtonText={this.props.container && "Download to my notebooks"}
|
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
|
||||||
onTagClick={this.props.onTagClick}
|
onTagClick={this.props.onTagClick}
|
||||||
onFavoriteClick={this.favoriteItem}
|
onFavoriteClick={this.favoriteItem}
|
||||||
onUnfavoriteClick={this.unfavoriteItem}
|
onUnfavoriteClick={this.unfavoriteItem}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
@@ -71,6 +72,7 @@ export interface SettingsComponentState {
|
|||||||
wasAutopilotOriginallySet: boolean;
|
wasAutopilotOriginallySet: boolean;
|
||||||
isScaleSaveable: boolean;
|
isScaleSaveable: boolean;
|
||||||
isScaleDiscardable: boolean;
|
isScaleDiscardable: boolean;
|
||||||
|
throughputError: string;
|
||||||
|
|
||||||
timeToLive: TtlType;
|
timeToLive: TtlType;
|
||||||
timeToLiveBaseline: TtlType;
|
timeToLiveBaseline: TtlType;
|
||||||
@@ -124,6 +126,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
@@ -155,6 +158,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
wasAutopilotOriginallySet: false,
|
wasAutopilotOriginallySet: false,
|
||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
|
throughputError: undefined,
|
||||||
|
|
||||||
timeToLive: undefined,
|
timeToLive: undefined,
|
||||||
timeToLiveBaseline: undefined,
|
timeToLiveBaseline: undefined,
|
||||||
@@ -208,6 +212,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
if (throughputCap && throughputCap !== -1) {
|
||||||
|
this.calculateTotalThroughputUsed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
@@ -254,6 +263,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.throughputError) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.state.isScaleSaveable ||
|
this.state.isScaleSaveable ||
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
@@ -481,6 +494,26 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
||||||
this.setState({ isMongoIndexingPolicyDiscardable });
|
this.setState({ isMongoIndexingPolicyDiscardable });
|
||||||
|
|
||||||
|
private calculateTotalThroughputUsed = (): void => {
|
||||||
|
this.totalThroughputUsed = 0;
|
||||||
|
(useDatabases.getState().databases || []).forEach(async (database) => {
|
||||||
|
if (database.offer()) {
|
||||||
|
const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput;
|
||||||
|
this.totalThroughputUsed += dbThroughput;
|
||||||
|
}
|
||||||
|
|
||||||
|
(database.collections() || []).forEach(async (collection) => {
|
||||||
|
if (collection.offer()) {
|
||||||
|
const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput;
|
||||||
|
this.totalThroughputUsed += colThroughput;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||||
|
this.totalThroughputUsed *= numberOfRegions;
|
||||||
|
};
|
||||||
|
|
||||||
public getAnalyticalStorageTtl = (): number => {
|
public getAnalyticalStorageTtl = (): number => {
|
||||||
if (this.isAnalyticalStorageEnabled) {
|
if (this.isAnalyticalStorageEnabled) {
|
||||||
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
|
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
|
||||||
@@ -643,10 +676,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return buttons;
|
return buttons;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
|
private onMaxAutoPilotThroughputChange = (newThroughput: number): void => {
|
||||||
this.setState({ autoPilotThroughput: newThroughput });
|
let throughputError = "";
|
||||||
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||||
|
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
|
||||||
|
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
|
||||||
|
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
|
||||||
|
this.totalThroughputUsed + throughputDelta
|
||||||
|
} RU/s. Change total throughput limit in cost management.`;
|
||||||
|
}
|
||||||
|
this.setState({ autoPilotThroughput: newThroughput, throughputError });
|
||||||
|
};
|
||||||
|
|
||||||
private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput });
|
private onThroughputChange = (newThroughput: number): void => {
|
||||||
|
let throughputError = "";
|
||||||
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||||
|
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
|
||||||
|
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
|
||||||
|
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
|
||||||
|
this.totalThroughputUsed + throughputDelta
|
||||||
|
} RU/s. Change total throughput limit in cost management.`;
|
||||||
|
}
|
||||||
|
this.setState({ throughput: newThroughput, throughputError });
|
||||||
|
};
|
||||||
|
|
||||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||||
@@ -893,6 +947,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||||
initialNotification: this.props.settingsTab.pendingNotification(),
|
initialNotification: this.props.settingsTab.pendingNotification(),
|
||||||
|
throughputError: this.state.throughputError,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.isCollectionSettingsTab) {
|
if (!this.isCollectionSettingsTab) {
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
|
||||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
|
||||||
import { Urls, StyleConstants } from "../../../Common/Constants";
|
|
||||||
import {
|
import {
|
||||||
getPriceCurrency,
|
DetailsList,
|
||||||
getCurrencySign,
|
DetailsListLayoutMode,
|
||||||
getAutoscalePricePerRu,
|
DetailsRow,
|
||||||
getMultimasterMultiplier,
|
|
||||||
computeRUUsagePriceHourly,
|
|
||||||
getPricePerRu,
|
|
||||||
estimatedCostDisclaimer,
|
|
||||||
} from "../../../Utils/PricingUtils";
|
|
||||||
import {
|
|
||||||
ITextFieldStyles,
|
|
||||||
ICheckboxStyles,
|
ICheckboxStyles,
|
||||||
IStackProps,
|
|
||||||
IStackTokens,
|
|
||||||
IChoiceGroupStyles,
|
IChoiceGroupStyles,
|
||||||
Link,
|
IColumn,
|
||||||
Text,
|
IDetailsColumnStyles,
|
||||||
IMessageBarStyles,
|
|
||||||
ITextStyles,
|
|
||||||
IDetailsRowStyles,
|
|
||||||
IStackStyles,
|
|
||||||
IDetailsListStyles,
|
IDetailsListStyles,
|
||||||
|
IDetailsRowProps,
|
||||||
|
IDetailsRowStyles,
|
||||||
IDropdownStyles,
|
IDropdownStyles,
|
||||||
|
IMessageBarStyles,
|
||||||
ISeparatorStyles,
|
ISeparatorStyles,
|
||||||
|
IStackProps,
|
||||||
|
IStackStyles,
|
||||||
|
IStackTokens,
|
||||||
|
ITextFieldStyles,
|
||||||
|
ITextStyles,
|
||||||
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType,
|
MessageBarType,
|
||||||
Stack,
|
SelectionMode,
|
||||||
Spinner,
|
Spinner,
|
||||||
SpinnerSize,
|
SpinnerSize,
|
||||||
DetailsList,
|
Stack,
|
||||||
IColumn,
|
Text,
|
||||||
SelectionMode,
|
|
||||||
DetailsListLayoutMode,
|
|
||||||
IDetailsRowProps,
|
|
||||||
DetailsRow,
|
|
||||||
IDetailsColumnStyles,
|
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
import * as React from "react";
|
||||||
|
import { StyleConstants, Urls } from "../../../Common/Constants";
|
||||||
|
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||||
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
|
import {
|
||||||
|
computeRUUsagePriceHourly,
|
||||||
|
estimatedCostDisclaimer,
|
||||||
|
getAutoscalePricePerRu,
|
||||||
|
getCurrencySign,
|
||||||
|
getMultimasterMultiplier,
|
||||||
|
getPriceCurrency,
|
||||||
|
getPricePerRu,
|
||||||
|
} from "../../../Utils/PricingUtils";
|
||||||
|
import { isDirty, isDirtyTypes } from "./SettingsUtils";
|
||||||
|
|
||||||
export interface EstimatedSpendingDisplayProps {
|
export interface EstimatedSpendingDisplayProps {
|
||||||
costType: JSX.Element;
|
costType: JSX.Element;
|
||||||
@@ -65,7 +65,7 @@ export interface PriceBreakdown {
|
|||||||
currencySign: string;
|
currencySign: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
label: {
|
label: {
|
||||||
@@ -141,6 +141,19 @@ export const transparentDetailsHeaderStyle: Partial<IDetailsColumnStyles> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const transparentDetailsCostHeaderStyle: Partial<IDetailsColumnStyles> = {
|
||||||
|
root: {
|
||||||
|
selectors: {
|
||||||
|
":hover": {
|
||||||
|
background: "transparent",
|
||||||
|
},
|
||||||
|
".ms-DetailsHeader-cellTitle": {
|
||||||
|
visibility: "hidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const customDetailsListStyles: Partial<IDetailsListStyles> = {
|
export const customDetailsListStyles: Partial<IDetailsListStyles> = {
|
||||||
root: {
|
root: {
|
||||||
selectors: {
|
selectors: {
|
||||||
@@ -223,14 +236,15 @@ export const getRuPriceBreakdown = (
|
|||||||
multimasterEnabled: isMultimaster,
|
multimasterEnabled: isMultimaster,
|
||||||
isAutoscale: isAutoscale,
|
isAutoscale: isAutoscale,
|
||||||
});
|
});
|
||||||
const basePricePerRu: number = isAutoscale
|
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
|
||||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
const pricePerRu: number = isAutoscale
|
||||||
: getPricePerRu(serverId);
|
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
|
||||||
|
: getPricePerRu(serverId, multimasterMultiplier);
|
||||||
return {
|
return {
|
||||||
hourlyPrice: hourlyPrice,
|
hourlyPrice,
|
||||||
dailyPrice: hourlyPrice * 24,
|
dailyPrice: hourlyPrice * 24,
|
||||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
pricePerRu,
|
||||||
currency: getPriceCurrency(serverId),
|
currency: getPriceCurrency(serverId),
|
||||||
currencySign: getCurrencySign(serverId),
|
currencySign: getCurrencySign(serverId),
|
||||||
};
|
};
|
||||||
@@ -271,7 +285,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
|||||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
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.{" "}
|
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>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface ScaleComponentProps {
|
|||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
initialNotification: DataModels.Notification;
|
initialNotification: DataModels.Notification;
|
||||||
|
throughputError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||||
@@ -189,6 +190,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||||
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
||||||
|
throughputError={this.props.throughputError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
PriceBreakdown,
|
PriceBreakdown,
|
||||||
saveThroughputWarningMessage,
|
saveThroughputWarningMessage,
|
||||||
titleAndInputStackProps,
|
titleAndInputStackProps,
|
||||||
|
transparentDetailsCostHeaderStyle,
|
||||||
transparentDetailsHeaderStyle,
|
transparentDetailsHeaderStyle,
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||||
@@ -75,6 +76,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
getThroughputWarningMessage: () => JSX.Element;
|
getThroughputWarningMessage: () => JSX.Element;
|
||||||
usageSizeInKB: number;
|
usageSizeInKB: number;
|
||||||
|
throughputError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -218,12 +220,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
const estimatedSpendingColumns: IColumn[] = [
|
const estimatedSpendingColumns: IColumn[] = [
|
||||||
{
|
{
|
||||||
key: "costType",
|
key: "costType",
|
||||||
name: "",
|
name: "Cost Type",
|
||||||
fieldName: "costType",
|
fieldName: "costType",
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
maxWidth: 200,
|
maxWidth: 200,
|
||||||
isResizable: true,
|
isResizable: true,
|
||||||
styles: transparentDetailsHeaderStyle,
|
styles: transparentDetailsCostHeaderStyle,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "minPerMonth",
|
key: "minPerMonth",
|
||||||
@@ -540,6 +542,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||||
onChange={this.onAutoPilotThroughputChange}
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
min={minAutoPilotThroughput}
|
min={minAutoPilotThroughput}
|
||||||
|
errorMessage={this.props.throughputError}
|
||||||
/>
|
/>
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||||
{this.minRUperGBSurvey()}
|
{this.minRUperGBSurvey()}
|
||||||
@@ -579,6 +582,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
min={this.props.minimum}
|
min={this.props.minimum}
|
||||||
|
errorMessage={this.props.throughputError}
|
||||||
/>
|
/>
|
||||||
{this.state.exceedFreeTierThroughput && (
|
{this.state.exceedFreeTierThroughput && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"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.
|
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"
|
href="https://aka.ms/cosmos-autoscale-migration"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
</StyledMessageBar>
|
</StyledMessageBar>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
@@ -186,6 +189,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -460,6 +464,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -412,6 +413,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -952,6 +954,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1228,6 +1231,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -101,6 +108,13 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"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.
|
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"
|
href="https://aka.ms/cosmos-autoscale-migration"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -195,6 +197,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -207,6 +210,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -219,6 +223,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -230,6 +235,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -249,6 +255,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -264,6 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -278,6 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -291,6 +300,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -302,6 +312,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -321,6 +332,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -363,6 +375,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -378,6 +391,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -394,6 +408,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
@import "../../../../less/Common/Constants";
|
||||||
|
|
||||||
.tabComponentContainer {
|
.tabComponentContainer {
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.flex-display();
|
.flex-display();
|
||||||
.flex-direction();
|
.flex-direction();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext";
|
|||||||
import {
|
import {
|
||||||
calculateEstimateNumber,
|
calculateEstimateNumber,
|
||||||
computeRUUsagePriceHourly,
|
computeRUUsagePriceHourly,
|
||||||
|
estimatedCostDisclaimer,
|
||||||
getAutoscalePricePerRu,
|
getAutoscalePricePerRu,
|
||||||
getCurrencySign,
|
getCurrencySign,
|
||||||
getMultimasterMultiplier,
|
getMultimasterMultiplier,
|
||||||
@@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
|
|||||||
const currency: string = getPriceCurrency(serverId);
|
const currency: string = getPriceCurrency(serverId);
|
||||||
const currencySign: string = getCurrencySign(serverId);
|
const currencySign: string = getCurrencySign(serverId);
|
||||||
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||||
const pricePerRu = isAutoscale
|
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||||
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
|
|
||||||
: getPricePerRu(serverId) * multiplier;
|
|
||||||
|
|
||||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
|
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||||
|
|
||||||
if (isAutoscale) {
|
if (isAutoscale) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const props = {
|
|||||||
isSharded: true,
|
isSharded: true,
|
||||||
setThroughputValue: () => jest.fn(),
|
setThroughputValue: () => jest.fn(),
|
||||||
setIsAutoscale: () => jest.fn(),
|
setIsAutoscale: () => jest.fn(),
|
||||||
|
setIsThroughputCapExceeded: () => jest.fn(),
|
||||||
onCostAcknowledgeChange: () => jest.fn(),
|
onCostAcknowledgeChange: () => jest.fn(),
|
||||||
};
|
};
|
||||||
describe("ThroughputInput Pane", () => {
|
describe("ThroughputInput Pane", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
@@ -16,6 +17,7 @@ export interface ThroughputInputProps {
|
|||||||
showFreeTierExceedThroughputTooltip: boolean;
|
showFreeTierExceedThroughputTooltip: boolean;
|
||||||
setThroughputValue: (throughput: number) => void;
|
setThroughputValue: (throughput: number) => void;
|
||||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||||
|
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
|
||||||
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
|
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
showFreeTierExceedThroughputTooltip,
|
showFreeTierExceedThroughputTooltip,
|
||||||
setThroughputValue,
|
setThroughputValue,
|
||||||
setIsAutoscale,
|
setIsAutoscale,
|
||||||
|
setIsThroughputCapExceeded,
|
||||||
isSharded,
|
isSharded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
@@ -31,10 +34,60 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
|
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
|
||||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||||
const [throughputError, setThroughputError] = useState<string>("");
|
const [throughputError, setThroughputError] = useState<string>("");
|
||||||
|
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||||
|
|
||||||
setIsAutoscale(isAutoscaleSelected);
|
setIsAutoscale(isAutoscaleSelected);
|
||||||
setThroughputValue(throughput);
|
setThroughputValue(throughput);
|
||||||
|
|
||||||
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// throughput cap check for the initial state
|
||||||
|
let totalThroughput = 0;
|
||||||
|
(useDatabases.getState().databases || []).forEach((database) => {
|
||||||
|
if (database.offer()) {
|
||||||
|
const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput;
|
||||||
|
totalThroughput += dbThroughput;
|
||||||
|
}
|
||||||
|
|
||||||
|
(database.collections() || []).forEach((collection) => {
|
||||||
|
if (collection.offer()) {
|
||||||
|
const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput;
|
||||||
|
totalThroughput += colThroughput;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
totalThroughput *= numberOfRegions;
|
||||||
|
setTotalThroughputUsed(totalThroughput);
|
||||||
|
|
||||||
|
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughput < throughput) {
|
||||||
|
setThroughputError(
|
||||||
|
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
|
||||||
|
totalThroughput + throughput * numberOfRegions
|
||||||
|
} RU/s. Change total throughput limit in cost management.`
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsThroughputCapExceeded(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkThroughputCap = (newThroughput: number): boolean => {
|
||||||
|
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughputUsed < newThroughput) {
|
||||||
|
setThroughputError(
|
||||||
|
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
|
||||||
|
totalThroughputUsed + newThroughput * numberOfRegions
|
||||||
|
} RU/s. Change total throughput limit in cost management.`
|
||||||
|
);
|
||||||
|
setIsThroughputCapExceeded(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThroughputError("");
|
||||||
|
setIsThroughputCapExceeded(false);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const getThroughputLabelText = (): string => {
|
const getThroughputLabelText = (): string => {
|
||||||
let throughputHeaderText: string;
|
let throughputHeaderText: string;
|
||||||
if (isAutoscaleSelected) {
|
if (isAutoscaleSelected) {
|
||||||
@@ -60,11 +113,17 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
const newThroughput = parseInt(newInput);
|
const newThroughput = parseInt(newInput);
|
||||||
setThroughput(newThroughput);
|
setThroughput(newThroughput);
|
||||||
setThroughputValue(newThroughput);
|
setThroughputValue(newThroughput);
|
||||||
|
|
||||||
if (!isSharded && newThroughput > 10000) {
|
if (!isSharded && newThroughput > 10000) {
|
||||||
setThroughputError("Unsharded collections support up to 10,000 RUs");
|
setThroughputError("Unsharded collections support up to 10,000 RUs");
|
||||||
} else {
|
return;
|
||||||
setThroughputError("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!checkThroughputCap(newThroughput)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThroughputError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAutoScaleTooltip = (): string => {
|
const getAutoScaleTooltip = (): string => {
|
||||||
@@ -96,11 +155,13 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsAutoScaleSelected(true);
|
setIsAutoScaleSelected(true);
|
||||||
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||||
setIsAutoscale(true);
|
setIsAutoscale(true);
|
||||||
|
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
|
||||||
} else {
|
} else {
|
||||||
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||||
setIsAutoScaleSelected(false);
|
setIsAutoScaleSelected(false);
|
||||||
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||||
setIsAutoscale(false);
|
setIsAutoscale(false);
|
||||||
|
checkThroughputCap(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,6 +179,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<input
|
<input
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
aria-label="Autoscale mode"
|
aria-label="Autoscale mode"
|
||||||
|
aria-required={true}
|
||||||
checked={isAutoscaleSelected}
|
checked={isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
role="radio"
|
role="radio"
|
||||||
@@ -131,6 +193,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
aria-label="Manual mode"
|
aria-label="Manual mode"
|
||||||
checked={!isAutoscaleSelected}
|
checked={!isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
|
aria-required={true}
|
||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
isSharded={true}
|
isSharded={true}
|
||||||
onCostAcknowledgeChange={[Function]}
|
onCostAcknowledgeChange={[Function]}
|
||||||
setIsAutoscale={[Function]}
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
setThroughputValue={[Function]}
|
setThroughputValue={[Function]}
|
||||||
showFreeTierExceedThroughputTooltip={true}
|
showFreeTierExceedThroughputTooltip={true}
|
||||||
>
|
>
|
||||||
@@ -345,12 +346,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<IconBase
|
<IconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -630,6 +633,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
className="panelInfoIcon root-57"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
|
||||||
</i>
|
</i>
|
||||||
@@ -651,6 +655,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-label="Autoscale mode"
|
aria-label="Autoscale mode"
|
||||||
|
aria-required={true}
|
||||||
checked={true}
|
checked={true}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
key=".0:$.0"
|
key=".0:$.0"
|
||||||
@@ -667,6 +672,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
aria-label="Manual mode"
|
aria-label="Manual mode"
|
||||||
|
aria-required={true}
|
||||||
checked={false}
|
checked={false}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
key=".0:$.2"
|
key=".0:$.2"
|
||||||
@@ -1327,12 +1333,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<IconBase
|
<IconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -1612,6 +1620,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
className="panelInfoIcon root-57"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
|
||||||
</i>
|
</i>
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||||
<IconButton
|
<IconButton
|
||||||
name="More"
|
name="More"
|
||||||
|
title="More"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
ariaLabel={menuItemLabel}
|
ariaLabel={menuItemLabel}
|
||||||
menuIconProps={{
|
menuIconProps={{
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
title="More"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,6 +424,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
title="More"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,39 @@
|
|||||||
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
|
import shallow from "zustand/shallow";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
|
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
|
|
||||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
import {
|
||||||
|
ContainerConnectionInfo,
|
||||||
|
IPhoenixConnectionInfoResult,
|
||||||
|
IProvisionData,
|
||||||
|
IResponse,
|
||||||
|
} from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import { IGalleryItem } from "../Juno/JunoClient";
|
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import {
|
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||||
get as getWorkspace,
|
|
||||||
listByDatabaseAccount,
|
|
||||||
listConnectionInfo,
|
|
||||||
start,
|
|
||||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||||
@@ -41,13 +47,12 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
|||||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
import type NotebookManager from "./Notebook/NotebookManager";
|
import type NotebookManager from "./Notebook/NotebookManager";
|
||||||
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||||
@@ -87,12 +92,13 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||||
|
private phoenixClient: PhoenixClient;
|
||||||
constructor() {
|
constructor() {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
this._isInitializingNotebooks = false;
|
this._isInitializingNotebooks = false;
|
||||||
|
this.phoenixClient = new PhoenixClient();
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
() => this.refreshCommandBarButtons(),
|
() => this.refreshCommandBarButtons(),
|
||||||
(state) => state.isNotebooksEnabledForAccount
|
(state) => state.isNotebooksEnabledForAccount
|
||||||
@@ -160,11 +166,37 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
async () => {
|
async () => this.initiateAndRefreshNotebookList(),
|
||||||
|
(state) => [state.isNotebookEnabled, state.isRefreshed],
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
|
this.resourceTree = new ResourceTreeAdapter(this);
|
||||||
|
|
||||||
|
// Override notebook server parameters from URL parameters
|
||||||
|
if (
|
||||||
|
userContext.features.notebookServerUrl &&
|
||||||
|
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||||
|
userContext.features.notebookServerToken
|
||||||
|
) {
|
||||||
|
useNotebook.getState().setNotebookServerInfo({
|
||||||
|
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
||||||
|
authToken: userContext.features.notebookServerToken,
|
||||||
|
forwardingId: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.features.notebookBasePath) {
|
||||||
|
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshExplorer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initiateAndRefreshNotebookList(): Promise<void> {
|
||||||
if (!this.notebookManager) {
|
if (!this.notebookManager) {
|
||||||
const NotebookManager = await (
|
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
|
||||||
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
|
.default;
|
||||||
).default;
|
|
||||||
this.notebookManager = new NotebookManager();
|
this.notebookManager = new NotebookManager();
|
||||||
this.notebookManager.initialize({
|
this.notebookManager.initialize({
|
||||||
container: this,
|
container: this,
|
||||||
@@ -176,38 +208,6 @@ export default class Explorer {
|
|||||||
|
|
||||||
this.refreshCommandBarButtons();
|
this.refreshCommandBarButtons();
|
||||||
this.refreshNotebookList();
|
this.refreshNotebookList();
|
||||||
},
|
|
||||||
(state) => state.isNotebookEnabled
|
|
||||||
);
|
|
||||||
|
|
||||||
this.resourceTree = new ResourceTreeAdapter(this);
|
|
||||||
|
|
||||||
// Override notebook server parameters from URL parameters
|
|
||||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
|
||||||
useNotebook.getState().setNotebookServerInfo({
|
|
||||||
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
|
||||||
authToken: userContext.features.notebookServerToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContext.features.notebookBasePath) {
|
|
||||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContext.features.livyEndpoint) {
|
|
||||||
useNotebook.getState().setSparkClusterConnectionInfo({
|
|
||||||
userName: undefined,
|
|
||||||
password: undefined,
|
|
||||||
endpoints: [
|
|
||||||
{
|
|
||||||
endpoint: userContext.features.livyEndpoint,
|
|
||||||
kind: DataModels.SparkClusterEndpointKind.Livy,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshExplorer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public openEnableSynapseLinkDialog(): void {
|
public openEnableSynapseLinkDialog(): void {
|
||||||
@@ -343,28 +343,85 @@ export default class Explorer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._isInitializingNotebooks = true;
|
this._isInitializingNotebooks = true;
|
||||||
|
|
||||||
await this.ensureNotebookWorkspaceRunning();
|
|
||||||
const connectionInfo = await listConnectionInfo(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
databaseAccount.name,
|
|
||||||
"default"
|
|
||||||
);
|
|
||||||
|
|
||||||
useNotebook.getState().setNotebookServerInfo({
|
|
||||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
|
||||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
|
||||||
|
|
||||||
this.refreshNotebookList();
|
this.refreshNotebookList();
|
||||||
|
|
||||||
this._isInitializingNotebooks = false;
|
this._isInitializingNotebooks = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetNotebookWorkspace() {
|
public async allocateContainer(): Promise<void> {
|
||||||
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
const isAllocating = useNotebook.getState().isAllocating;
|
||||||
|
if (
|
||||||
|
isAllocating === false &&
|
||||||
|
(notebookServerInfo === undefined ||
|
||||||
|
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
|
||||||
|
) {
|
||||||
|
const provisionData: IProvisionData = {
|
||||||
|
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||||
|
};
|
||||||
|
const connectionStatus: ContainerConnectionInfo = {
|
||||||
|
status: ConnectionStatusType.Connecting,
|
||||||
|
};
|
||||||
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
try {
|
||||||
|
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
useNotebook.getState().setIsAllocating(true);
|
||||||
|
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||||
|
if (connectionInfo.status !== HttpStatusCodes.OK) {
|
||||||
|
throw new Error(`Received status code: ${connectionInfo?.status}`);
|
||||||
|
}
|
||||||
|
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||||
|
throw new Error(`NotebookServerUrl is invalid!`);
|
||||||
|
}
|
||||||
|
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||||
|
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
});
|
||||||
|
connectionStatus.status = ConnectionStatusType.Failed;
|
||||||
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
useNotebook.getState().setIsAllocating(false);
|
||||||
|
this.refreshCommandBarButtons();
|
||||||
|
this.refreshNotebookList();
|
||||||
|
this._isInitializingNotebooks = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setNotebookInfo(
|
||||||
|
connectionInfo: IResponse<IPhoenixConnectionInfoResult>,
|
||||||
|
connectionStatus: DataModels.ContainerConnectionInfo
|
||||||
|
) {
|
||||||
|
const containerData = {
|
||||||
|
forwardingId: connectionInfo.data.forwardingId,
|
||||||
|
dbAccountName: userContext.databaseAccount.name,
|
||||||
|
};
|
||||||
|
await this.phoenixClient.initiateContainerHeartBeat(containerData);
|
||||||
|
|
||||||
|
connectionStatus.status = ConnectionStatusType.Connected;
|
||||||
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
useNotebook.getState().setNotebookServerInfo({
|
||||||
|
notebookServerEndpoint:
|
||||||
|
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||||
|
userContext.features.notebookServerUrl) ||
|
||||||
|
connectionInfo.data.notebookServerUrl,
|
||||||
|
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||||
|
forwardingId: connectionInfo.data.forwardingId,
|
||||||
|
});
|
||||||
|
this.notebookManager?.notebookClient
|
||||||
|
.getMemoryUsage()
|
||||||
|
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetNotebookWorkspace(): void {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||||
handleError(
|
handleError(
|
||||||
"Attempt to reset notebook workspace, but notebook is not enabled",
|
"Attempt to reset notebook workspace, but notebook is not enabled",
|
||||||
@@ -372,11 +429,14 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dialogContent = useNotebook.getState().isPhoenixNotebooks
|
||||||
|
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
|
||||||
|
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
|
||||||
|
|
||||||
const resetConfirmationDialogProps: DialogProps = {
|
const resetConfirmationDialogProps: DialogProps = {
|
||||||
isModal: true,
|
isModal: true,
|
||||||
title: "Reset Workspace",
|
title: "Reset Workspace",
|
||||||
subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?",
|
subText: dialogContent,
|
||||||
primaryButtonText: "OK",
|
primaryButtonText: "OK",
|
||||||
secondaryButtonText: "Cancel",
|
secondaryButtonText: "Cancel",
|
||||||
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
||||||
@@ -389,7 +449,6 @@ export default class Explorer {
|
|||||||
if (!databaseAccount) {
|
if (!databaseAccount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { value: workspaces } = await listByDatabaseAccount(
|
const { value: workspaces } = await listByDatabaseAccount(
|
||||||
userContext.subscriptionId,
|
userContext.subscriptionId,
|
||||||
@@ -403,48 +462,57 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureNotebookWorkspaceRunning() {
|
|
||||||
if (!userContext.databaseAccount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clearMessage;
|
|
||||||
try {
|
|
||||||
const notebookWorkspace = await getWorkspace(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
"default"
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
notebookWorkspace &&
|
|
||||||
notebookWorkspace.properties &&
|
|
||||||
notebookWorkspace.properties.status &&
|
|
||||||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
|
||||||
) {
|
|
||||||
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
|
||||||
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
|
|
||||||
} finally {
|
|
||||||
clearMessage && clearMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _resetNotebookWorkspace = async () => {
|
private _resetNotebookWorkspace = async () => {
|
||||||
useDialog.getState().closeDialog();
|
useDialog.getState().closeDialog();
|
||||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||||
|
let connectionStatus: ContainerConnectionInfo;
|
||||||
try {
|
try {
|
||||||
await this.notebookManager?.notebookClient.resetWorkspace();
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||||
|
const error = "No server endpoint detected";
|
||||||
|
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
|
||||||
|
logConsoleError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
useTabs.getState().closeAllNotebookTabs(true);
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Connecting,
|
||||||
|
};
|
||||||
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
}
|
||||||
|
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
|
||||||
|
if (connectionInfo?.status !== HttpStatusCodes.OK) {
|
||||||
|
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
|
||||||
|
}
|
||||||
|
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||||
|
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
|
||||||
|
}
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
logConsoleInfo("Successfully reset notebook workspace");
|
logConsoleInfo("Successfully reset notebook workspace");
|
||||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
||||||
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
|
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
errorStack: getErrorStack(error),
|
errorStack: getErrorStack(error),
|
||||||
});
|
});
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Failed,
|
||||||
|
};
|
||||||
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearInProgressMessage();
|
clearInProgressMessage();
|
||||||
@@ -636,6 +704,9 @@ export default class Explorer {
|
|||||||
if (!notebookContentItem || !notebookContentItem.path) {
|
if (!notebookContentItem || !notebookContentItem.path) {
|
||||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||||
}
|
}
|
||||||
|
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
await this.allocateContainer();
|
||||||
|
}
|
||||||
|
|
||||||
const notebookTabs = useTabs
|
const notebookTabs = useTabs
|
||||||
.getState()
|
.getState()
|
||||||
@@ -851,15 +922,54 @@ export default class Explorer {
|
|||||||
/**
|
/**
|
||||||
* This creates a new notebook file, then opens the notebook
|
* This creates a new notebook file, then opens the notebook
|
||||||
*/
|
*/
|
||||||
public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void {
|
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||||
const error = "Attempt to create new notebook, but notebook is not enabled";
|
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||||
handleError(error, "Explorer/onNewNotebookClicked");
|
handleError(error, "Explorer/onNewNotebookClicked");
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
if (isGithubTree) {
|
||||||
|
await this.allocateContainer();
|
||||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
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 clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
|
||||||
dataExplorerArea: Constants.Areas.Notebook,
|
dataExplorerArea: Constants.Areas.Notebook,
|
||||||
@@ -906,7 +1016,26 @@ export default class Explorer {
|
|||||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||||
|
if (useNotebook.getState().isPhoenixFeatures) {
|
||||||
|
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 to temporary workspace. This could happen because of network issues. Please refresh the page and try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.connectToNotebookTerminal(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||||
let title: string;
|
let title: string;
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
@@ -928,7 +1057,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
const terminalTabs: TerminalTab[] = useTabs
|
const terminalTabs: TerminalTab[] = useTabs
|
||||||
.getState()
|
.getState()
|
||||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
|
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
|
||||||
|
|
||||||
let index = 1;
|
let index = 1;
|
||||||
if (terminalTabs.length > 0) {
|
if (terminalTabs.length > 0) {
|
||||||
@@ -957,7 +1086,7 @@ export default class Explorer {
|
|||||||
notebookUrl?: string,
|
notebookUrl?: string,
|
||||||
galleryItem?: IGalleryItem,
|
galleryItem?: IGalleryItem,
|
||||||
isFavorite?: boolean
|
isFavorite?: boolean
|
||||||
) {
|
): Promise<void> {
|
||||||
const title = "Gallery";
|
const title = "Gallery";
|
||||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||||
const galleryTab = useTabs
|
const galleryTab = useTabs
|
||||||
@@ -1000,7 +1129,10 @@ export default class Explorer {
|
|||||||
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
|
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await useDatabases.getState().loadDatabaseOffers();
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
throughputCap && throughputCap !== -1
|
||||||
|
? await useDatabases.getState().loadAllOffers()
|
||||||
|
: await useDatabases.getState().loadDatabaseOffers();
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
|
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
|
||||||
@@ -1016,18 +1148,12 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openSetupNotebooksPaneForQuickstart(): void {
|
|
||||||
const title = "Enable Notebooks (Preview)";
|
|
||||||
const description =
|
|
||||||
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleOpenFileAction(path: string): Promise<void> {
|
public async handleOpenFileAction(path: string): Promise<void> {
|
||||||
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
|
||||||
this._openSetupNotebooksPaneForQuickstart();
|
await useNotebook.getState().getPhoenixStatus();
|
||||||
|
}
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
await this.allocateContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
||||||
@@ -1058,7 +1184,27 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
useDialog.getState().showOkCancelModalDialog(
|
||||||
|
Notebook.newNotebookUploadModalTitle,
|
||||||
|
undefined,
|
||||||
|
"Upload",
|
||||||
|
async () => {
|
||||||
|
await this.allocateContainer();
|
||||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
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
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
@@ -1067,30 +1213,44 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
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> {
|
public async refreshExplorer(): Promise<void> {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
const isNotebookEnabled: boolean =
|
|
||||||
userContext.authType !== AuthType.ResourceToken &&
|
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
|
||||||
userContext.features.enableNotebooks);
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||||
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
|
useNotebook
|
||||||
|
.getState()
|
||||||
|
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
|
||||||
|
|
||||||
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
||||||
isNotebookEnabled,
|
isNotebookEnabled,
|
||||||
dataExplorerArea: Constants.Areas.Notebook,
|
dataExplorerArea: Constants.Areas.Notebook,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userContext.features.notebooksTemporarilyDown) {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
if (isNotebookEnabled) {
|
|
||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
} else if (this.notebookToImport) {
|
|
||||||
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
|
|
||||||
this._openSetupNotebooksPaneForQuickstart();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -294,8 +296,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
this.setGremlinParams();
|
this.setGremlinParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedNode = this.state.highlightedNode;
|
|
||||||
|
|
||||||
props.onGraphAccessorCreated({
|
props.onGraphAccessorCreated({
|
||||||
applyFilter: this.submitQuery.bind(this),
|
applyFilter: this.submitQuery.bind(this),
|
||||||
addVertex: this.addVertex.bind(this),
|
addVertex: this.addVertex.bind(this),
|
||||||
@@ -303,7 +303,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
});
|
});
|
||||||
} // constructor
|
} // constructor
|
||||||
|
|
||||||
public shareIGraphConfig(igraphConfig: IGraphConfig) {
|
public shareIGraphConfig(igraphConfig: IGraphConfig): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
igraphConfig: { ...igraphConfig },
|
igraphConfig: { ...igraphConfig },
|
||||||
});
|
});
|
||||||
@@ -330,10 +330,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
|
const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
|
||||||
|
|
||||||
// aggregate all the properties, remove dropped ones
|
// aggregate all the properties, remove dropped ones
|
||||||
let finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
|
const finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
|
||||||
|
|
||||||
// Compose the query
|
// Compose the query
|
||||||
let pkId = editedProperties.pkId;
|
const pkId = editedProperties.pkId;
|
||||||
let updateQueryFragment = "";
|
let updateQueryFragment = "";
|
||||||
|
|
||||||
finalProperties.forEach((p) => {
|
finalProperties.forEach((p) => {
|
||||||
@@ -422,7 +422,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* Called from ko binding
|
* Called from ko binding
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
public selectNode(id: string) {
|
public selectNode(id: string): void {
|
||||||
if (!this.d3ForceGraph) {
|
if (!this.d3ForceGraph) {
|
||||||
console.warn("Attempting to select node, but d3ForceGraph not initialized, yet.");
|
console.warn("Attempting to select node, but d3ForceGraph not initialized, yet.");
|
||||||
return;
|
return;
|
||||||
@@ -431,7 +431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
this.d3ForceGraph.selectNode(id);
|
this.d3ForceGraph.selectNode(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteHighlightedNode() {
|
public deleteHighlightedNode(): void {
|
||||||
if (!this.state.highlightedNode) {
|
if (!this.state.highlightedNode) {
|
||||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, "No highlighted node to remove.");
|
GraphExplorer.reportToConsole(ConsoleDataType.Error, "No highlighted node to remove.");
|
||||||
return;
|
return;
|
||||||
@@ -467,23 +467,23 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* Is of type: {e: GremlinEdge, v: GremlinVertex}[]
|
* Is of type: {e: GremlinEdge, v: GremlinVertex}[]
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
public static isEdgeVertexPairArray(data: any) {
|
public static isEdgeVertexPairArray(data: any): boolean {
|
||||||
if (!(data instanceof Array)) {
|
if (!(data instanceof Array)) {
|
||||||
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result not an array", data);
|
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result not an array", data);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pairs: any[] = data;
|
const pairs: any[] = data;
|
||||||
for (let i = 0; i < pairs.length; i++) {
|
for (let i = 0; i < pairs.length; i++) {
|
||||||
const item = pairs[i];
|
const item = pairs[i];
|
||||||
if (
|
if (
|
||||||
!item.hasOwnProperty("e") ||
|
!Object.prototype.hasOwnProperty.call(item, "e") ||
|
||||||
!item.hasOwnProperty("v") ||
|
!Object.prototype.hasOwnProperty.call(item, "v") ||
|
||||||
!item["e"].hasOwnProperty("id") ||
|
!Object.prototype.hasOwnProperty.call(item["e"], "id") ||
|
||||||
!item["e"].hasOwnProperty("type") ||
|
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
|
||||||
item["e"].type !== "edge" ||
|
item["e"].type !== "edge" ||
|
||||||
!item["v"].hasOwnProperty("id") ||
|
!Object.prototype.hasOwnProperty.call(item["v"], "id") ||
|
||||||
!item["v"].hasOwnProperty("type") ||
|
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
|
||||||
item["v"].type !== "vertex"
|
item["v"].type !== "vertex"
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@@ -514,7 +514,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
// Try hitting cache first
|
// Try hitting cache first
|
||||||
const cache = outE ? this.outECache : this.inECache;
|
const cache = outE ? this.outECache : this.inECache;
|
||||||
const pairs = cache.retrieve(vertex.id, startIndex, pageSize);
|
const pairs = cache.retrieve(vertex.id, startIndex, pageSize);
|
||||||
if (pairs != null && pairs.length === pageSize) {
|
if (pairs !== null && pairs.length === pageSize) {
|
||||||
const msg = `Retrieved ${pairs.length} ${outE ? "outE" : "inE"} edges from cache for vertex id: ${vertex.id}`;
|
const msg = `Retrieved ${pairs.length} ${outE ? "outE" : "inE"} edges from cache for vertex id: ${vertex.id}`;
|
||||||
GraphExplorer.reportToConsole(ConsoleDataType.Info, msg);
|
GraphExplorer.reportToConsole(ConsoleDataType.Info, msg);
|
||||||
return Q.resolve(pairs);
|
return Q.resolve(pairs);
|
||||||
@@ -588,7 +588,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
vertex._outEAllLoaded &&
|
vertex._outEAllLoaded &&
|
||||||
vertex._inEAllLoaded
|
vertex._inEAllLoaded
|
||||||
) {
|
) {
|
||||||
console.info("No more edges to load for vertex " + vertex.id);
|
|
||||||
updateGraphData();
|
updateGraphData();
|
||||||
return Q.resolve(graphData);
|
return Q.resolve(graphData);
|
||||||
}
|
}
|
||||||
@@ -668,7 +667,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return promise.then((nbPairsFetched: number) => {
|
return promise.then(() => {
|
||||||
if (offsetIndex >= GraphExplorer.LOAD_PAGE_SIZE || !vertex._outEAllLoaded || !vertex._inEAllLoaded) {
|
if (offsetIndex >= GraphExplorer.LOAD_PAGE_SIZE || !vertex._outEAllLoaded || !vertex._inEAllLoaded) {
|
||||||
vertex._pagination = {
|
vertex._pagination = {
|
||||||
total:
|
total:
|
||||||
@@ -754,7 +753,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* Create a new edge in docdb and update graph
|
* Create a new edge in docdb and update graph
|
||||||
* @param e
|
* @param e
|
||||||
*/
|
*/
|
||||||
public createNewEdge(e: GraphNewEdgeData): Q.Promise<any> {
|
public createNewEdge(e: GraphNewEdgeData): Q.Promise<unknown> {
|
||||||
const q = `g.V('${GraphUtil.escapeSingleQuotes(e.inputOutV)}').addE('${GraphUtil.escapeSingleQuotes(
|
const q = `g.V('${GraphUtil.escapeSingleQuotes(e.inputOutV)}').addE('${GraphUtil.escapeSingleQuotes(
|
||||||
e.label
|
e.label
|
||||||
)}').To(g.V('${GraphUtil.escapeSingleQuotes(e.inputInV)}'))`;
|
)}').To(g.V('${GraphUtil.escapeSingleQuotes(e.inputInV)}'))`;
|
||||||
@@ -772,8 +771,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let edge = edges[0];
|
const edge = edges[0];
|
||||||
let graphData = this.originalGraphData;
|
const graphData = this.originalGraphData;
|
||||||
graphData.addEdge(edge);
|
graphData.addEdge(edge);
|
||||||
|
|
||||||
// Allow loadNeighbors to load list new edge
|
// Allow loadNeighbors to load list new edge
|
||||||
@@ -800,10 +799,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* Manually update in-memory graph.
|
* Manually update in-memory graph.
|
||||||
* @param edgeId
|
* @param edgeId
|
||||||
*/
|
*/
|
||||||
public removeEdge(edgeId: string): Q.Promise<any> {
|
public removeEdge(edgeId: string): Q.Promise<unknown> {
|
||||||
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
|
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
|
||||||
() => {
|
() => {
|
||||||
let graphData = this.originalGraphData;
|
const graphData = this.originalGraphData;
|
||||||
graphData.removeEdge(edgeId, false);
|
graphData.removeEdge(edgeId, false);
|
||||||
this.updateGraphData(graphData, this.state.igraphConfig);
|
this.updateGraphData(graphData, this.state.igraphConfig);
|
||||||
},
|
},
|
||||||
@@ -826,10 +825,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let vertices: any[] = data;
|
const vertices: any[] = data;
|
||||||
if (vertices.length > 0) {
|
if (vertices.length > 0) {
|
||||||
let v0 = vertices[0];
|
const v0 = vertices[0];
|
||||||
if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") {
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(v0, "id") ||
|
||||||
|
!Object.prototype.hasOwnProperty.call(v0, "type") ||
|
||||||
|
v0.type !== "vertex"
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -837,7 +840,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
public processGremlinQueryResults(result: GremlinClient.GremlinRequestResult): void {
|
public processGremlinQueryResults(result: GremlinClient.GremlinRequestResult): void {
|
||||||
const data = result.data as any;
|
const data = result.data as GraphData.GremlinVertex[];
|
||||||
this.setFilterQueryStatus(FilterQueryStatus.GraphEmptyResult);
|
this.setFilterQueryStatus(FilterQueryStatus.GraphEmptyResult);
|
||||||
|
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
@@ -927,13 +930,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
throw { title: err };
|
throw { title: err };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vertices == null || vertices.length < 1) {
|
if (vertices === null || vertices.length < 1) {
|
||||||
const err = "Failed to create vertex (no vertex in response)";
|
const err = "Failed to create vertex (no vertex in response)";
|
||||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, err, vertices);
|
GraphExplorer.reportToConsole(ConsoleDataType.Error, err, vertices);
|
||||||
throw { title: err };
|
throw { title: err };
|
||||||
}
|
}
|
||||||
|
|
||||||
let vertex = vertices[0];
|
const vertex = vertices[0];
|
||||||
const graphData = this.originalGraphData;
|
const graphData = this.originalGraphData;
|
||||||
graphData.addVertex(vertex);
|
graphData.addVertex(vertex);
|
||||||
this.updateGraphData(graphData, this.state.igraphConfig);
|
this.updateGraphData(graphData, this.state.igraphConfig);
|
||||||
@@ -1022,7 +1025,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
this.gremlinClient.destroy();
|
this.gremlinClient.destroy();
|
||||||
}
|
}
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) {
|
if (this.props.onLoadStartKey !== null && this.props.onLoadStartKey !== undefined) {
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.Tab,
|
Action.Tab,
|
||||||
{
|
{
|
||||||
@@ -1082,7 +1085,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
|
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
|
||||||
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
|
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
|
||||||
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
|
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
|
||||||
let errorDataStr: string = "";
|
let errorDataStr = "";
|
||||||
if (errorData && errorData.length > 0) {
|
if (errorData && errorData.length > 0) {
|
||||||
console.error(msg, errorData);
|
console.error(msg, errorData);
|
||||||
errorDataStr = ": " + JSON.stringify(errorData);
|
errorDataStr = ": " + JSON.stringify(errorData);
|
||||||
@@ -1161,12 +1164,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
)}"`
|
)}"`
|
||||||
).then(
|
).then(
|
||||||
(documents: DataModels.DocumentId[]) => {
|
(documents: DataModels.DocumentId[]) => {
|
||||||
$.each(documents, (index: number, doc: any) => {
|
$.each(
|
||||||
|
documents,
|
||||||
|
(index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => {
|
||||||
newIconsMap[doc["_graph_icon_property_value"]] = {
|
newIconsMap[doc["_graph_icon_property_value"]] = {
|
||||||
data: doc["icon"],
|
data: doc["icon"],
|
||||||
format: doc["format"],
|
format: doc["format"],
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Update graph configuration
|
// Update graph configuration
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -1223,8 +1229,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
const key = this.state.igraphConfig.nodeCaption;
|
const key = this.state.igraphConfig.nodeCaption;
|
||||||
return $.map(
|
return $.map(
|
||||||
this.state.rootMap,
|
this.state.rootMap,
|
||||||
(value: any, index: number): LeftPane.CaptionId => {
|
(value: any): LeftPane.CaptionId => {
|
||||||
let result = GraphData.GraphData.getNodePropValue(value, key);
|
const result = GraphData.GraphData.getNodePropValue(value, key);
|
||||||
return {
|
return {
|
||||||
caption: result !== undefined ? result : value.id,
|
caption: result !== undefined ? result : value.id,
|
||||||
id: value.id,
|
id: value.id,
|
||||||
@@ -1237,7 +1243,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* Selecting a root node means
|
* Selecting a root node means
|
||||||
* @param node
|
* @param node
|
||||||
*/
|
*/
|
||||||
private selectRootNode(id: string): Q.Promise<any> {
|
private selectRootNode(id: string): Q.Promise<unknown> {
|
||||||
if (!this.d3ForceGraph) {
|
if (!this.d3ForceGraph) {
|
||||||
console.warn("Attempting to reset zoom, but d3ForceGraph not initialized, yet.");
|
console.warn("Attempting to reset zoom, but d3ForceGraph not initialized, yet.");
|
||||||
} else {
|
} else {
|
||||||
@@ -1282,7 +1288,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
this.collectNodeProperties(this.originalGraphData.vertices);
|
this.collectNodeProperties(this.originalGraphData.vertices);
|
||||||
this.updatePropertiesPane(id);
|
this.updatePropertiesPane(id);
|
||||||
},
|
},
|
||||||
(reason: any) => {
|
(reason: string) => {
|
||||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to select root node. Reason:${reason}`);
|
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to select root node. Reason:${reason}`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1349,10 +1355,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
private getPkIdFromVertex(v: GraphData.GremlinVertex): string {
|
private getPkIdFromVertex(v: GraphData.GremlinVertex): string {
|
||||||
if (
|
if (
|
||||||
this.props.collectionPartitionKeyProperty &&
|
this.props.collectionPartitionKeyProperty &&
|
||||||
v.hasOwnProperty("properties") &&
|
Object.prototype.hasOwnProperty.call(v, "properties") &&
|
||||||
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty) &&
|
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty) &&
|
||||||
v.properties[this.props.collectionPartitionKeyProperty].length > 0 &&
|
v.properties[this.props.collectionPartitionKeyProperty].length > 0 &&
|
||||||
v.properties[this.props.collectionPartitionKeyProperty][0].hasOwnProperty("value")
|
Object.prototype.hasOwnProperty.call(v.properties[this.props.collectionPartitionKeyProperty][0], "value")
|
||||||
) {
|
) {
|
||||||
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
|
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
|
||||||
return GraphExplorer.generatePkIdPair(pk, v.id);
|
return GraphExplorer.generatePkIdPair(pk, v.id);
|
||||||
@@ -1370,8 +1376,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
private getPkIdFromNodeData(v: GraphHighlightedNodeData): string {
|
private getPkIdFromNodeData(v: GraphHighlightedNodeData): string {
|
||||||
if (
|
if (
|
||||||
this.props.collectionPartitionKeyProperty &&
|
this.props.collectionPartitionKeyProperty &&
|
||||||
v.hasOwnProperty("properties") &&
|
Object.prototype.hasOwnProperty.call(v, "properties") &&
|
||||||
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty)
|
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty)
|
||||||
) {
|
) {
|
||||||
const pk = v.properties[this.props.collectionPartitionKeyProperty];
|
const pk = v.properties[this.props.collectionPartitionKeyProperty];
|
||||||
return GraphExplorer.generatePkIdPair(pk[0] as PartitionKeyValueType, v.id);
|
return GraphExplorer.generatePkIdPair(pk[0] as PartitionKeyValueType, v.id);
|
||||||
@@ -1388,14 +1394,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* @return id
|
* @return id
|
||||||
*/
|
*/
|
||||||
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
|
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
|
||||||
let { id } = d;
|
const { id } = d;
|
||||||
if (typeof id !== "string") {
|
if (typeof id !== "string") {
|
||||||
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
|
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
|
||||||
logConsoleError(error);
|
logConsoleError(error);
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
|
if (collectionPartitionKeyProperty && Object.prototype.hasOwnProperty.call(d, collectionPartitionKeyProperty)) {
|
||||||
let pk = (d as any)[collectionPartitionKeyProperty];
|
let pk = (d as any)[collectionPartitionKeyProperty];
|
||||||
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
|
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
|
||||||
if (Array.isArray(pk) && pk.length > 0) {
|
if (Array.isArray(pk) && pk.length > 0) {
|
||||||
@@ -1425,7 +1431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
|
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
|
||||||
return this.executeNonPagedDocDbQuery(q).then(
|
return this.executeNonPagedDocDbQuery(q).then(
|
||||||
(documents: DataModels.DocumentId[]) => {
|
(documents: DataModels.DocumentId[]) => {
|
||||||
let possibleVertices = [] as PossibleVertex[];
|
const possibleVertices = [] as PossibleVertex[];
|
||||||
$.each(documents, (index: number, item: any) => {
|
$.each(documents, (index: number, item: any) => {
|
||||||
if (highlightedNodeId && item.id === highlightedNodeId) {
|
if (highlightedNodeId && item.id === highlightedNodeId) {
|
||||||
// Exclude highlighed node in the list
|
// Exclude highlighed node in the list
|
||||||
@@ -1439,7 +1445,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
caption: item.p,
|
caption: item.p,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (item.hasOwnProperty("p")) {
|
if (Object.prototype.hasOwnProperty.call(item, "p")) {
|
||||||
possibleVertices.push({
|
possibleVertices.push({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
caption: item.p[0]["_value"],
|
caption: item.p[0]["_value"],
|
||||||
@@ -1462,17 +1468,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
* @param addedEdges
|
* @param addedEdges
|
||||||
* @return promise when done
|
* @return promise when done
|
||||||
*/
|
*/
|
||||||
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> {
|
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<unknown> {
|
||||||
let promises = [];
|
const promises = [];
|
||||||
// Drop edges
|
// Drop edges
|
||||||
for (let i = 0; i < editedEdges.droppedIds.length; i++) {
|
for (let i = 0; i < editedEdges.droppedIds.length; i++) {
|
||||||
let id = editedEdges.droppedIds[i];
|
const id = editedEdges.droppedIds[i];
|
||||||
promises.push(this.removeEdge(id));
|
promises.push(this.removeEdge(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add edges
|
// Add edges
|
||||||
for (let i = 0; i < editedEdges.addedEdges.length; i++) {
|
for (let i = 0; i < editedEdges.addedEdges.length; i++) {
|
||||||
let e = editedEdges.addedEdges[i];
|
const e = editedEdges.addedEdges[i];
|
||||||
promises.push(
|
promises.push(
|
||||||
this.createNewEdge(e).then(() => {
|
this.createNewEdge(e).then(() => {
|
||||||
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph
|
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph
|
||||||
@@ -1525,7 +1531,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
/**
|
/**
|
||||||
* For unit testing purposes
|
* For unit testing purposes
|
||||||
*/
|
*/
|
||||||
public onGraphUpdated(timestamp: number): void {}
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public onGraphUpdated(_timestamp: number): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get node properties for styling purposes. Result is the union of all properties of all nodes.
|
* Get node properties for styling purposes. Result is the union of all properties of all nodes.
|
||||||
@@ -1533,17 +1541,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
|
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
|
||||||
const props = {} as any; // Hashset
|
const props = {} as any; // Hashset
|
||||||
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
|
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
|
||||||
for (var p in item) {
|
for (const p in item) {
|
||||||
// DocDB: Exclude type because it's always 'vertex'
|
// DocDB: Exclude type because it's always 'vertex'
|
||||||
if (p !== "type" && typeof (item as any)[p] === "string") {
|
if (p !== "type" && typeof (item as any)[p] === "string") {
|
||||||
props[p] = true;
|
props[p] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Inspect properties
|
// Inspect properties
|
||||||
if (item.hasOwnProperty("properties")) {
|
if (Object.prototype.hasOwnProperty.call(item, "properties")) {
|
||||||
// TODO This is DocDB-graph specific
|
// TODO This is DocDB-graph specific
|
||||||
// Assume each property value is [{value:... }]
|
// Assume each property value is [{value:... }]
|
||||||
for (var f in item.properties) {
|
for (const f in item.properties) {
|
||||||
props[f] = true;
|
props[f] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1570,21 +1578,21 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = this.originalGraphData.getVertexById(id);
|
const data = this.originalGraphData.getVertexById(id);
|
||||||
|
|
||||||
// A bit of translation to make it easier to display
|
// A bit of translation to make it easier to display
|
||||||
let props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
|
const props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
|
||||||
for (let p in data.properties) {
|
for (const p in data.properties) {
|
||||||
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
|
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update neighbors
|
// update neighbors
|
||||||
let sources: NeighborVertexBasicInfo[] = [];
|
const sources: NeighborVertexBasicInfo[] = [];
|
||||||
let targets: NeighborVertexBasicInfo[] = [];
|
const targets: NeighborVertexBasicInfo[] = [];
|
||||||
this.props.onResetDefaultGraphConfigValues();
|
this.props.onResetDefaultGraphConfigValues();
|
||||||
let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
|
const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
|
||||||
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
|
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
|
||||||
let sData: GraphHighlightedNodeData = {
|
const sData: GraphHighlightedNodeData = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
properties: props,
|
properties: props,
|
||||||
@@ -1611,16 +1619,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
targets: NeighborVertexBasicInfo[]
|
targets: NeighborVertexBasicInfo[]
|
||||||
): void {
|
): void {
|
||||||
// update neighbors
|
// update neighbors
|
||||||
let gd = this.originalGraphData;
|
const gd = this.originalGraphData;
|
||||||
let v = gd.getVertexById(id);
|
const v = gd.getVertexById(id);
|
||||||
|
|
||||||
// Clear the array while keeping the references
|
// Clear the array while keeping the references
|
||||||
sources.length = 0;
|
sources.length = 0;
|
||||||
targets.length = 0;
|
targets.length = 0;
|
||||||
|
|
||||||
let possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
|
const possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
|
||||||
|
|
||||||
for (let p in v.inE) {
|
for (const p in v.inE) {
|
||||||
possibleEdgeLabels[p] = true;
|
possibleEdgeLabels[p] = true;
|
||||||
const edges = v.inE[p];
|
const edges = v.inE[p];
|
||||||
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
|
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
|
||||||
@@ -1629,7 +1637,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
|
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||||
sources.push({
|
sources.push({
|
||||||
name: caption,
|
name: caption,
|
||||||
id: neighborId,
|
id: neighborId,
|
||||||
@@ -1639,7 +1647,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let p in v.outE) {
|
for (const p in v.outE) {
|
||||||
possibleEdgeLabels[p] = true;
|
possibleEdgeLabels[p] = true;
|
||||||
const edges = v.outE[p];
|
const edges = v.outE[p];
|
||||||
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
|
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
|
||||||
@@ -1648,7 +1656,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
|
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||||
targets.push({
|
targets.push({
|
||||||
name: caption,
|
name: caption,
|
||||||
id: neighborId,
|
id: neighborId,
|
||||||
@@ -1660,7 +1668,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
possibleEdgeLabels: Object.keys(possibleEdgeLabels).map(
|
possibleEdgeLabels: Object.keys(possibleEdgeLabels).map(
|
||||||
(value: string, index: number, array: string[]): InputTypeaheadComponent.Item => {
|
(value: string): InputTypeaheadComponent.Item => {
|
||||||
return { caption: value, value: value };
|
return { caption: value, value: value };
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -1681,20 +1689,20 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedVertex = vertices[0];
|
const updatedVertex = vertices[0];
|
||||||
if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
|
if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
|
||||||
let currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
|
const currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
|
||||||
// Copy updated properties
|
// Copy updated properties
|
||||||
if (currentVertex.hasOwnProperty("properties")) {
|
if (Object.prototype.hasOwnProperty.call(currentVertex, "properties")) {
|
||||||
delete currentVertex["properties"];
|
delete currentVertex["properties"];
|
||||||
}
|
}
|
||||||
for (var p in updatedVertex) {
|
for (const p in updatedVertex) {
|
||||||
(currentVertex as any)[p] = updatedVertex[p];
|
(currentVertex as any)[p] = updatedVertex[p];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO This kind of assumes saveVertexProperty is done from property panes.
|
// TODO This kind of assumes saveVertexProperty is done from property panes.
|
||||||
let hn = this.state.highlightedNode;
|
const hn = this.state.highlightedNode;
|
||||||
if (hn && hn.id === updatedVertex.id) {
|
if (hn && hn.id === updatedVertex.id) {
|
||||||
this.updatePropertiesPane(hn.id);
|
this.updatePropertiesPane(hn.id);
|
||||||
}
|
}
|
||||||
@@ -1708,7 +1716,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
igraphConfig?: IGraphConfig
|
igraphConfig?: IGraphConfig
|
||||||
) {
|
) {
|
||||||
this.originalGraphData = graphData;
|
this.originalGraphData = graphData;
|
||||||
let gd = JSON.parse(JSON.stringify(this.originalGraphData));
|
const gd = JSON.parse(JSON.stringify(this.originalGraphData));
|
||||||
if (!this.d3ForceGraph) {
|
if (!this.d3ForceGraph) {
|
||||||
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
|
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
|
||||||
return;
|
return;
|
||||||
@@ -1873,7 +1881,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
|
|
||||||
promise
|
promise
|
||||||
.then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result))
|
.then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result))
|
||||||
.catch((error: any) => {
|
.catch((error: Error) => {
|
||||||
const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`;
|
const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`;
|
||||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> {
|
|||||||
className={className}
|
className={className}
|
||||||
as="tr"
|
as="tr"
|
||||||
aria-label={node.caption}
|
aria-label={node.caption}
|
||||||
onActivated={(e) => this.props.onRootNodeSelected(node.id)}
|
onActivated={() => this.props.onRootNodeSelected(node.id)}
|
||||||
key={node.id}
|
key={node.id}
|
||||||
>
|
>
|
||||||
<td className="resultItem">
|
<td className="resultItem">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
|
||||||
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
|
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
|
||||||
import ExpandIcon from "../../../../images/Expand_14x14.svg";
|
import ExpandIcon from "../../../../images/Expand_14x14.svg";
|
||||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||||
|
|
||||||
interface MiddlePaneComponentProps {
|
interface MiddlePaneComponentProps {
|
||||||
isTabsContentExpanded: boolean;
|
isTabsContentExpanded: boolean;
|
||||||
@@ -17,7 +17,14 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
|
|||||||
<div className="middlePane">
|
<div className="middlePane">
|
||||||
<div className="graphTitle">
|
<div className="graphTitle">
|
||||||
<span className="paneTitle">Graph</span>
|
<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
|
<img
|
||||||
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
||||||
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
|
||||||
import { mount, ReactWrapper } from "enzyme";
|
import { mount, ReactWrapper } from "enzyme";
|
||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import { NodePropertiesComponent, NodePropertiesComponentProps, Mode } from "./NodePropertiesComponent";
|
import React from "react";
|
||||||
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
|
import { GraphHighlightedNodeData, PossibleVertex } from "./GraphExplorer";
|
||||||
|
import { Mode, NodePropertiesComponent, NodePropertiesComponentProps } from "./NodePropertiesComponent";
|
||||||
|
|
||||||
describe("Property pane", () => {
|
describe("Property pane", () => {
|
||||||
const title = "My Title";
|
const title = "My Title";
|
||||||
@@ -37,17 +37,18 @@ describe("Property pane", () => {
|
|||||||
return {
|
return {
|
||||||
expandedTitle: title,
|
expandedTitle: title,
|
||||||
isCollapsed: false,
|
isCollapsed: false,
|
||||||
onCollapsedChanged: (newValue: boolean): void => {},
|
onCollapsedChanged: jest.fn(),
|
||||||
node: highlightedNode,
|
node: highlightedNode,
|
||||||
getPkIdFromNodeData: (v: GraphHighlightedNodeData): string => null,
|
getPkIdFromNodeData: (): string => undefined,
|
||||||
collectionPartitionKeyProperty: null,
|
collectionPartitionKeyProperty: undefined,
|
||||||
updateVertexProperties: (editedProperties: EditedProperties): Q.Promise<void> => Q.resolve(),
|
updateVertexProperties: (): Q.Promise<void> => Q.resolve(),
|
||||||
selectNode: (id: string): void => {},
|
selectNode: jest.fn(),
|
||||||
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(null),
|
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(undefined),
|
||||||
possibleEdgeLabels: null,
|
possibleEdgeLabels: undefined,
|
||||||
editGraphEdges: (editedEdges: EditedEdges): Q.Promise<any> => Q.resolve(),
|
//eslint-disable-next-line
|
||||||
deleteHighlightedNode: (): void => {},
|
editGraphEdges: (): Q.Promise<any> => Q.resolve(),
|
||||||
onModeChanged: (newMode: Mode): void => {},
|
deleteHighlightedNode: jest.fn(),
|
||||||
|
onModeChanged: jest.fn(),
|
||||||
viewMode: Mode.READONLY_PROP,
|
viewMode: Mode.READONLY_PROP,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class NodePropertiesComponent extends React.Component<
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
editedProperties: {
|
editedProperties: {
|
||||||
pkId: null,
|
pkId: undefined,
|
||||||
readOnlyProperties: [],
|
readOnlyProperties: [],
|
||||||
existingProperties: [],
|
existingProperties: [],
|
||||||
addedProperties: [],
|
addedProperties: [],
|
||||||
@@ -98,15 +98,12 @@ export class NodePropertiesComponent extends React.Component<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getDerivedStateFromProps(
|
public static getDerivedStateFromProps(props: NodePropertiesComponentProps): Partial<NodePropertiesComponentState> {
|
||||||
props: NodePropertiesComponentProps,
|
|
||||||
state: NodePropertiesComponentState
|
|
||||||
): Partial<NodePropertiesComponentState> {
|
|
||||||
if (props.viewMode !== Mode.READONLY_PROP) {
|
if (props.viewMode !== Mode.READONLY_PROP) {
|
||||||
return { isDeleteConfirm: false };
|
return { isDeleteConfirm: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
@@ -138,10 +135,10 @@ export class NodePropertiesComponent extends React.Component<
|
|||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString {
|
private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString {
|
||||||
if (value == null) {
|
if (value === undefined) {
|
||||||
return "null";
|
return "null";
|
||||||
}
|
}
|
||||||
let type = typeof value;
|
const type = typeof value;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "number":
|
case "number":
|
||||||
case "boolean":
|
case "boolean":
|
||||||
@@ -172,10 +169,9 @@ export class NodePropertiesComponent extends React.Component<
|
|||||||
];
|
];
|
||||||
|
|
||||||
const existingProps: ViewModels.InputProperty[] = [];
|
const existingProps: ViewModels.InputProperty[] = [];
|
||||||
|
|
||||||
if (this.props.node.hasOwnProperty("properties")) {
|
if (this.props.node.hasOwnProperty("properties")) {
|
||||||
const hProps = this.props.node["properties"];
|
const hProps = this.props.node["properties"];
|
||||||
for (let p in hProps) {
|
for (const p in hProps) {
|
||||||
const propValues = hProps[p];
|
const propValues = hProps[p];
|
||||||
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({
|
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({
|
||||||
key: p,
|
key: p,
|
||||||
@@ -437,7 +433,7 @@ export class NodePropertiesComponent extends React.Component<
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
|
|
||||||
.graphExplorerContainer {
|
.graphExplorerContainer {
|
||||||
margin-left: @SmallSpace;
|
margin-left: @SmallSpace;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
height:100%;
|
overflow-x: clip;
|
||||||
|
height: 100%;
|
||||||
.flex-display();
|
.flex-display();
|
||||||
.flex-direction();
|
.flex-direction();
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
.loadGraphHelper {
|
.loadGraphHelper {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height:50%;
|
height: 50%;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: @LoadGraphHelperWidth;
|
width: @LoadGraphHelperWidth;
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
margin: @DefaultSpace;
|
margin: @DefaultSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadGraphBtn{
|
.loadGraphBtn {
|
||||||
margin-top: @LargeSpace;
|
margin-top: @LargeSpace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,17 +95,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Override bootstrap's nav pills */
|
/* Override bootstrap's nav pills */
|
||||||
.nav-pills>li.active>a,
|
.nav-pills > li.active > a,
|
||||||
.nav-pills>li.active>a:focus,
|
.nav-pills > li.active > a:focus,
|
||||||
.nav-pills>li.active>a:hover {
|
.nav-pills > li.active > a:hover {
|
||||||
background-color: @AccentMediumHigh;
|
background-color: @AccentMediumHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-pills>li>a {
|
.nav-pills > li > a {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav>li>a {
|
.nav > li > a {
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,16 +115,16 @@
|
|||||||
|
|
||||||
.typeahead__cancel-button {
|
.typeahead__cancel-button {
|
||||||
top: 5px !important;
|
top: 5px !important;
|
||||||
right: .4em !important;
|
right: 0.4em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.queryMetricsSummary {
|
.queryMetricsSummary {
|
||||||
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
|
margin: @LargeSpace @LargeSpace 0px @DefaultSpace;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
display: block;
|
display: block;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
min-height: 100px;
|
||||||
|
|
||||||
.queryMetricsSummaryHead {
|
.queryMetricsSummaryHead {
|
||||||
.flex-display();
|
.flex-display();
|
||||||
@@ -144,7 +145,8 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.flex-display();
|
.flex-display();
|
||||||
th, td {
|
th,
|
||||||
|
td {
|
||||||
padding: @DefaultSpace;
|
padding: @DefaultSpace;
|
||||||
|
|
||||||
&:nth-child(1) {
|
&:nth-child(1) {
|
||||||
@@ -168,7 +170,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.graphContainer {
|
.graphContainer {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -176,7 +177,6 @@
|
|||||||
padding-top: (2 * @MediumSpace);
|
padding-top: (2 * @MediumSpace);
|
||||||
.flex-display();
|
.flex-display();
|
||||||
|
|
||||||
|
|
||||||
.leftPane {
|
.leftPane {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
padding: 0px 0px 0px @DefaultSpace;
|
padding: 0px 0px 0px @DefaultSpace;
|
||||||
@@ -224,8 +224,8 @@
|
|||||||
.graphTitle {
|
.graphTitle {
|
||||||
margin-top: @DefaultSpace;
|
margin-top: @DefaultSpace;
|
||||||
|
|
||||||
.graphExpandCollapseBtn{
|
.graphExpandCollapseBtn {
|
||||||
padding:2px 6px 5px 6px;
|
padding: 2px 6px 5px 6px;
|
||||||
margin-right: @SmallSpace;
|
margin-right: @SmallSpace;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
.active();
|
.active();
|
||||||
}
|
}
|
||||||
|
|
||||||
img{
|
img {
|
||||||
.dataExplorerIcons();
|
.dataExplorerIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.graphModal {
|
.graphModal {
|
||||||
background-color: rgba(255, 255, 255, .7);
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
@@ -282,7 +282,6 @@
|
|||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
|
||||||
|
|
||||||
.rightPaneHeader {
|
.rightPaneHeader {
|
||||||
/* TODO: Hack to align the trashbox with the header for now. */
|
/* TODO: Hack to align the trashbox with the header for now. */
|
||||||
margin-top: -28px;
|
margin-top: -28px;
|
||||||
@@ -406,16 +405,16 @@
|
|||||||
|
|
||||||
.propertyTableHeader {
|
.propertyTableHeader {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: @DefaultFontColor ;
|
color: @DefaultFontColor;
|
||||||
border-bottom: 1px solid @BaseMediumHigh;
|
border-bottom: 1px solid @BaseMediumHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override autocomplete stuff */
|
/* Override autocomplete stuff */
|
||||||
.typeahead__container {
|
.typeahead__container {
|
||||||
font: @mediumFontSize 'Segoe UI' !important;
|
font: @mediumFontSize "Segoe UI" !important;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
font: @mediumFontSize 'Segoe UI' !important;
|
font: @mediumFontSize "Segoe UI" !important;
|
||||||
padding: 0px @SmallSpace !important;
|
padding: 0px @SmallSpace !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,12 +425,12 @@
|
|||||||
|
|
||||||
.typeahead__cancel-button {
|
.typeahead__cancel-button {
|
||||||
top: 5px !important;
|
top: 5px !important;
|
||||||
right: .4em !important;
|
right: 0.4em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightPaneAddPropertyBtn {
|
.rightPaneAddPropertyBtn {
|
||||||
padding: @DefaultSpace;
|
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 {
|
img {
|
||||||
margin: 0px @SmallSpace @SmallSpace 0px;
|
margin: 0px @SmallSpace @SmallSpace 0px;
|
||||||
@@ -450,6 +449,8 @@
|
|||||||
.flex-direction();
|
.flex-direction();
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paneTitle {
|
.paneTitle {
|
||||||
@@ -586,7 +587,7 @@
|
|||||||
|
|
||||||
.loadmore {
|
.loadmore {
|
||||||
use.loadMoreIcon {
|
use.loadMoreIcon {
|
||||||
fill:@GraphNodeDefaultColor;
|
fill: @GraphNodeDefaultColor;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
fill: @AccentMediumHigh;
|
fill: @AccentMediumHigh;
|
||||||
@@ -608,8 +609,6 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* scroll for leftpane, rightpane and newvertex pane*/
|
/* scroll for leftpane, rightpane and newvertex pane*/
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
* and update any knockout observables passed from the parent.
|
* and update any knockout observables passed from the parent.
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { StyleConstants } from "../../../Common/Constants";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import { useTabs } from "../../../hooks/useTabs";
|
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -54,8 +53,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
if (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) {
|
||||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -31,28 +31,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Account is not serverless - button should be visible", () => {
|
it("Button should be visible", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||||
);
|
);
|
||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Account is serverless - button should be hidden", () => {
|
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
properties: {
|
|
||||||
capabilities: [{ name: "EnableServerless" }],
|
|
||||||
},
|
|
||||||
} as DatabaseAccount,
|
|
||||||
});
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
|
||||||
);
|
|
||||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Enable notebook button", () => {
|
describe("Enable notebook button", () => {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
|||||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||||
import GitHubIcon from "../../../../images/github.svg";
|
import GitHubIcon from "../../../../images/github.svg";
|
||||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||||
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
|
|
||||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||||
@@ -25,7 +24,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
|||||||
import { JunoClient } from "../../../Juno/JunoClient";
|
import { JunoClient } from "../../../Juno/JunoClient";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
|
||||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
@@ -36,7 +34,6 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
|
|||||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||||
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { SelectedNodeState } from "../../useSelectedNode";
|
import { SelectedNodeState } from "../../useSelectedNode";
|
||||||
|
|
||||||
@@ -78,10 +75,12 @@ export function createStaticCommandBarButtons(
|
|||||||
if (container.notebookManager?.gitHubOAuthService) {
|
if (container.notebookManager?.gitHubOAuthService) {
|
||||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||||
}
|
}
|
||||||
|
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
||||||
notebookButtons.push(createOpenTerminalButton(container));
|
notebookButtons.push(createOpenTerminalButton(container));
|
||||||
|
}
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
||||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(userContext.apiType === "Mongo" &&
|
(userContext.apiType === "Mongo" &&
|
||||||
useNotebook.getState().isShellEnabled &&
|
useNotebook.getState().isShellEnabled &&
|
||||||
@@ -97,22 +96,19 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
notebookButtons.forEach((btn) => {
|
notebookButtons.forEach((btn) => {
|
||||||
if (userContext.features.notebooksTemporarilyDown) {
|
|
||||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||||
|
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
|
||||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
|
||||||
} else {
|
|
||||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
|
||||||
}
|
}
|
||||||
|
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||||
|
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||||
|
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||||
|
}
|
||||||
|
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||||
}
|
}
|
||||||
buttons.push(btn);
|
buttons.push(btn);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
|
|
||||||
buttons.push(createDivider());
|
|
||||||
buttons.push(createEnableNotebooksButton(container));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||||
@@ -167,9 +163,7 @@ export function createContextCommandBarButtons(
|
|||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
if (!userContext.features.notebooksTemporarilyDown) {
|
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||||
}
|
}
|
||||||
@@ -177,13 +171,6 @@ export function createContextCommandBarButtons(
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
tooltipText:
|
|
||||||
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
|
|
||||||
? Constants.Notebook.mongoShellTemporarilyDownMsg
|
|
||||||
: undefined,
|
|
||||||
disabled:
|
|
||||||
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
|
|
||||||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
|
|
||||||
};
|
};
|
||||||
buttons.push(newMongoShellBtn);
|
buttons.push(newMongoShellBtn);
|
||||||
}
|
}
|
||||||
@@ -279,10 +266,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isServerlessAccount()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
|
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -309,8 +292,13 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () =>
|
onCommandClick: async () => {
|
||||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
if (throughputCap && throughputCap !== -1) {
|
||||||
|
await useDatabases.getState().loadAllOffers();
|
||||||
|
}
|
||||||
|
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
|
||||||
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
@@ -471,33 +459,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
|
||||||
if (configContext.platform === Platform.Emulator) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const label = "Enable Notebooks (Preview)";
|
|
||||||
const tooltip =
|
|
||||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
|
||||||
const description =
|
|
||||||
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
|
||||||
return {
|
|
||||||
iconSrc: EnableNotebooksIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () =>
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
label,
|
|
||||||
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
|
||||||
),
|
|
||||||
commandButtonLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
|
||||||
ariaLabel: label,
|
|
||||||
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "Open Terminal";
|
const label = "Open Terminal";
|
||||||
return {
|
return {
|
||||||
@@ -515,9 +476,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
|||||||
const label = "Open Mongo Shell";
|
const label = "Open Mongo Shell";
|
||||||
const tooltip =
|
const tooltip =
|
||||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||||
const title = "Set up workspace";
|
|
||||||
const description =
|
|
||||||
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
|
|
||||||
const disableButton =
|
const disableButton =
|
||||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||||
return {
|
return {
|
||||||
@@ -526,13 +484,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
|||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
} else {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
title,
|
|
||||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
@@ -547,9 +498,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
|||||||
const label = "Open Cassandra Shell";
|
const label = "Open Cassandra Shell";
|
||||||
const tooltip =
|
const tooltip =
|
||||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||||
const title = "Set up workspace";
|
|
||||||
const description =
|
|
||||||
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
|
|
||||||
const disableButton =
|
const disableButton =
|
||||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||||
return {
|
return {
|
||||||
@@ -558,13 +506,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
|||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||||
} else {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
title,
|
|
||||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
@@ -595,7 +536,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
|||||||
return {
|
return {
|
||||||
iconSrc: GitHubIcon,
|
iconSrc: GitHubIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () =>
|
onCommandClick: () => {
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
@@ -605,7 +546,8 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
|||||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||||
junoClientProp={junoClient}
|
junoClientProp={junoClient}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { StyleConstants } from "../../../Common/Constants";
|
|||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
import { ConnectionStatus } from "./ConnectionStatusComponent";
|
||||||
import { MemoryTracker } from "./MemoryTrackerComponent";
|
import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,3 +203,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
|||||||
onRender: () => <MemoryTracker />,
|
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
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
Normal file
186
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import {
|
||||||
|
FocusTrapCallout,
|
||||||
|
FocusZone,
|
||||||
|
FocusZoneTabbableElements,
|
||||||
|
FontWeights,
|
||||||
|
Icon,
|
||||||
|
mergeStyleSets,
|
||||||
|
ProgressIndicator,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TooltipHost,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { useId } from "@fluentui/react-hooks";
|
||||||
|
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
|
||||||
|
import * as React from "react";
|
||||||
|
import "../../../../less/hostedexplorer.less";
|
||||||
|
import { ConnectionStatusType, ContainerStatusType, 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 connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
|
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.");
|
||||||
|
const [isBarDismissed, setIsBarDismissed] = React.useState<boolean>(false);
|
||||||
|
const buttonId = useId("callout-button");
|
||||||
|
const containerInfo = useNotebook((state) => state.containerStatus);
|
||||||
|
|
||||||
|
const styles = mergeStyleSets({
|
||||||
|
callout: {
|
||||||
|
width: 320,
|
||||||
|
padding: "20px 24px",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginBottom: 12,
|
||||||
|
fontWeight: FontWeights.semilight,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (connectionInfo?.status === ConnectionStatusType.Reconnect) {
|
||||||
|
setToolTipContent("Click here to Reconnect to temporary workspace.");
|
||||||
|
} else if (connectionInfo?.status === ConnectionStatusType.Failed) {
|
||||||
|
setStatusColor("status failed is-animating");
|
||||||
|
setToolTipContent("Click here to Reconnect to temporary workspace.");
|
||||||
|
}
|
||||||
|
}, [connectionInfo.status]);
|
||||||
|
|
||||||
|
const stopTimer = () => {
|
||||||
|
setIsActive(false);
|
||||||
|
setCounter(0);
|
||||||
|
setSecond("00");
|
||||||
|
setMinute("00");
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
stopTimer();
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<TooltipHost
|
||||||
|
content={
|
||||||
|
containerInfo?.status === ContainerStatusType.Active
|
||||||
|
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
|
||||||
|
containerInfo.durationLeftInMinutes
|
||||||
|
)} minutes.`
|
||||||
|
: toolTipContent
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ActionButton
|
||||||
|
id={buttonId}
|
||||||
|
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
|
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||||
|
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||||
|
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{!isBarDismissed &&
|
||||||
|
containerInfo.status &&
|
||||||
|
containerInfo.status === ContainerStatusType.Active &&
|
||||||
|
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
|
||||||
|
<FocusTrapCallout
|
||||||
|
role="alertdialog"
|
||||||
|
className={styles.callout}
|
||||||
|
gapSpace={0}
|
||||||
|
target={`#${buttonId}`}
|
||||||
|
onDismiss={() => setIsBarDismissed(true)}
|
||||||
|
setInitialFocus
|
||||||
|
>
|
||||||
|
<Text block variant="xLarge" className={styles.title}>
|
||||||
|
Remaining Time
|
||||||
|
</Text>
|
||||||
|
<Text block variant="small">
|
||||||
|
This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "}
|
||||||
|
minutes. To save your work permanently, save your notebooks to a GitHub repository or download the
|
||||||
|
notebooks to your local machine before the session ends.
|
||||||
|
</Text>
|
||||||
|
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
|
||||||
|
<Stack className={styles.buttons} gap={8} horizontal>
|
||||||
|
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</FocusZone>
|
||||||
|
</FocusTrapCallout>
|
||||||
|
) : undefined}
|
||||||
|
</ActionButton>
|
||||||
|
</TooltipHost>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -129,7 +129,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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}
|
aria-expanded={!this.props.isConsoleExpanded}
|
||||||
>
|
>
|
||||||
<img
|
<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.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
||||||
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
||||||
<span className="date">{item.date}</span>
|
<span className="date">{item.date}</span>
|
||||||
<span className="message">{item.message}</span>
|
<span className="message" role="alert" aria-live="assertive">
|
||||||
|
{item.message}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
aria-label="console button expanded"
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -236,7 +236,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
aria-label="console button expanded"
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -340,7 +340,9 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
date
|
date
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
aria-live="assertive"
|
||||||
className="message"
|
className="message"
|
||||||
|
role="alert"
|
||||||
>
|
>
|
||||||
message
|
message
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Link } from "@fluentui/react";
|
||||||
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
|
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
|
||||||
// Vendor modules
|
// Vendor modules
|
||||||
import {
|
import {
|
||||||
@@ -13,13 +15,15 @@ import "@nteract/styles/editor-overrides.css";
|
|||||||
import "@nteract/styles/global-variables.css";
|
import "@nteract/styles/global-variables.css";
|
||||||
import "codemirror/addon/hint/show-hint.css";
|
import "codemirror/addon/hint/show-hint.css";
|
||||||
import "codemirror/lib/codemirror.css";
|
import "codemirror/lib/codemirror.css";
|
||||||
|
import { Notebook } from "Common/Constants";
|
||||||
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import * as Immutable from "immutable";
|
import * as Immutable from "immutable";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import "react-table/react-table.css";
|
import "react-table/react-table.css";
|
||||||
import { AnyAction, Store } from "redux";
|
import { AnyAction, Store } from "redux";
|
||||||
import { NotebookClientV2 } from "../NotebookClientV2";
|
import { NotebookClientV2 } from "../NotebookClientV2";
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||||
import * as NteractUtil from "../NTeractUtil";
|
import * as NteractUtil from "../NTeractUtil";
|
||||||
import * as CdbActions from "./actions";
|
import * as CdbActions from "./actions";
|
||||||
import { NotebookComponent } from "./NotebookComponent";
|
import { NotebookComponent } from "./NotebookComponent";
|
||||||
@@ -30,6 +34,19 @@ export interface NotebookComponentBootstrapperOptions {
|
|||||||
contentRef: ContentRef;
|
contentRef: ContentRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IWrapModel {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
last_modified: Date;
|
||||||
|
created: string;
|
||||||
|
content: unknown;
|
||||||
|
format: string;
|
||||||
|
mimetype: unknown;
|
||||||
|
size: number;
|
||||||
|
writeable: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class NotebookComponentBootstrapper {
|
export class NotebookComponentBootstrapper {
|
||||||
public contentRef: ContentRef;
|
public contentRef: ContentRef;
|
||||||
protected renderExtraComponent: () => JSX.Element;
|
protected renderExtraComponent: () => JSX.Element;
|
||||||
@@ -41,7 +58,7 @@ export class NotebookComponentBootstrapper {
|
|||||||
this.contentRef = options.contentRef;
|
this.contentRef = options.contentRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static wrapModelIntoContent(name: string, path: string, content: any) {
|
protected static wrapModelIntoContent(name: string, path: string, content: unknown): IWrapModel {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
@@ -49,7 +66,7 @@ export class NotebookComponentBootstrapper {
|
|||||||
created: "",
|
created: "",
|
||||||
content,
|
content,
|
||||||
format: "json",
|
format: "json",
|
||||||
mimetype: null as any,
|
mimetype: undefined,
|
||||||
size: 0,
|
size: 0,
|
||||||
writeable: false,
|
writeable: false,
|
||||||
type: "notebook",
|
type: "notebook",
|
||||||
@@ -85,7 +102,11 @@ export class NotebookComponentBootstrapper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public setContent(name: string, content: any): void {
|
public getNotebookPath(): string {
|
||||||
|
return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setContent(name: string, content: unknown): void {
|
||||||
this.getStore().dispatch(
|
this.getStore().dispatch(
|
||||||
actions.fetchContentFulfilled({
|
actions.fetchContentFulfilled({
|
||||||
filepath: undefined,
|
filepath: undefined,
|
||||||
@@ -116,11 +137,32 @@ export class NotebookComponentBootstrapper {
|
|||||||
|
|
||||||
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
|
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
|
||||||
public notebookSave(): void {
|
public notebookSave(): void {
|
||||||
|
if (
|
||||||
|
NotebookUtil.getContentProviderType(this.getNotebookPath()) ===
|
||||||
|
NotebookContentProviderType.JupyterContentProviderType
|
||||||
|
) {
|
||||||
|
useDialog.getState().showOkCancelModalDialog(
|
||||||
|
Notebook.saveNotebookModalTitle,
|
||||||
|
undefined,
|
||||||
|
"Save",
|
||||||
|
async () => {
|
||||||
this.getStore().dispatch(
|
this.getStore().dispatch(
|
||||||
actions.save({
|
actions.save({
|
||||||
contentRef: this.contentRef,
|
contentRef: this.contentRef,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
"Cancel",
|
||||||
|
undefined,
|
||||||
|
this.getSaveNotebookSubText()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.getStore().dispatch(
|
||||||
|
actions.save({
|
||||||
|
contentRef: this.contentRef,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public notebookChangeKernel(kernelSpecName: string): void {
|
public notebookChangeKernel(kernelSpecName: string): void {
|
||||||
@@ -270,7 +312,6 @@ export class NotebookComponentBootstrapper {
|
|||||||
public isContentDirty(): boolean {
|
public isContentDirty(): boolean {
|
||||||
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
||||||
if (!content) {
|
if (!content) {
|
||||||
console.log("No error");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,4 +369,19 @@ export class NotebookComponentBootstrapper {
|
|||||||
protected getStore(): Store<AppState, AnyAction> {
|
protected getStore(): Store<AppState, AnyAction> {
|
||||||
return this.notebookClient.getStore();
|
return this.notebookClient.getStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSaveNotebookSubText(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{Notebook.saveNotebookModalContent}</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{Notebook.newNotebookModalContent2}
|
||||||
|
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
||||||
|
{Notebook.learnMore}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
ServerConfig as JupyterServerConfig,
|
ServerConfig as JupyterServerConfig,
|
||||||
} from "@nteract/core";
|
} from "@nteract/core";
|
||||||
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
||||||
|
import { defineConfigOption } from "@nteract/mythic-configuration";
|
||||||
import { RecordOf } from "immutable";
|
import { RecordOf } from "immutable";
|
||||||
import { AnyAction } from "redux";
|
import { Action, AnyAction } from "redux";
|
||||||
import { ofType, StateObservable } from "redux-observable";
|
import { ofType, StateObservable } from "redux-observable";
|
||||||
import { kernels, sessions } from "rx-jupyter";
|
import { kernels, sessions } from "rx-jupyter";
|
||||||
import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
concatMap,
|
concatMap,
|
||||||
@@ -41,7 +42,7 @@ import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationCons
|
|||||||
import { useDialog } from "../../Controls/Dialog";
|
import { useDialog } from "../../Controls/Dialog";
|
||||||
import * as FileSystemUtil from "../FileSystemUtil";
|
import * as FileSystemUtil from "../FileSystemUtil";
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||||
import * as CdbActions from "./actions";
|
import * as CdbActions from "./actions";
|
||||||
import * as TextFile from "./contents/file/text-file";
|
import * as TextFile from "./contents/file/text-file";
|
||||||
import { CdbAppState } from "./types";
|
import { CdbAppState } from "./types";
|
||||||
@@ -109,7 +110,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
|
|||||||
const q = params.toString();
|
const q = params.toString();
|
||||||
const suffix = q !== "" ? `?${q}` : "";
|
const suffix = q !== "" ? `?${q}` : "";
|
||||||
|
|
||||||
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||||
|
|
||||||
return url.replace(/^http(s)?/, "ws$1");
|
return url.replace(/^http(s)?/, "ws$1");
|
||||||
};
|
};
|
||||||
@@ -948,6 +949,54 @@ const resetCellStatusOnExecuteCanceledEpic = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { selector: autoSaveInterval } = defineConfigOption({
|
||||||
|
key: "autoSaveInterval",
|
||||||
|
label: "Auto-save interval",
|
||||||
|
defaultValue: 120_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override autoSaveCurrentContentEpic to disable auto save for notebooks under temporary workspace.
|
||||||
|
* @param action$
|
||||||
|
*/
|
||||||
|
export function autoSaveCurrentContentEpic(
|
||||||
|
action$: Observable<Action>,
|
||||||
|
state$: StateObservable<AppState>
|
||||||
|
): Observable<actions.Save> {
|
||||||
|
return state$.pipe(
|
||||||
|
map((state) => autoSaveInterval(state)),
|
||||||
|
switchMap((time) => interval(time)),
|
||||||
|
mergeMap(() => {
|
||||||
|
const state = state$.value;
|
||||||
|
return from(
|
||||||
|
selectors
|
||||||
|
.contentByRef(state)
|
||||||
|
.filter(
|
||||||
|
/*
|
||||||
|
* Only save contents that are files or notebooks with
|
||||||
|
* a filepath already set.
|
||||||
|
*/
|
||||||
|
(content) => (content.type === "file" || content.type === "notebook") && content.filepath !== ""
|
||||||
|
)
|
||||||
|
.keys()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
filter((contentRef: ContentRef) => {
|
||||||
|
const model = selectors.model(state$.value, { contentRef });
|
||||||
|
const content = selectors.content(state$.value, { contentRef });
|
||||||
|
if (
|
||||||
|
model &&
|
||||||
|
model.type === "notebook" &&
|
||||||
|
NotebookUtil.getContentProviderType(content.filepath) !== NotebookContentProviderType.JupyterContentProviderType
|
||||||
|
) {
|
||||||
|
return selectors.notebook.isDirty(model);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
map((contentRef: ContentRef) => actions.save({ contentRef }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const allEpics = [
|
export const allEpics = [
|
||||||
addInitialCodeCellEpic,
|
addInitialCodeCellEpic,
|
||||||
focusInitialCodeCellEpic,
|
focusInitialCodeCellEpic,
|
||||||
@@ -965,4 +1014,5 @@ export const allEpics = [
|
|||||||
traceNotebookInfoEpic,
|
traceNotebookInfoEpic,
|
||||||
traceNotebookKernelEpic,
|
traceNotebookKernelEpic,
|
||||||
resetCellStatusOnExecuteCanceledEpic,
|
resetCellStatusOnExecuteCanceledEpic,
|
||||||
|
autoSaveCurrentContentEpic,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { AppState, epics as coreEpics, reducers, IContentProvider } from "@nteract/core";
|
import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
|
||||||
import { compose, Store, AnyAction, Middleware, Dispatch, MiddlewareAPI } from "redux";
|
|
||||||
import { Epic } from "redux-observable";
|
|
||||||
import { allEpics } from "./epics";
|
|
||||||
import { coreReducer, cdbReducer } from "./reducers";
|
|
||||||
import { catchError } from "rxjs/operators";
|
|
||||||
import { Observable } from "rxjs";
|
|
||||||
import { configuration } from "@nteract/mythic-configuration";
|
import { configuration } from "@nteract/mythic-configuration";
|
||||||
import { makeConfigureStore } from "@nteract/myths";
|
import { makeConfigureStore } from "@nteract/myths";
|
||||||
|
import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
|
||||||
|
import { Epic } from "redux-observable";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { catchError } from "rxjs/operators";
|
||||||
|
import { allEpics } from "./epics";
|
||||||
|
import { cdbReducer, coreReducer } from "./reducers";
|
||||||
import { CdbAppState } from "./types";
|
import { CdbAppState } from "./types";
|
||||||
|
|
||||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
@@ -81,7 +81,6 @@ export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] =>
|
|||||||
// This list needs to be consistent and in sync with core.allEpics until we figure
|
// This list needs to be consistent and in sync with core.allEpics until we figure
|
||||||
// out how to safely filter out the ones we are overriding here.
|
// out how to safely filter out the ones we are overriding here.
|
||||||
const filteredCoreEpics = [
|
const filteredCoreEpics = [
|
||||||
coreEpics.autoSaveCurrentContentEpic,
|
|
||||||
coreEpics.executeCellEpic,
|
coreEpics.executeCellEpic,
|
||||||
coreEpics.executeFocusedCellEpic,
|
coreEpics.executeFocusedCellEpic,
|
||||||
coreEpics.executeCellAfterKernelLaunchEpic,
|
coreEpics.executeCellAfterKernelLaunchEpic,
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* Notebook container related stuff
|
* Notebook container related stuff
|
||||||
*/
|
*/
|
||||||
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
|
||||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { IPhoenixConnectionInfoResult, IProvisionData, IResponse } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { useNotebook } from "./useNotebook";
|
import { useNotebook } from "./useNotebook";
|
||||||
|
|
||||||
export class NotebookContainerClient {
|
export class NotebookContainerClient {
|
||||||
private clearReconnectionAttemptMessage? = () => {};
|
private clearReconnectionAttemptMessage? = () => {};
|
||||||
private isResettingWorkspace: boolean;
|
private isResettingWorkspace: boolean;
|
||||||
|
private phoenixClient: PhoenixClient;
|
||||||
|
private retryOptions: promiseRetry.Options;
|
||||||
|
|
||||||
constructor(private onConnectionLost: () => void) {
|
constructor(private onConnectionLost: () => void) {
|
||||||
|
this.phoenixClient = new PhoenixClient();
|
||||||
|
this.retryOptions = {
|
||||||
|
retries: Notebook.retryAttempts,
|
||||||
|
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||||
|
minTimeout: Notebook.retryAttemptDelayMs,
|
||||||
|
};
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||||
@@ -35,14 +47,17 @@ export class NotebookContainerClient {
|
|||||||
* Heartbeat: each ping schedules another ping
|
* Heartbeat: each ping schedules another ping
|
||||||
*/
|
*/
|
||||||
private scheduleHeartbeat(delayMs: number): void {
|
private scheduleHeartbeat(delayMs: number): void {
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
this.getMemoryUsage()
|
const memoryUsageInfo = await this.getMemoryUsage();
|
||||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
|
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||||
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||||
|
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||||
|
}
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||||
const error = "No server endpoint detected";
|
const error = "No server endpoint detected";
|
||||||
@@ -56,7 +71,28 @@ export class NotebookContainerClient {
|
|||||||
|
|
||||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
|
const runMemoryAsync = async () => {
|
||||||
|
return await this._getMemoryAsync(notebookServerEndpoint, authToken);
|
||||||
|
};
|
||||||
|
return await promiseRetry(runMemoryAsync, this.retryOptions);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
|
||||||
|
if (!this.clearReconnectionAttemptMessage) {
|
||||||
|
this.clearReconnectionAttemptMessage = logConsoleProgress(
|
||||||
|
"Connection lost with Notebook server. Attempting to reconnect..."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.onConnectionLost();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getMemoryAsync(
|
||||||
|
notebookServerEndpoint: string,
|
||||||
|
authToken: string
|
||||||
|
): Promise<DataModels.MemoryUsageInfo> {
|
||||||
|
if (this.shouldExecuteMemoryCall()) {
|
||||||
|
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: authToken,
|
Authorization: authToken,
|
||||||
@@ -75,31 +111,36 @@ export class NotebookContainerClient {
|
|||||||
freeKB: memoryUsageInfo.free,
|
freeKB: memoryUsageInfo.free,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (response.status === HttpStatusCodes.NotFound) {
|
||||||
|
throw new AbortError(response.statusText);
|
||||||
}
|
}
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (error) {
|
}
|
||||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
|
}
|
||||||
if (!this.clearReconnectionAttemptMessage) {
|
|
||||||
this.clearReconnectionAttemptMessage = logConsoleProgress(
|
private shouldExecuteMemoryCall(): boolean {
|
||||||
"Connection lost with Notebook server. Attempting to reconnect..."
|
return (
|
||||||
|
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
|
||||||
|
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.onConnectionLost();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async resetWorkspace(): Promise<void> {
|
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||||
this.isResettingWorkspace = true;
|
this.isResettingWorkspace = true;
|
||||||
|
let response: IResponse<IPhoenixConnectionInfoResult>;
|
||||||
try {
|
try {
|
||||||
await this._resetWorkspace();
|
response = await this._resetWorkspace();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Promise.reject(error);
|
Promise.reject(error);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
this.isResettingWorkspace = false;
|
this.isResettingWorkspace = false;
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resetWorkspace(): Promise<void> {
|
private async _resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||||
const error = "No server endpoint detected";
|
const error = "No server endpoint detected";
|
||||||
@@ -107,15 +148,17 @@ export class NotebookContainerClient {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${notebookServerEndpoint}/api/shutdown`, {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
method: "POST",
|
const provisionData: IProvisionData = {
|
||||||
headers: { Authorization: authToken },
|
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||||
});
|
};
|
||||||
|
return await this.phoenixClient.resetContainer(provisionData);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
||||||
await this.recreateNotebookWorkspaceAsync();
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,22 +172,11 @@ export class NotebookContainerClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async recreateNotebookWorkspaceAsync(): Promise<void> {
|
private getHeaders(): HeadersInit {
|
||||||
const { databaseAccount } = userContext;
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
if (!databaseAccount?.id) {
|
return {
|
||||||
throw new Error("DataExplorer not initialized");
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
}
|
[HttpHeaders.contentType]: "application/json",
|
||||||
try {
|
};
|
||||||
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
|
||||||
await createOrUpdate(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
"default"
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export class NotebookContentClient {
|
|||||||
*
|
*
|
||||||
* @param parent parent folder
|
* @param parent parent folder
|
||||||
*/
|
*/
|
||||||
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> {
|
public async createNewNotebookFile(
|
||||||
|
parent: NotebookContentItem,
|
||||||
|
isGithubTree?: boolean
|
||||||
|
): Promise<NotebookContentItem> {
|
||||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
throw new Error(`Parent must be a directory: ${parent}`);
|
||||||
}
|
}
|
||||||
@@ -99,7 +102,6 @@ export class NotebookContentClient {
|
|||||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
throw new Error(`Parent must be a directory: ${parent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
||||||
if (await this.checkIfFilepathExists(filepath)) {
|
if (await this.checkIfFilepathExists(filepath)) {
|
||||||
throw new Error(`File already exists: ${filepath}`);
|
throw new Error(`File already exists: ${filepath}`);
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export default class NotebookManager {
|
|||||||
"Cancel",
|
"Cancel",
|
||||||
() => reject(new Error("Commit dialog canceled")),
|
() => reject(new Error("Commit dialog canceled")),
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
{
|
{
|
||||||
label: "Commit message",
|
label: "Commit message",
|
||||||
autoAdjustHeight: true,
|
autoAdjustHeight: true,
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import "./NotebookReadOnlyRenderer.less";
|
|||||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
import SandboxOutputs from "./outputs/SandboxOutputs";
|
||||||
|
|
||||||
export interface NotebookRendererProps {
|
export interface NotebookRendererProps {
|
||||||
contentRef: any;
|
contentRef: ContentRef;
|
||||||
hideInputs?: boolean;
|
hideInputs?: boolean;
|
||||||
hidePrompts?: boolean;
|
hidePrompts?: boolean;
|
||||||
|
addTransform: (component: React.ComponentType & { MIMETYPE: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +28,7 @@ export interface NotebookRendererProps {
|
|||||||
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!userContext.features.sandboxNotebookOutputs) {
|
if (!userContext.features.sandboxNotebookOutputs) {
|
||||||
loadTransform(this.props as any);
|
loadTransform(this.props as NotebookRendererProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
<div className="NotebookReadOnlyRender">
|
<div className="NotebookReadOnlyRender">
|
||||||
<Cells contentRef={this.props.contentRef}>
|
<Cells contentRef={this.props.contentRef}>
|
||||||
{{
|
{{
|
||||||
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
code: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
||||||
<CodeCell id={id} contentRef={contentRef}>
|
<CodeCell id={id} contentRef={contentRef}>
|
||||||
{{
|
{{
|
||||||
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
|
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
|
||||||
@@ -73,14 +74,14 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
}}
|
}}
|
||||||
</CodeCell>
|
</CodeCell>
|
||||||
),
|
),
|
||||||
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
markdown: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
||||||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
||||||
{{
|
{{
|
||||||
editor: {},
|
editor: {},
|
||||||
}}
|
}}
|
||||||
</MarkdownCell>
|
</MarkdownCell>
|
||||||
),
|
),
|
||||||
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
raw: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
||||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||||
{{
|
{{
|
||||||
editor: {
|
editor: {
|
||||||
@@ -98,6 +99,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
|
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||||
return {
|
return {
|
||||||
@@ -114,4 +116,4 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
|
|||||||
return mapDispatchToProps;
|
return mapDispatchToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(null, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
|
export default connect(undefined, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ import Html2Canvas from "html2canvas";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
import * as StringUtils from "../../Utils/StringUtils";
|
import * as StringUtils from "../../Utils/StringUtils";
|
||||||
|
import * as InMemoryContentProviderUtils from "../Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
|
||||||
import { SnapshotFragment } from "./NotebookComponent/types";
|
import { SnapshotFragment } from "./NotebookComponent/types";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
|
|
||||||
// Must match rx-jupyter' FileType
|
// Must match rx-jupyter' FileType
|
||||||
export type FileType = "directory" | "file" | "notebook";
|
export type FileType = "directory" | "file" | "notebook";
|
||||||
|
export enum NotebookContentProviderType {
|
||||||
|
GitHubContentProviderType,
|
||||||
|
InMemoryContentProviderType,
|
||||||
|
JupyterContentProviderType,
|
||||||
|
}
|
||||||
// Utilities for notebooks
|
// Utilities for notebooks
|
||||||
export class NotebookUtil {
|
export class NotebookUtil {
|
||||||
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
|
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
|
||||||
@@ -126,6 +132,18 @@ export class NotebookUtil {
|
|||||||
return relativePath.split("/").pop();
|
return relativePath.split("/").pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getContentProviderType(path: string): NotebookContentProviderType {
|
||||||
|
if (InMemoryContentProviderUtils.fromContentUri(path)) {
|
||||||
|
return NotebookContentProviderType.InMemoryContentProviderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GitHubUtils.fromContentUri(path)) {
|
||||||
|
return NotebookContentProviderType.GitHubContentProviderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotebookContentProviderType.JupyterContentProviderType;
|
||||||
|
}
|
||||||
|
|
||||||
public static replaceName(path: string, newName: string): string {
|
public static replaceName(path: string, newName: string): string {
|
||||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
if (contentInfo) {
|
if (contentInfo) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("auto start kernel", () => {
|
|||||||
connectionInfo: {
|
connectionInfo: {
|
||||||
authToken: "autToken",
|
authToken: "autToken",
|
||||||
notebookServerEndpoint: "notebookServerEndpoint",
|
notebookServerEndpoint: "notebookServerEndpoint",
|
||||||
|
forwardingId: "Id",
|
||||||
},
|
},
|
||||||
databaseAccountName: undefined,
|
databaseAccountName: undefined,
|
||||||
defaultExperience: undefined,
|
defaultExperience: undefined,
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import { ConnectionStatusType } from "../../Common/Constants";
|
||||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -28,6 +33,13 @@ interface NotebookState {
|
|||||||
myNotebooksContentRoot: NotebookContentItem;
|
myNotebooksContentRoot: NotebookContentItem;
|
||||||
gitHubNotebooksContentRoot: NotebookContentItem;
|
gitHubNotebooksContentRoot: NotebookContentItem;
|
||||||
galleryContentRoot: NotebookContentItem;
|
galleryContentRoot: NotebookContentItem;
|
||||||
|
connectionInfo: ContainerConnectionInfo;
|
||||||
|
notebookFolderName: string;
|
||||||
|
isAllocating: boolean;
|
||||||
|
isRefreshed: boolean;
|
||||||
|
containerStatus: ContainerInfo;
|
||||||
|
isPhoenixNotebooks: boolean;
|
||||||
|
isPhoenixFeatures: boolean;
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||||
@@ -36,6 +48,7 @@ interface NotebookState {
|
|||||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
|
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
|
||||||
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
||||||
setNotebookBasePath: (notebookBasePath: string) => void;
|
setNotebookBasePath: (notebookBasePath: string) => void;
|
||||||
|
setNotebookFolderName: (notebookFolderName: string) => void;
|
||||||
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
||||||
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
||||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
|
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||||
@@ -43,6 +56,14 @@ interface NotebookState {
|
|||||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||||
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
||||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
||||||
|
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
|
||||||
|
setIsAllocating: (isAllocating: boolean) => void;
|
||||||
|
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
|
||||||
|
setIsRefreshed: (isAllocating: boolean) => void;
|
||||||
|
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
||||||
|
getPhoenixStatus: () => Promise<void>;
|
||||||
|
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
|
||||||
|
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||||
@@ -51,6 +72,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
notebookServerInfo: {
|
notebookServerInfo: {
|
||||||
notebookServerEndpoint: undefined,
|
notebookServerEndpoint: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
|
forwardingId: undefined,
|
||||||
},
|
},
|
||||||
sparkClusterConnectionInfo: {
|
sparkClusterConnectionInfo: {
|
||||||
userName: undefined,
|
userName: undefined,
|
||||||
@@ -65,6 +87,19 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
myNotebooksContentRoot: undefined,
|
myNotebooksContentRoot: undefined,
|
||||||
gitHubNotebooksContentRoot: undefined,
|
gitHubNotebooksContentRoot: undefined,
|
||||||
galleryContentRoot: undefined,
|
galleryContentRoot: undefined,
|
||||||
|
connectionInfo: {
|
||||||
|
status: ConnectionStatusType.Connect,
|
||||||
|
},
|
||||||
|
notebookFolderName: undefined,
|
||||||
|
isAllocating: false,
|
||||||
|
isRefreshed: false,
|
||||||
|
containerStatus: {
|
||||||
|
status: undefined,
|
||||||
|
durationLeftInMinutes: undefined,
|
||||||
|
notebookServerInfo: undefined,
|
||||||
|
},
|
||||||
|
isPhoenixNotebooks: undefined,
|
||||||
|
isPhoenixFeatures: undefined,
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||||
@@ -75,7 +110,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||||
|
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||||
|
await get().getPhoenixStatus();
|
||||||
const { databaseAccount, authType } = userContext;
|
const { databaseAccount, authType } = userContext;
|
||||||
if (
|
if (
|
||||||
authType === AuthType.EncryptedToken ||
|
authType === AuthType.EncryptedToken ||
|
||||||
@@ -168,8 +205,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||||
},
|
},
|
||||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||||
|
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
|
||||||
|
set({ notebookFolderName });
|
||||||
const myNotebooksContentRoot = {
|
const myNotebooksContentRoot = {
|
||||||
name: "My Notebooks",
|
name: get().notebookFolderName,
|
||||||
path: get().notebookBasePath,
|
path: get().notebookBasePath,
|
||||||
type: NotebookContentItemType.Directory,
|
type: NotebookContentItemType.Directory,
|
||||||
};
|
};
|
||||||
@@ -185,6 +224,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
type: NotebookContentItemType.Directory,
|
type: NotebookContentItemType.Directory,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
set({
|
set({
|
||||||
myNotebooksContentRoot,
|
myNotebooksContentRoot,
|
||||||
galleryContentRoot,
|
galleryContentRoot,
|
||||||
@@ -246,4 +286,36 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
set({ gitHubNotebooksContentRoot });
|
set({ gitHubNotebooksContentRoot });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
||||||
|
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
||||||
|
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
||||||
|
useTabs.getState().closeAllNotebookTabs(true);
|
||||||
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
useNotebook.getState().setNotebookServerInfo(undefined);
|
||||||
|
useNotebook.getState().setIsAllocating(false);
|
||||||
|
useNotebook.getState().setContainerStatus({
|
||||||
|
status: undefined,
|
||||||
|
durationLeftInMinutes: undefined,
|
||||||
|
notebookServerInfo: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||||
|
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||||
|
getPhoenixStatus: async () => {
|
||||||
|
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
|
||||||
|
let isPhoenix = false;
|
||||||
|
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
|
||||||
|
const phoenixClient = new PhoenixClient();
|
||||||
|
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
|
||||||
|
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
|
||||||
|
|
||||||
|
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
||||||
|
set({ isPhoenixFeatures: isPhoenixFeatures });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
|
||||||
|
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import * as React from "react";
|
|||||||
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
||||||
|
|
||||||
export const OpenFullScreen: React.FunctionComponent = () => {
|
export const OpenFullScreen: React.FunctionComponent = () => {
|
||||||
|
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
|
||||||
|
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
|
||||||
const result = useFullScreenURLs();
|
const result = useFullScreenURLs();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
|
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
|
||||||
@@ -23,10 +25,12 @@ export const OpenFullScreen: React.FunctionComponent = () => {
|
|||||||
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
|
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
|
||||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
|
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copyToClipboard(readWriteUrl);
|
copyToClipboard(readWriteUrl);
|
||||||
|
setIsReadWriteUrlCopy(true);
|
||||||
}}
|
}}
|
||||||
text="Copy"
|
text={isReadWriteUrlCopy ? "Copied" : "Copy"}
|
||||||
iconProps={{ iconName: "Copy" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@@ -40,10 +44,12 @@ export const OpenFullScreen: React.FunctionComponent = () => {
|
|||||||
<TextField label="Read Only" readOnly defaultValue={readUrl} />
|
<TextField label="Read Only" readOnly defaultValue={readUrl} />
|
||||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
|
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setIsReadUrlCopy(true);
|
||||||
copyToClipboard(readUrl);
|
copyToClipboard(readUrl);
|
||||||
}}
|
}}
|
||||||
text="Copy"
|
text={isReadUrlCopy ? "Copied" : "Copy"}
|
||||||
iconProps={{ iconName: "Copy" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
|||||||
@@ -13,21 +13,21 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TooltipHost,
|
TooltipHost,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as Constants from "../../Common/Constants";
|
import { CollectionCreation } from "Shared/Constants";
|
||||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { userContext } from "UserContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { CollectionCreation } from "../../Shared/Constants";
|
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
|
||||||
import { isCapabilityEnabled, isServerlessAccount } from "../../Utils/CapabilityUtils";
|
|
||||||
import { getUpsellMessage } from "../../Utils/PricingUtils";
|
|
||||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
@@ -92,6 +92,7 @@ export interface AddCollectionPanelState {
|
|||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
showErrorDetails: boolean;
|
showErrorDetails: boolean;
|
||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
|
isThroughputCapExceeded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
@@ -122,6 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
showErrorDetails: false,
|
showErrorDetails: false,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
|
isThroughputCapExceeded: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +163,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
true
|
true
|
||||||
).toLocaleLowerCase()}.`}
|
).toLocaleLowerCase()}.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -210,6 +212,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
aria-label="New database id"
|
aria-label="New database id"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
tabIndex={0}
|
||||||
value={this.state.newDatabaseId}
|
value={this.state.newDatabaseId}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
this.setState({ newDatabaseId: event.target.value })
|
this.setState({ newDatabaseId: event.target.value })
|
||||||
@@ -236,7 +239,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
true
|
true
|
||||||
).toLocaleLowerCase()} within the database.`}
|
).toLocaleLowerCase()} within the database.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -248,6 +251,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||||
|
this.setState({ isThroughputCapExceeded })
|
||||||
|
}
|
||||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -273,13 +279,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`}
|
{`${getCollectionName()} id`}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -362,7 +368,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -409,7 +415,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={this.getPartitionKeyTooltipText()}
|
content={this.getPartitionKeyTooltipText()}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -467,7 +473,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -479,6 +485,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||||
|
this.setState({ isThroughputCapExceeded })
|
||||||
|
}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
||||||
this.isCostAcknowledged = isAcknowledged;
|
this.isCostAcknowledged = isAcknowledged;
|
||||||
}}
|
}}
|
||||||
@@ -497,7 +506,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
||||||
per partition key."
|
per partition key."
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -560,7 +569,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={this.getAnalyticalStorageTooltipContent()}
|
content={this.getAnalyticalStorageTooltipContent()}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -637,7 +646,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -658,7 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="My partition key is larger than 100 bytes"
|
label="My partition key is larger than 101 bytes"
|
||||||
checked={this.state.useHashV2}
|
checked={this.state.useHashV2}
|
||||||
styles={{
|
styles={{
|
||||||
text: { fontSize: 12 },
|
text: { fontSize: 12 },
|
||||||
@@ -675,7 +684,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PanelFooterComponent buttonLabel="OK" />
|
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
|
||||||
|
|
||||||
{this.state.isExecuting && <PanelLoadingScreen />}
|
{this.state.isExecuting && <PanelLoadingScreen />}
|
||||||
</form>
|
</form>
|
||||||
@@ -878,10 +887,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isServerlessAccount()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (userContext.apiType) {
|
switch (userContext.apiType) {
|
||||||
case "SQL":
|
case "SQL":
|
||||||
case "Mongo":
|
case "Mongo":
|
||||||
@@ -998,7 +1003,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
const collectionId: string = this.state.collectionId.trim();
|
const collectionId: string = this.state.collectionId.trim();
|
||||||
let databaseId = this.state.createNewDatabase ? this.state.newDatabaseId.trim() : this.state.selectedDatabaseId;
|
let databaseId = this.state.createNewDatabase ? this.state.newDatabaseId.trim() : this.state.selectedDatabaseId;
|
||||||
let partitionKeyString = this.state.partitionKey.trim();
|
let partitionKeyString = this.state.isSharded ? this.state.partitionKey.trim() : undefined;
|
||||||
|
|
||||||
if (userContext.apiType === "Tables") {
|
if (userContext.apiType === "Tables") {
|
||||||
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
|
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
|||||||
|
|
||||||
export interface AddDatabasePaneProps {
|
export interface AddDatabasePaneProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
|
buttonElement?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||||
explorer: container,
|
explorer: container,
|
||||||
|
buttonElement,
|
||||||
}: AddDatabasePaneProps) => {
|
}: AddDatabasePaneProps) => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
let throughput: number;
|
let throughput: number;
|
||||||
@@ -50,6 +52,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
);
|
);
|
||||||
const [formErrors, setFormErrors] = useState<string>("");
|
const [formErrors, setFormErrors] = useState<string>("");
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
|
||||||
|
|
||||||
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
|
|
||||||
@@ -77,6 +80,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
};
|
};
|
||||||
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
||||||
|
if (buttonElement) {
|
||||||
|
buttonElement.focus();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
@@ -166,6 +172,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
formError: formErrors,
|
formError: formErrors,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
submitButtonText: "OK",
|
submitButtonText: "OK",
|
||||||
|
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,6 +243,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
isSharded={databaseCreateNewShared}
|
isSharded={databaseCreateNewShared}
|
||||||
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
<RightPaneForm
|
<RightPaneForm
|
||||||
formError=""
|
formError=""
|
||||||
isExecuting={false}
|
isExecuting={false}
|
||||||
|
isSubmitButtonDisabled={false}
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
submitButtonText="OK"
|
submitButtonText="OK"
|
||||||
>
|
>
|
||||||
@@ -92,6 +93,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
isSharded={true}
|
isSharded={true}
|
||||||
onCostAcknowledgeChange={[Function]}
|
onCostAcknowledgeChange={[Function]}
|
||||||
setIsAutoscale={[Function]}
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
setThroughputValue={[Function]}
|
setThroughputValue={[Function]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react";
|
import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as SharedConstants from "Shared/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { userContext } from "UserContext";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||||
@@ -43,6 +43,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
|
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||||
const [formError, setFormError] = useState<string>("");
|
const [formError, setFormError] = useState<string>("");
|
||||||
|
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
|
||||||
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
|
|
||||||
const addCollectionPaneOpenMessage = {
|
const addCollectionPaneOpenMessage = {
|
||||||
@@ -149,6 +150,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
formError,
|
formError,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
submitButtonText: "OK",
|
submitButtonText: "OK",
|
||||||
|
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,6 +200,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<TextField
|
<TextField
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
styles={getTextFieldStyles()}
|
styles={getTextFieldStyles()}
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
@@ -261,6 +264,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
isSharded
|
isSharded
|
||||||
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -285,6 +289,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
underlined
|
underlined
|
||||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
|
required={true}
|
||||||
ariaLabel="addCollection-tableId"
|
ariaLabel="addCollection-tableId"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
@@ -329,9 +334,10 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
isSharded={false}
|
isSharded
|
||||||
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
|||||||
selectedLocation.owner,
|
selectedLocation.owner,
|
||||||
selectedLocation.repo
|
selectedLocation.repo
|
||||||
)} - ${selectedLocation.branch}`;
|
)} - ${selectedLocation.branch}`;
|
||||||
|
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
destination = useNotebook.getState().notebookFolderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import React, { FormEvent, FunctionComponent } from "react";
|
import React, { FormEvent, FunctionComponent } from "react";
|
||||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||||
|
|
||||||
interface Location {
|
interface Location {
|
||||||
@@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
|||||||
|
|
||||||
const getDropDownOptions = (): IDropdownOption[] => {
|
const getDropDownOptions = (): IDropdownOption[] => {
|
||||||
const options: IDropdownOption[] = [];
|
const options: IDropdownOption[] = [];
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
key: "MyNotebooks-Item",
|
key: "MyNotebooks-Item",
|
||||||
text: ResourceTreeAdapter.MyNotebooksTitle,
|
text: useNotebook.getState().notebookFolderName,
|
||||||
title: ResourceTreeAdapter.MyNotebooksTitle,
|
title: useNotebook.getState().notebookFolderName,
|
||||||
data: {
|
data: {
|
||||||
type: "MyNotebooks",
|
type: "MyNotebooks",
|
||||||
} as Location,
|
} as Location,
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Text, TextField } from "@fluentui/react";
|
import { Text, TextField } from "@fluentui/react";
|
||||||
|
import { Areas } from "Common/Constants";
|
||||||
|
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
||||||
|
import DeleteFeedback from "Common/DeleteFeedback";
|
||||||
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
|
import { Collection } from "Contracts/ViewModels";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { Areas } from "../../../Common/Constants";
|
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||||
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import DeleteFeedback from "../../../Common/DeleteFeedback";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { userContext } from "UserContext";
|
||||||
import { Collection } from "../../../Contracts/ViewModels";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
import { useTabs } from "../../../hooks/useTabs";
|
|
||||||
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
|
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
@@ -38,7 +38,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
const onSubmit = async (): Promise<void> => {
|
const onSubmit = async (): Promise<void> => {
|
||||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
if (!collection || inputCollectionName !== collection.id()) {
|
if (!collection || inputCollectionName !== collection.id()) {
|
||||||
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
|
const errorMessage = "Input " + collectionName + " id does not match the selected " + collectionName;
|
||||||
setFormError(errorMessage);
|
setFormError(errorMessage);
|
||||||
NotificationConsoleUtils.logConsoleError(
|
NotificationConsoleUtils.logConsoleError(
|
||||||
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`
|
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`
|
||||||
@@ -108,6 +108,8 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
submitButtonText: "OK",
|
submitButtonText: "OK",
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
|
||||||
|
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
<div className="panelFormWrapper">
|
<div className="panelFormWrapper">
|
||||||
@@ -123,6 +125,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setInputCollectionName(newInput);
|
setInputCollectionName(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={confirmContainer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{shouldRecordFeedback() && (
|
{shouldRecordFeedback() && (
|
||||||
@@ -142,6 +145,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setDeleteCollectionFeedback(newInput);
|
setDeleteCollectionFeedback(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={reasonInfo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
ariaLabel="Confirm by typing the container id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -53,6 +54,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
value=""
|
value=""
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
|
ariaLabel="Confirm by typing the container id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
@@ -346,6 +348,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
|
aria-label="Confirm by typing the container id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="ms-TextField-field field-57"
|
className="ms-TextField-field field-57"
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
@@ -366,18 +369,21 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="OK"
|
buttonLabel="OK"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
theme={
|
theme={
|
||||||
@@ -657,6 +663,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -938,6 +945,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1220,6 +1228,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Text, TextField } from "@fluentui/react";
|
import { Text, TextField } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
|
import { Areas } from "Common/Constants";
|
||||||
|
import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
|
||||||
|
import DeleteFeedback from "Common/DeleteFeedback";
|
||||||
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
|
import { Collection, Database } from "Contracts/ViewModels";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { Areas } from "../../Common/Constants";
|
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||||
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { userContext } from "UserContext";
|
||||||
import { Collection, Database } from "../../Contracts/ViewModels";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
|
||||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
|
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
|
||||||
@@ -118,7 +118,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
message:
|
message:
|
||||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||||
};
|
};
|
||||||
|
const confirmDatabase = "Confirm by typing the database id";
|
||||||
|
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?";
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||||
@@ -133,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setDatabaseInput(newInput);
|
setDatabaseInput(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={confirmDatabase}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLastNonEmptyDatabase() && (
|
{isLastNonEmptyDatabase() && (
|
||||||
@@ -151,6 +153,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setDatabaseFeedbackInput(newInput);
|
setDatabaseFeedbackInput(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={reasonInfo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react";
|
import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useRef, useState } from "react";
|
||||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||||
@@ -25,19 +25,16 @@ interface UnwrappedExecuteSprocParam {
|
|||||||
export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
|
export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
|
||||||
storedProcedure,
|
storedProcedure,
|
||||||
}: ExecuteSprocParamsPaneProps): JSX.Element => {
|
}: ExecuteSprocParamsPaneProps): JSX.Element => {
|
||||||
|
const paramKeyValuesRef = useRef<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
|
||||||
|
const partitionValueRef = useRef<string>();
|
||||||
|
const partitionKeyRef = useRef<string>("string");
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
|
const [numberOfParams, setNumberOfParams] = useState<number>(1);
|
||||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||||
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
|
|
||||||
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
|
|
||||||
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
|
|
||||||
const [formError, setFormError] = useState<string>("");
|
const [formError, setFormError] = useState<string>("");
|
||||||
|
|
||||||
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
|
|
||||||
setSelectedKey(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateUnwrappedParams = (): boolean => {
|
const validateUnwrappedParams = (): boolean => {
|
||||||
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
|
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
|
||||||
for (let i = 0; i < unwrappedParams.length; i++) {
|
for (let i = 0; i < unwrappedParams.length; i++) {
|
||||||
const { key: paramType, text: paramValue } = unwrappedParams[i];
|
const { key: paramType, text: paramValue } = unwrappedParams[i];
|
||||||
if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) {
|
if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) {
|
||||||
@@ -53,8 +50,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submit = (): void => {
|
const submit = (): void => {
|
||||||
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
|
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
|
||||||
const { key: partitionKey } = selectedKey;
|
const partitionValue: string = partitionValueRef.current;
|
||||||
|
const partitionKey: string = partitionKeyRef.current;
|
||||||
if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) {
|
if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) {
|
||||||
setInvalidParamError(partitionValue);
|
setInvalidParamError(partitionValue);
|
||||||
return;
|
return;
|
||||||
@@ -78,37 +76,21 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteParamAtIndex = (indexToRemove: number): void => {
|
const deleteParamAtIndex = (indexToRemove: number): void => {
|
||||||
const cloneParamKeyValue = [...paramKeyValues];
|
paramKeyValuesRef.current.splice(indexToRemove, 1);
|
||||||
cloneParamKeyValue.splice(indexToRemove, 1);
|
setNumberOfParams(numberOfParams - 1);
|
||||||
setParamKeyValues(cloneParamKeyValue);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addNewParamAtIndex = (indexToAdd: number): void => {
|
const addNewParamAtIndex = (indexToAdd: number): void => {
|
||||||
const cloneParamKeyValue = [...paramKeyValues];
|
paramKeyValuesRef.current.splice(indexToAdd, 0, { key: "string", text: "" });
|
||||||
cloneParamKeyValue.splice(indexToAdd, 0, { key: "string", text: "" });
|
setNumberOfParams(numberOfParams + 1);
|
||||||
setParamKeyValues(cloneParamKeyValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const paramValueChange = (value: string, indexOfInput: number): void => {
|
|
||||||
const cloneParamKeyValue = [...paramKeyValues];
|
|
||||||
cloneParamKeyValue[indexOfInput].text = value;
|
|
||||||
setParamKeyValues(cloneParamKeyValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const paramKeyChange = (
|
|
||||||
_event: React.FormEvent<HTMLDivElement>,
|
|
||||||
selectedParam: IDropdownOption,
|
|
||||||
indexOfParam: number
|
|
||||||
): void => {
|
|
||||||
const cloneParamKeyValue = [...paramKeyValues];
|
|
||||||
cloneParamKeyValue[indexOfParam].key = selectedParam.key.toString();
|
|
||||||
setParamKeyValues(cloneParamKeyValue);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addNewParamAtLastIndex = (): void => {
|
const addNewParamAtLastIndex = (): void => {
|
||||||
const cloneParamKeyValue = [...paramKeyValues];
|
paramKeyValuesRef.current.push({
|
||||||
cloneParamKeyValue.splice(cloneParamKeyValue.length, 0, { key: "string", text: "" });
|
key: "string",
|
||||||
setParamKeyValues(cloneParamKeyValue);
|
text: "",
|
||||||
|
});
|
||||||
|
setNumberOfParams(numberOfParams + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const props: RightPaneFormProps = {
|
const props: RightPaneFormProps = {
|
||||||
@@ -118,47 +100,53 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
|||||||
onSubmit: () => submit(),
|
onSubmit: () => submit(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInputParameterComponent = (): JSX.Element[] => {
|
||||||
|
const inputParameters: JSX.Element[] = [];
|
||||||
|
for (let i = 0; i < numberOfParams; i++) {
|
||||||
|
const paramKeyValue = paramKeyValuesRef.current[i];
|
||||||
|
inputParameters.push(
|
||||||
|
<InputParameter
|
||||||
|
key={paramKeyValue.text + i}
|
||||||
|
dropdownLabel={i === 0 ? "Key" : ""}
|
||||||
|
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""}
|
||||||
|
inputLabel={i === 0 ? "Param" : ""}
|
||||||
|
isAddRemoveVisible={true}
|
||||||
|
onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
|
||||||
|
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
|
||||||
|
onParamValueChange={(_event, newInput?: string) => (paramKeyValuesRef.current[i].text = newInput)}
|
||||||
|
onParamKeyChange={(_event, selectedParam: IDropdownOption) =>
|
||||||
|
(paramKeyValuesRef.current[i].key = selectedParam.key.toString())
|
||||||
|
}
|
||||||
|
paramValue={paramKeyValue.text}
|
||||||
|
selectedKey={paramKeyValue.key}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputParameters;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
<div className="panelFormWrapper">
|
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
<InputParameter
|
<InputParameter
|
||||||
dropdownLabel="Key"
|
dropdownLabel="Key"
|
||||||
inputParameterTitle="Partition key value"
|
inputParameterTitle="Partition key value"
|
||||||
inputLabel="Value"
|
inputLabel="Value"
|
||||||
isAddRemoveVisible={false}
|
isAddRemoveVisible={false}
|
||||||
onParamValueChange={(_event, newInput?: string) => {
|
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
|
||||||
setPartitionValue(newInput);
|
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
|
||||||
}}
|
(partitionKeyRef.current = item.key.toString())
|
||||||
onParamKeyChange={onPartitionKeyChange}
|
}
|
||||||
paramValue={partitionValue}
|
paramValue={partitionValueRef.current}
|
||||||
selectedKey={selectedKey.key}
|
selectedKey={partitionKeyRef.current}
|
||||||
/>
|
/>
|
||||||
{paramKeyValues.map((paramKeyValue, index) => (
|
{getInputParameterComponent()}
|
||||||
<InputParameter
|
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
|
||||||
key={paramKeyValue && paramKeyValue.text + index}
|
|
||||||
dropdownLabel={!index && "Key"}
|
|
||||||
inputParameterTitle={!index && "Enter input parameters (if any)"}
|
|
||||||
inputLabel={!index && "Param"}
|
|
||||||
isAddRemoveVisible={true}
|
|
||||||
onDeleteParamKeyPress={() => deleteParamAtIndex(index)}
|
|
||||||
onAddNewParamKeyPress={() => addNewParamAtIndex(index + 1)}
|
|
||||||
onParamValueChange={(event, newInput?: string) => {
|
|
||||||
paramValueChange(newInput, index);
|
|
||||||
}}
|
|
||||||
onParamKeyChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
|
|
||||||
paramKeyChange(event, selectedParam, index);
|
|
||||||
}}
|
|
||||||
paramValue={paramKeyValue && paramKeyValue.text}
|
|
||||||
selectedKey={paramKeyValue && paramKeyValue.key}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Stack horizontal onClick={addNewParamAtLastIndex}>
|
|
||||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||||
<Text className="addNewParamStyle">Add New Param</Text>
|
<Text className="addNewParamStyle">Add New Param</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,34 +55,41 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
|||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
label={dropdownLabel && dropdownLabel}
|
label={dropdownLabel && dropdownLabel}
|
||||||
selectedKey={selectedKey}
|
defaultSelectedKey={selectedKey}
|
||||||
onChange={onParamKeyChange}
|
onChange={onParamKeyChange}
|
||||||
options={options}
|
options={options}
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label={inputLabel && inputLabel}
|
label={inputLabel && inputLabel}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
autoFocus
|
defaultValue={paramValue}
|
||||||
value={paramValue}
|
|
||||||
onChange={onParamValueChange}
|
onChange={onParamValueChange}
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
{isAddRemoveVisible && (
|
{isAddRemoveVisible && (
|
||||||
<>
|
<>
|
||||||
|
<div tabIndex={0}>
|
||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={EntityCancelIcon}
|
src={EntityCancelIcon}
|
||||||
alt="Delete param"
|
alt="Delete param"
|
||||||
id="deleteparam"
|
id="deleteparam"
|
||||||
|
role="button"
|
||||||
onClick={onDeleteParamKeyPress}
|
onClick={onDeleteParamKeyPress}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div tabIndex={0}>
|
||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={AddPropertyIcon}
|
src={AddPropertyIcon}
|
||||||
alt="Add param"
|
alt="Add param"
|
||||||
id="addparam"
|
id="addparam"
|
||||||
|
role="button"
|
||||||
onClick={onAddNewParamKeyPress}
|
onClick={onAddNewParamKeyPress}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
<form
|
<form
|
||||||
className="panelFormWrapper"
|
className="panelFormWrapper"
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="panelFormWrapper"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelMainContent"
|
className="panelMainContent"
|
||||||
@@ -322,6 +319,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
className="ms-Stack css-54"
|
className="ms-Stack css-54"
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
defaultSelectedKey="string"
|
||||||
key=".0:$.0"
|
key=".0:$.0"
|
||||||
label="Key"
|
label="Key"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -337,7 +335,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="string"
|
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"dropdown": Object {
|
"dropdown": Object {
|
||||||
@@ -345,8 +342,10 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<DropdownBase
|
<DropdownBase
|
||||||
|
defaultSelectedKey="string"
|
||||||
label="Key"
|
label="Key"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
@@ -361,8 +360,8 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="string"
|
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -638,6 +637,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DropdownInternal
|
<DropdownInternal
|
||||||
|
defaultSelectedKey="string"
|
||||||
hoisted={
|
hoisted={
|
||||||
Object {
|
Object {
|
||||||
"rootRef": [Function],
|
"rootRef": [Function],
|
||||||
@@ -662,8 +662,8 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
responsiveMode={3}
|
responsiveMode={3}
|
||||||
selectedKey="string"
|
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -1562,20 +1562,20 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
</DropdownBase>
|
</DropdownBase>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
autoFocus={true}
|
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
key=".0:$.1"
|
key=".0:$.1"
|
||||||
label="Value"
|
label="Value"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
autoFocus={true}
|
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
label="Value"
|
label="Value"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
resizable={true}
|
resizable={true}
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -2155,13 +2155,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
aria-labelledby="TextFieldLabel3"
|
aria-labelledby="TextFieldLabel3"
|
||||||
autoFocus={true}
|
|
||||||
className="ms-TextField-field field-77"
|
className="ms-TextField-field field-77"
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onInput={[Function]}
|
onInput={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
@@ -2477,6 +2477,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
className="ms-Stack css-54"
|
className="ms-Stack css-54"
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
defaultSelectedKey="string"
|
||||||
key=".0:$.0"
|
key=".0:$.0"
|
||||||
label="Key"
|
label="Key"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -2492,7 +2493,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="string"
|
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"dropdown": Object {
|
"dropdown": Object {
|
||||||
@@ -2500,8 +2500,10 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<DropdownBase
|
<DropdownBase
|
||||||
|
defaultSelectedKey="string"
|
||||||
label="Key"
|
label="Key"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
@@ -2516,8 +2518,8 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="string"
|
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -2793,6 +2795,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DropdownInternal
|
<DropdownInternal
|
||||||
|
defaultSelectedKey="string"
|
||||||
hoisted={
|
hoisted={
|
||||||
Object {
|
Object {
|
||||||
"rootRef": [Function],
|
"rootRef": [Function],
|
||||||
@@ -2817,8 +2820,8 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
responsiveMode={3}
|
responsiveMode={3}
|
||||||
selectedKey="string"
|
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -3717,21 +3720,22 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
</DropdownBase>
|
</DropdownBase>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
autoFocus={true}
|
defaultValue=""
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
key=".0:$.1"
|
key=".0:$.1"
|
||||||
label="Param"
|
label="Param"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value=""
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
autoFocus={true}
|
defaultValue=""
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
label="Param"
|
label="Param"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
resizable={true}
|
resizable={true}
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -4006,7 +4010,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateOnLoad={true}
|
validateOnLoad={true}
|
||||||
value=""
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField root-75"
|
className="ms-TextField root-75"
|
||||||
@@ -4312,13 +4315,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
aria-labelledby="TextFieldLabel7"
|
aria-labelledby="TextFieldLabel7"
|
||||||
autoFocus={true}
|
|
||||||
className="ms-TextField-field field-77"
|
className="ms-TextField-field field-77"
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onInput={[Function]}
|
onInput={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
@@ -4327,12 +4330,16 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</TextFieldBase>
|
</TextFieldBase>
|
||||||
</StyledTextFieldBase>
|
</StyledTextFieldBase>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<StyledImageBase
|
<StyledImageBase
|
||||||
alt="Delete param"
|
alt="Delete param"
|
||||||
className="addRemoveIconLabel"
|
className="addRemoveIconLabel"
|
||||||
height={30}
|
height={30}
|
||||||
id="deleteparam"
|
id="deleteparam"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
src=""
|
src=""
|
||||||
width={20}
|
width={20}
|
||||||
>
|
>
|
||||||
@@ -4342,6 +4349,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
height={30}
|
height={30}
|
||||||
id="deleteparam"
|
id="deleteparam"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
src=""
|
src=""
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
theme={
|
theme={
|
||||||
@@ -4636,17 +4644,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onError={[Function]}
|
onError={[Function]}
|
||||||
onLoad={[Function]}
|
onLoad={[Function]}
|
||||||
|
role="button"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ImageBase>
|
</ImageBase>
|
||||||
</StyledImageBase>
|
</StyledImageBase>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<StyledImageBase
|
<StyledImageBase
|
||||||
alt="Add param"
|
alt="Add param"
|
||||||
className="addRemoveIconLabel"
|
className="addRemoveIconLabel"
|
||||||
height={30}
|
height={30}
|
||||||
id="addparam"
|
id="addparam"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
src=""
|
src=""
|
||||||
width={20}
|
width={20}
|
||||||
>
|
>
|
||||||
@@ -4656,6 +4670,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
height={30}
|
height={30}
|
||||||
id="addparam"
|
id="addparam"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
src=""
|
src=""
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
theme={
|
theme={
|
||||||
@@ -4950,21 +4965,25 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onError={[Function]}
|
onError={[Function]}
|
||||||
onLoad={[Function]}
|
onLoad={[Function]}
|
||||||
|
role="button"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ImageBase>
|
</ImageBase>
|
||||||
</StyledImageBase>
|
</StyledImageBase>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</InputParameter>
|
</InputParameter>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-Stack css-54"
|
className="ms-Stack css-54"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<StyledImageBase
|
<StyledImageBase
|
||||||
alt="Add param"
|
alt="Add param"
|
||||||
@@ -5286,21 +5305,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Execute"
|
buttonLabel="Execute"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Execute"
|
text="Execute"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Execute"
|
text="Execute"
|
||||||
theme={
|
theme={
|
||||||
@@ -5580,6 +5601,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -5861,6 +5883,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -6143,6 +6166,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ interface IGitHubReposPanelState {
|
|||||||
}
|
}
|
||||||
export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IGitHubReposPanelState> {
|
export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IGitHubReposPanelState> {
|
||||||
private static readonly PageSize = 30;
|
private static readonly PageSize = 30;
|
||||||
|
private static readonly MasterBranchName = "master";
|
||||||
|
private static readonly MainBranchName = "main";
|
||||||
|
|
||||||
private isAddedRepo = false;
|
private isAddedRepo = false;
|
||||||
private gitHubClient: GitHubClient;
|
private gitHubClient: GitHubClient;
|
||||||
private junoClient: JunoClient;
|
private junoClient: JunoClient;
|
||||||
@@ -116,6 +119,8 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
if (response.status !== HttpStatusCodes.OK) {
|
if (response.status !== HttpStatusCodes.OK) {
|
||||||
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
|
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.props.explorer.notebookManager?.refreshPinnedRepos();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
||||||
}
|
}
|
||||||
@@ -207,6 +212,14 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
if (response.data) {
|
if (response.data) {
|
||||||
branchesProps.branches = branchesProps.branches.concat(response.data);
|
branchesProps.branches = branchesProps.branches.concat(response.data);
|
||||||
branchesProps.lastPageInfo = response.pageInfo;
|
branchesProps.lastPageInfo = response.pageInfo;
|
||||||
|
branchesProps.defaultBranchName = branchesProps.branches[0].name;
|
||||||
|
const defaultbranchName = branchesProps.branches.find(
|
||||||
|
(branch) =>
|
||||||
|
branch.name === GitHubReposPanel.MasterBranchName || branch.name === GitHubReposPanel.MainBranchName
|
||||||
|
)?.name;
|
||||||
|
if (defaultbranchName) {
|
||||||
|
branchesProps.defaultBranchName = defaultbranchName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
|
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
|
||||||
@@ -298,6 +311,17 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
const existingRepo = this.pinnedReposProps.repos.find((repo) => repo.key === item.key);
|
const existingRepo = this.pinnedReposProps.repos.find((repo) => repo.key === item.key);
|
||||||
if (existingRepo) {
|
if (existingRepo) {
|
||||||
existingRepo.branches = item.branches;
|
existingRepo.branches = item.branches;
|
||||||
|
this.setState({
|
||||||
|
gitHubReposState: {
|
||||||
|
...this.state.gitHubReposState,
|
||||||
|
reposListProps: {
|
||||||
|
...this.state.gitHubReposState.reposListProps,
|
||||||
|
pinnedReposProps: {
|
||||||
|
repos: this.pinnedReposProps.repos,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
|
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
|
||||||
}
|
}
|
||||||
@@ -374,6 +398,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
lastPageInfo: undefined,
|
lastPageInfo: undefined,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
defaultBranchName: undefined,
|
||||||
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
|
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
|
||||||
};
|
};
|
||||||
this.loadMoreBranches(item.repo);
|
this.loadMoreBranches(item.repo);
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
.panelFormWrapper {
|
.panelFormWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
|
|
||||||
.panelMainContent {
|
.panelMainContent {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 0 34px;
|
padding: 0 34px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
overflow: auto;
|
overflow-x: hidden;
|
||||||
|
|
||||||
& > :not(.collapsibleSection) {
|
& > :not(.collapsibleSection) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ import React from "react";
|
|||||||
|
|
||||||
export interface PanelFooterProps {
|
export interface PanelFooterProps {
|
||||||
buttonLabel: string;
|
buttonLabel: string;
|
||||||
|
isButtonDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
|
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
|
||||||
buttonLabel,
|
buttonLabel,
|
||||||
|
isButtonDisabled,
|
||||||
}: PanelFooterProps): JSX.Element => (
|
}: PanelFooterProps): JSX.Element => (
|
||||||
<div className="panelFooter">
|
<div className="panelFooter">
|
||||||
<PrimaryButton type="submit" id="sidePanelOkButton" text={buttonLabel} ariaLabel={buttonLabel} />
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
id="sidePanelOkButton"
|
||||||
|
text={buttonLabel}
|
||||||
|
ariaLabel={buttonLabel}
|
||||||
|
disabled={!!isButtonDisabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
|||||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||||
{icon}
|
{icon}
|
||||||
<span className="panelWarningErrorDetailsLinkContainer">
|
<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}
|
{message}
|
||||||
{link && linkText && (
|
{link && linkText && (
|
||||||
<Link target="_blank" href={link}>
|
<Link target="_blank" href={link}>
|
||||||
|
|||||||
@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
|
|||||||
it("should render error in header", () => {
|
it("should render error in header", () => {
|
||||||
render(<RightPaneForm {...props} formError="file already Exist" />);
|
render(<RightPaneForm {...props} formError="file already Exist" />);
|
||||||
expect(screen.getByLabelText("error")).toBeDefined();
|
expect(screen.getByLabelText("error")).toBeDefined();
|
||||||
expect(screen.getByLabelText("message").innerHTML).toEqual("file already Exist");
|
expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface RightPaneFormProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
submitButtonText: string;
|
submitButtonText: string;
|
||||||
isSubmitButtonHidden?: boolean;
|
isSubmitButtonHidden?: boolean;
|
||||||
|
isSubmitButtonDisabled?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
submitButtonText,
|
submitButtonText,
|
||||||
isSubmitButtonHidden = false,
|
isSubmitButtonHidden = false,
|
||||||
|
isSubmitButtonDisabled = false,
|
||||||
children,
|
children,
|
||||||
}: RightPaneFormProps) => {
|
}: RightPaneFormProps) => {
|
||||||
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -30,7 +32,9 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
|||||||
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
|
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
|
||||||
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
|
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
|
||||||
{children}
|
{children}
|
||||||
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
|
{!isSubmitButtonHidden && (
|
||||||
|
<PanelFooterComponent buttonLabel={submitButtonText} isButtonDisabled={isSubmitButtonDisabled} />
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
{isExecuting && <PanelLoadingScreen />}
|
{isExecuting && <PanelLoadingScreen />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,18 +14,21 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Load"
|
buttonLabel="Load"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Load"
|
text="Load"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Load"
|
text="Load"
|
||||||
theme={
|
theme={
|
||||||
@@ -305,6 +308,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -586,6 +590,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -868,6 +873,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
|
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, MouseEvent, useState } from "react";
|
import React, { FunctionComponent, MouseEvent, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { configContext } from "../../../ConfigContext";
|
import { userContext } from "UserContext";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
|
|
||||||
import * as StringUtility from "../../../Shared/StringUtility";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
|
||||||
export const SettingsPane: FunctionComponent = () => {
|
export const SettingsPane: FunctionComponent = () => {
|
||||||
@@ -113,20 +113,50 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const choiceButtonStyles = {
|
||||||
|
root: {
|
||||||
|
clear: "both",
|
||||||
|
},
|
||||||
|
flexContainer: [
|
||||||
|
{
|
||||||
|
selectors: {
|
||||||
|
".ms-ChoiceFieldGroup root-133": {
|
||||||
|
clear: "both",
|
||||||
|
},
|
||||||
|
".ms-ChoiceField-wrapper label": {
|
||||||
|
fontSize: 12,
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
".ms-ChoiceField": {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className="paneMainContent">
|
<div className="paneMainContent">
|
||||||
{shouldShowQueryPageOptions && (
|
{shouldShowQueryPageOptions && (
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart pageOptionsPart">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionLabel">
|
<fieldset>
|
||||||
Page options
|
<legend id="pageOptions" className="settingsSectionLabel legendLabel">
|
||||||
|
Page Options
|
||||||
|
</legend>
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
|
||||||
query results per page.
|
query results per page.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
<ChoiceGroup
|
||||||
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
|
ariaLabelledBy="pageOptions"
|
||||||
|
selectedKey={pageOption}
|
||||||
|
options={pageOptionList}
|
||||||
|
styles={choiceButtonStyles}
|
||||||
|
onChange={handleOnPageOptionChange}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabs settingsSectionPart">
|
<div className="tabs settingsSectionPart">
|
||||||
{isCustomPageOptionSelected() && (
|
{isCustomPageOptionSelected() && (
|
||||||
@@ -195,7 +225,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
step={1}
|
step={1}
|
||||||
className="textfontclr"
|
className="textfontclr"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabIndex={0}
|
|
||||||
id="max-degree"
|
id="max-degree"
|
||||||
value={"" + maxDegreeOfParallelism}
|
value={"" + maxDegreeOfParallelism}
|
||||||
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
||||||
|
|||||||
@@ -14,17 +14,20 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart pageOptionsPart"
|
className="settingsSectionPart"
|
||||||
>
|
>
|
||||||
<div
|
<fieldset>
|
||||||
className="settingsSectionLabel"
|
<legend
|
||||||
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="pageOptions"
|
||||||
>
|
>
|
||||||
Page options
|
Page Options
|
||||||
|
</legend>
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
|
ariaLabelledBy="pageOptions"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
Array [
|
Array [
|
||||||
@@ -39,7 +42,31 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="custom"
|
selectedKey="custom"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"flexContainer": Array [
|
||||||
|
Object {
|
||||||
|
"selectors": Object {
|
||||||
|
".ms-ChoiceField": Object {
|
||||||
|
"marginTop": 0,
|
||||||
|
},
|
||||||
|
".ms-ChoiceField-wrapper label": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
"paddingTop": 0,
|
||||||
|
},
|
||||||
|
".ms-ChoiceFieldGroup root-133": Object {
|
||||||
|
"clear": "both",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"root": Object {
|
||||||
|
"clear": "both",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="tabs settingsSectionPart"
|
className="tabs settingsSectionPart"
|
||||||
@@ -123,7 +150,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
onValidate={[Function]}
|
onValidate={[Function]}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
step={1}
|
step={1}
|
||||||
tabIndex={0}
|
|
||||||
value="6"
|
value="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { PrimaryButton } from "@fluentui/react";
|
|
||||||
import { mount } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { SetupNoteBooksPanel } from "./SetupNotebooksPanel";
|
|
||||||
|
|
||||||
describe("Setup Notebooks Panel", () => {
|
|
||||||
it("should render Default properly", () => {
|
|
||||||
const fakeExplorer = {} as Explorer;
|
|
||||||
const props = {
|
|
||||||
explorer: fakeExplorer,
|
|
||||||
closePanel: (): void => undefined,
|
|
||||||
openNotificationConsole: (): void => undefined,
|
|
||||||
panelTitle: "",
|
|
||||||
panelDescription: "",
|
|
||||||
};
|
|
||||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render button", () => {
|
|
||||||
const fakeExplorer = {} as Explorer;
|
|
||||||
const props = {
|
|
||||||
explorer: fakeExplorer,
|
|
||||||
closePanel: (): void => undefined,
|
|
||||||
openNotificationConsole: (): void => undefined,
|
|
||||||
panelTitle: "",
|
|
||||||
panelDescription: "",
|
|
||||||
};
|
|
||||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
|
||||||
const button = wrapper.find("PrimaryButton").first();
|
|
||||||
expect(button).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Button onClick should call onCompleteSetup", () => {
|
|
||||||
const onCompleteSetupClick = jest.fn();
|
|
||||||
const wrapper = mount(<PrimaryButton onClick={onCompleteSetupClick} />);
|
|
||||||
wrapper.find("button").simulate("click");
|
|
||||||
|
|
||||||
expect(onCompleteSetupClick).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Button onKeyPress should call onCompleteSetupKeyPress", () => {
|
|
||||||
const onCompleteSetupKeyPress = jest.fn();
|
|
||||||
const wrapper = mount(<PrimaryButton onKeyPress={onCompleteSetupKeyPress} />);
|
|
||||||
wrapper.find("button").simulate("keypress");
|
|
||||||
|
|
||||||
expect(onCompleteSetupKeyPress).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { PrimaryButton } from "@fluentui/react";
|
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
|
||||||
import React, { FunctionComponent, KeyboardEvent, useState } from "react";
|
|
||||||
import { Areas, NormalizedEventKey } from "../../../Common/Constants";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
|
||||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
|
||||||
interface SetupNoteBooksPanelProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
panelTitle: string;
|
|
||||||
panelDescription: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> = ({
|
|
||||||
explorer,
|
|
||||||
panelTitle,
|
|
||||||
panelDescription,
|
|
||||||
}: SetupNoteBooksPanelProps): JSX.Element => {
|
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
|
||||||
|
|
||||||
const description = panelDescription;
|
|
||||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
||||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const onCompleteSetupClick = async () => {
|
|
||||||
await setupNotebookWorkspace();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCompleteSetupKeyPress = async (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
||||||
if (event.key === " " || event.key === NormalizedEventKey.Enter) {
|
|
||||||
await setupNotebookWorkspace();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupNotebookWorkspace = async (): Promise<void> => {
|
|
||||||
if (!explorer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, {
|
|
||||||
dataExplorerArea: Areas.ContextualPane,
|
|
||||||
paneTitle: panelTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
const clear = NotificationConsoleUtils.logConsoleProgress("Creating a new default notebook workspace");
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoadingTrue();
|
|
||||||
await createOrUpdate(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
"default"
|
|
||||||
);
|
|
||||||
explorer.refreshExplorer();
|
|
||||||
|
|
||||||
closeSidePanel();
|
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.CreateNotebookWorkspace,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Areas.ContextualPane,
|
|
||||||
paneTitle: panelTitle,
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
NotificationConsoleUtils.logConsoleInfo("Successfully created a default notebook workspace for the account");
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.CreateNotebookWorkspace,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Areas.ContextualPane,
|
|
||||||
paneTitle: panelTitle,
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
setErrorMessage(`Failed to setup a default notebook workspace: ${errorMessage}`);
|
|
||||||
setShowErrorDetails(true);
|
|
||||||
NotificationConsoleUtils.logConsoleError(`Failed to create a default notebook workspace: ${errorMessage}`);
|
|
||||||
} finally {
|
|
||||||
setLoadingFalse();
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="panelFormWrapper">
|
|
||||||
{errorMessage && (
|
|
||||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
|
||||||
)}
|
|
||||||
<div className="panelMainContent">
|
|
||||||
<div className="pkPadding">
|
|
||||||
<div>{description}</div>
|
|
||||||
<PrimaryButton
|
|
||||||
id="completeSetupBtn"
|
|
||||||
className="btncreatecoll1 btnSetupQueries"
|
|
||||||
text="Complete Setup"
|
|
||||||
onClick={onCompleteSetupClick}
|
|
||||||
onKeyPress={onCompleteSetupKeyPress}
|
|
||||||
aria-label="Complete setup"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isLoading && <PanelLoadingScreen />}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import { TextField } from "@fluentui/react";
|
import { TextField } from "@fluentui/react";
|
||||||
|
import * as ViewModels from "Contracts/ViewModels";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React, { FormEvent, FunctionComponent, useState } from "react";
|
import React, { FormEvent, FunctionComponent, useState } from "react";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
import { useTabs } from "../../../hooks/useTabs";
|
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||||
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
|
||||||
import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
|
import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user