Compare commits
5 Commits
fabricbot-
...
create_dat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be9e179383 | ||
|
|
98640785fc | ||
|
|
4db4d8494e | ||
|
|
b783ebc834 | ||
|
|
b4d23ac913 |
@@ -21,8 +21,16 @@ src/Common/MongoUtility.ts
|
||||
src/Common/NotificationsClientBase.ts
|
||||
src/Common/QueriesClient.ts
|
||||
src/Common/Splitter.ts
|
||||
src/Config.ts
|
||||
src/Contracts/ActionContracts.ts
|
||||
src/Contracts/DataModels.ts
|
||||
src/Contracts/Diagnostics.ts
|
||||
src/Contracts/ExplorerContracts.ts
|
||||
src/Contracts/Versions.ts
|
||||
src/Contracts/ViewModels.ts
|
||||
src/Controls/Heatmap/Heatmap.test.ts
|
||||
src/Controls/Heatmap/Heatmap.ts
|
||||
src/Controls/Heatmap/HeatmapDatatypes.ts
|
||||
src/Definitions/datatables.d.ts
|
||||
src/Definitions/gif.d.ts
|
||||
src/Definitions/globals.d.ts
|
||||
@@ -36,10 +44,29 @@ src/Definitions/png.d.ts
|
||||
src/Definitions/svg.d.ts
|
||||
src/Explorer/ComponentRegisterer.test.ts
|
||||
src/Explorer/ComponentRegisterer.ts
|
||||
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
|
||||
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
|
||||
|
||||
src/Explorer/Controls/DynamicList/DynamicList.test.ts
|
||||
src/Explorer/Controls/DynamicList/DynamicListComponent.ts
|
||||
src/Explorer/Controls/Editor/EditorComponent.ts
|
||||
src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
|
||||
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
|
||||
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
|
||||
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
|
||||
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
|
||||
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
|
||||
src/Explorer/Controls/Toolbar/IToolbarAction.ts
|
||||
src/Explorer/Controls/Toolbar/IToolbarDisplayable.ts
|
||||
src/Explorer/Controls/Toolbar/IToolbarDropDown.ts
|
||||
src/Explorer/Controls/Toolbar/IToolbarItem.ts
|
||||
src/Explorer/Controls/Toolbar/IToolbarSeperator.ts
|
||||
src/Explorer/Controls/Toolbar/IToolbarToggle.ts
|
||||
src/Explorer/Controls/Toolbar/KeyCodes.ts
|
||||
src/Explorer/Controls/Toolbar/Toolbar.ts
|
||||
src/Explorer/Controls/Toolbar/ToolbarAction.ts
|
||||
src/Explorer/Controls/Toolbar/ToolbarDropDown.ts
|
||||
src/Explorer/Controls/Toolbar/ToolbarToggle.ts
|
||||
src/Explorer/Controls/Toolbar/Utilities.ts
|
||||
src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
|
||||
src/Explorer/DataSamples/ContainerSampleGenerator.ts
|
||||
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
||||
@@ -81,9 +108,17 @@ src/Explorer/Tables/DataTable/DataTableBindingManager.ts
|
||||
src/Explorer/Tables/DataTable/DataTableBuilder.ts
|
||||
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
|
||||
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
|
||||
src/Explorer/Tables/DataTable/DataTableOperations.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/Entities.ts
|
||||
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
|
||||
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
|
||||
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
|
||||
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
|
||||
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
|
||||
src/Explorer/Tables/TableDataClient.ts
|
||||
src/Explorer/Tables/TableEntityProcessor.ts
|
||||
src/Explorer/Tables/Utilities.ts
|
||||
@@ -107,10 +142,15 @@ src/Explorer/Tree/ObjectId.ts
|
||||
src/Explorer/Tree/ResourceTokenCollection.ts
|
||||
src/Explorer/Tree/StoredProcedure.ts
|
||||
src/Explorer/Tree/TreeComponents.ts
|
||||
src/Explorer/Tree/Trigger.ts
|
||||
src/Explorer/WaitsForTemplateViewModel.ts
|
||||
src/GitHub/GitHubClient.test.ts
|
||||
src/GitHub/GitHubClient.ts
|
||||
src/GitHub/GitHubConnector.ts
|
||||
src/GitHub/GitHubOAuthService.ts
|
||||
src/Index.ts
|
||||
src/Juno/JunoClient.test.ts
|
||||
src/Juno/JunoClient.ts
|
||||
src/Platform/Hosted/Authorization.ts
|
||||
src/ReactDevTools.ts
|
||||
src/Shared/Constants.ts
|
||||
@@ -130,13 +170,20 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||
src/Explorer/Controls/TreeComponent/TreeComponent.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/ReadOnlyNodePropertiesComponent.test.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
|
||||
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/contents/index.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx
|
||||
@@ -144,4 +191,6 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
|
||||
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||
__mocks__/monaco-editor.ts
|
||||
src/Explorer/Tree/ResourceTree.tsx
|
||||
src/Explorer/Tree/ResourceTree.tsx
|
||||
src/Explorer/Tree/DatabasesResourceTree.tsx
|
||||
src/Explorer/Tree/NotebooksResourceTree.tsx
|
||||
@@ -39,6 +39,7 @@ module.exports = {
|
||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"no-null/no-null": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
|
||||
eqeqeq: "error",
|
||||
|
||||
30
.github/fabricbot.json
vendored
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"tasks": [
|
||||
{
|
||||
"taskType": "trigger",
|
||||
"capabilityId": "AutoMerge",
|
||||
"subCapability": "AutoMerge",
|
||||
"version": "1.0",
|
||||
"id": "LUEPwPETV",
|
||||
"config": {
|
||||
"taskName": "Auto Merge",
|
||||
"label": "automerge",
|
||||
"minMinutesOpen": "5",
|
||||
"mergeType": "squash",
|
||||
"deleteBranches": true,
|
||||
"requireAllStatuses": true,
|
||||
"requireSpecificCheckRuns": false,
|
||||
"usePrDescriptionAsCommitMessage": true,
|
||||
"requireAllStatuses_exemptList": [
|
||||
"Azure Pipelines",
|
||||
"Dependabot",
|
||||
"GitHub Pages",
|
||||
"Check Enforcer"
|
||||
],
|
||||
"silentMode": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"userGroups": []
|
||||
}
|
||||
4
.github/workflows/ci.yml
vendored
@@ -92,11 +92,11 @@ jobs:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Upload build to preview blob storage
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
- name: Upload preview config to blob storage
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
endtoendemulator:
|
||||
|
||||
6
.vscode/launch.json
vendored
@@ -12,8 +12,7 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"--runInBand",
|
||||
"--coverage",
|
||||
"false"
|
||||
"--coverage", "false"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
@@ -27,8 +26,7 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"${fileBasenameNoExtension}",
|
||||
"--coverage",
|
||||
"false",
|
||||
"--coverage", "false",
|
||||
// "--watch",
|
||||
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
||||
// // https://github.com/facebook/jest/issues/6683
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -22,6 +22,5 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"isTerminalEnabled" : true,
|
||||
"isPhoenixEnabled" : true
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled" : false,
|
||||
"isPhoenixEnabled" : false
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,23 +0,0 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_480_185430)">
|
||||
<path d="M35.4911 6.39638L33.4106 4.31591L37.559 0.167553C37.6611 0.0654497 37.7996 0.00808785 37.944 0.00808785C38.0884 0.00808801 38.2269 0.0654482 38.329 0.167551L39.641 1.47963C39.7431 1.58173 39.8005 1.72021 39.8005 1.86461C39.8005 2.009 39.7431 2.14749 39.641 2.24959L35.4927 6.39795L35.4911 6.39638Z" fill="#32BEDD"/>
|
||||
<path d="M4.45313 33.6455L6.53988 35.7323L2.44494 39.8272C2.34367 39.9285 2.20632 39.9853 2.06311 39.9853C1.91989 39.9853 1.78254 39.9285 1.68127 39.8272L0.364478 38.5104C0.312974 38.4606 0.271905 38.401 0.243665 38.3351C0.215426 38.2692 0.20058 38.1984 0.199995 38.1267C0.19941 38.0551 0.213098 37.984 0.240258 37.9177C0.267419 37.8514 0.307509 37.7911 0.358193 37.7404L4.45313 33.6455Z" fill="#0078D4"/>
|
||||
<path d="M5.09381 25.0287C3.78099 26.3415 3.04346 28.1221 3.04346 29.9787C3.04346 31.8353 3.78099 33.6159 5.09381 34.9287C6.40664 36.2415 8.1872 36.9791 10.0438 36.9791C11.9004 36.9791 13.681 36.2415 14.9938 34.9287L18.2027 31.7176L8.30492 21.8198L5.09381 25.0287Z" fill="url(#paint0_linear_480_185430)"/>
|
||||
<path d="M17.4209 18.1581L17.6157 18.353C17.8133 18.5505 17.9242 18.8185 17.9242 19.0978C17.9242 19.3772 17.8133 19.6451 17.6157 19.8426L13.6009 23.8574L11.918 22.1745L15.9344 18.1581C16.1319 17.9606 16.3998 17.8496 16.6792 17.8496C16.9586 17.8496 17.2265 17.9606 17.424 18.1581L17.4209 18.1581Z" fill="#C3F1FF"/>
|
||||
<path d="M21.5835 22.32L21.7783 22.5149C21.9759 22.7124 22.0868 22.9803 22.0868 23.2597C22.0868 23.539 21.9759 23.807 21.7783 24.0045L17.7588 28.024L16.0759 26.3411L20.097 22.32C20.2945 22.1225 20.5624 22.0115 20.8418 22.0115C21.1212 22.0115 21.3891 22.1225 21.5866 22.32L21.5835 22.32Z" fill="#C3F1FF"/>
|
||||
<path d="M20.9363 30.0618L9.87241 18.9979C9.66673 18.7922 9.33327 18.7922 9.12759 18.9979L7.67566 20.4498C7.46999 20.6555 7.46999 20.989 7.67566 21.1946L18.7395 32.2585C18.9452 32.4642 19.2787 32.4642 19.4843 32.2585L20.9363 30.8066C21.1419 30.6009 21.1419 30.2674 20.9363 30.0618Z" fill="#5EA0EF"/>
|
||||
<path d="M34.9067 14.9711C36.2196 13.6583 36.9571 11.8777 36.9571 10.0211C36.9571 8.1645 36.2196 6.38393 34.9067 5.07111C33.5939 3.75829 31.8134 3.02075 29.9567 3.02075C28.1001 3.02075 26.3196 3.75829 25.0067 5.07111L21.7979 8.28222L31.6956 18.18L34.9067 14.9711Z" fill="#ECF4FD"/>
|
||||
<path d="M22.5828 21.8375L22.388 21.6426C22.1904 21.4451 22.0795 21.1772 22.0795 20.8978C22.0795 20.6184 22.1904 20.3505 22.388 20.153L26.4075 16.1335L28.092 17.8179L24.0677 21.8422C23.8698 22.0376 23.6025 22.1469 23.3243 22.146C23.0461 22.1451 22.7795 22.0342 22.5828 21.8375Z" fill="#ECF4FD"/>
|
||||
<path d="M18.4178 17.6802L18.2229 17.4854C18.0254 17.2878 17.9144 17.0199 17.9144 16.7406C17.9144 16.4612 18.0254 16.1933 18.2229 15.9957L22.2409 11.9778L23.9254 13.6623L19.909 17.6787C19.7115 17.8762 19.4435 17.9872 19.1642 17.9872C18.8848 17.9872 18.6169 17.8762 18.4194 17.6787L18.4178 17.6802Z" fill="#ECF4FD"/>
|
||||
<path d="M19.0642 9.93799L30.1281 21.0019C30.3338 21.2075 30.6672 21.2075 30.8729 21.0019L32.3248 19.5499C32.5305 19.3443 32.5305 19.0108 32.3248 18.8051L21.261 7.74125C21.0553 7.53557 20.7218 7.53557 20.5161 7.74125L19.0642 9.19317C18.8585 9.39885 18.8585 9.73232 19.0642 9.93799Z" fill="#ECF4FD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_480_185430" x1="10.6227" y1="21.8179" x2="10.6227" y2="36.9783" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5EA0EF"/>
|
||||
<stop offset="0.997" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_480_185430">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 34.635C24 34.8143 23.9647 34.9918 23.8961 35.1574C23.8275 35.323 23.727 35.4734 23.6002 35.6002C23.4734 35.727 23.323 35.8275 23.1574 35.8961C22.9918 35.9647 22.8143 36 22.635 36H1.365C1.18575 36 1.00825 35.9647 0.842637 35.8961C0.677028 35.8275 0.526551 35.727 0.399799 35.6002C0.273047 35.4734 0.172502 35.323 0.103904 35.1574C0.0353068 34.9918 0 34.8143 0 34.635L0 13.365C0 13.003 0.143812 12.6558 0.399799 12.3998C0.655786 12.1438 1.00298 12 1.365 12H22.635C22.997 12 23.3442 12.1438 23.6002 12.3998C23.8562 12.6558 24 13.003 24 13.365V34.635Z" fill="#005BA1"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 28.635C30 28.997 29.8562 29.3442 29.6002 29.6002C29.3442 29.8562 28.997 30 28.635 30H7.365C7.00298 30 6.65579 29.8562 6.3998 29.6002C6.14381 29.3442 6 28.997 6 28.635V7.365C6 7.00298 6.14381 6.65579 6.3998 6.3998C6.65579 6.14381 7.00298 6 7.365 6H28.635C28.997 6 29.3442 6.14381 29.6002 6.3998C29.8562 6.65579 30 7.00298 30 7.365V28.635Z" fill="#5EA0EF"/>
|
||||
<path d="M22.635 12H6V28.635C6 28.997 6.14381 29.3442 6.3998 29.6002C6.65579 29.8562 7.00298 30 7.365 30H24V13.365C24 13.003 23.8562 12.6558 23.6002 12.3998C23.3442 12.1438 22.997 12 22.635 12Z" fill="#0078D4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36 22.635C36 22.8143 35.9647 22.9918 35.8961 23.1574C35.8275 23.323 35.727 23.4734 35.6002 23.6002C35.4734 23.727 35.323 23.8275 35.1574 23.8961C34.9918 23.9647 34.8143 24 34.635 24H13.365C13.003 24 12.6558 23.8562 12.3998 23.6002C12.1438 23.3442 12 22.997 12 22.635V1.365C12 1.00298 12.1438 0.655786 12.3998 0.399799C12.6558 0.143812 13.003 0 13.365 0L34.635 0C34.8143 0 34.9918 0.0353068 35.1574 0.103904C35.323 0.172502 35.4734 0.273047 35.6002 0.399799C35.727 0.526551 35.8275 0.677028 35.8961 0.842637C35.9647 1.00825 36 1.18575 36 1.365V22.635Z" fill="#E6E7E8"/>
|
||||
<path d="M22.635 12H12V22.635C12 22.997 12.1438 23.3442 12.3998 23.6002C12.6558 23.8562 13.003 24 13.365 24H24V13.365C24 13.003 23.8562 12.6558 23.6002 12.3998C23.3442 12.1438 22.997 12 22.635 12Z" fill="#BCBEC0"/>
|
||||
<path d="M28.635 6H12V12H22.635C22.997 12 23.3442 12.1438 23.6002 12.3998C23.8562 12.6558 24 13.003 24 13.365V24H30V7.365C30 7.00298 29.8562 6.65579 29.6002 6.3998C29.3442 6.14381 28.997 6 28.635 6Z" fill="#D1D3D4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 6H11V11H0V0H5V1H1V10H10V6ZM11 0V5H10V1.71094L5.35156 6.35156L4.64844 5.64844L9.28906 1H6V0H11Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 229 B |
@@ -1,23 +0,0 @@
|
||||
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30.1809 0H5.6045C5.45725 -2.06304e-07 5.31144 0.0290347 5.17541 0.0854435C5.03939 0.141852 4.91583 0.224528 4.81179 0.32874C4.70775 0.432952 4.62528 0.556655 4.5691 0.692771C4.51292 0.828887 4.48413 0.974745 4.48438 1.122V31.7149C4.48438 32.0121 4.60234 32.2972 4.81236 32.5075C5.02238 32.7178 5.30729 32.8361 5.6045 32.8365H30.1809C30.4781 32.8362 30.7631 32.7179 30.9731 32.5076C31.1832 32.2972 31.3011 32.0121 31.301 31.7149V1.122C31.301 0.824752 31.1831 0.53965 30.973 0.329288C30.763 0.118926 30.4781 0.000496739 30.1809 0V0Z" fill="url(#paint0_linear_1311_8669)"/>
|
||||
<path d="M16.1495 3.22485H2.17401C1.8837 3.22485 1.60528 3.34018 1.39999 3.54546C1.19471 3.75074 1.07939 4.02917 1.07939 4.31948V34.465C1.07815 34.6087 1.10524 34.7513 1.15912 34.8845C1.21299 35.0178 1.2926 35.1391 1.39338 35.2416C1.49416 35.3441 1.61414 35.4257 1.74648 35.4818C1.87881 35.5379 2.0209 35.5674 2.16464 35.5686H26.3926C26.6829 35.5686 26.9614 35.4533 27.1667 35.248C27.3719 35.0427 27.4873 34.7643 27.4873 34.474V14.5202C27.4874 14.3764 27.4591 14.234 27.4042 14.1011C27.3492 13.9682 27.2686 13.8475 27.1669 13.7457C27.0653 13.644 26.9446 13.5633 26.8117 13.5082C26.6788 13.4532 26.5364 13.4249 26.3926 13.4249H18.3549C18.0642 13.422 17.7865 13.3045 17.5819 13.098C17.3774 12.8915 17.2626 12.6126 17.2625 12.322V4.33185C17.2628 4.03998 17.1477 3.75982 16.9423 3.55245C16.7369 3.34508 16.4579 3.22733 16.166 3.22485H16.1495Z" fill="white"/>
|
||||
<path d="M16.175 3.16357H1.99513C1.69791 3.16397 1.41301 3.28232 1.20299 3.49262C0.992965 3.70292 0.875 3.98799 0.875 4.2852V34.8781C0.875 35.1753 0.992953 35.4604 1.20296 35.6708C1.41297 35.8812 1.69788 35.9996 1.99513 36.0001H26.5715C26.8687 35.9996 27.1537 35.8812 27.3637 35.6708C27.5737 35.4604 27.6916 35.1753 27.6916 34.8781V14.6307C27.6916 14.3336 27.5736 14.0487 27.3635 13.8387C27.1535 13.6286 26.8686 13.5106 26.5715 13.5106H18.4134C18.1163 13.5106 17.8314 13.3926 17.6213 13.1825C17.4113 12.9724 17.2933 12.6875 17.2933 12.3905V4.2852C17.2924 3.98858 17.1744 3.70432 16.9649 3.49426C16.7555 3.2842 16.4716 3.16535 16.175 3.16357Z" fill="url(#paint1_linear_1311_8669)"/>
|
||||
<path d="M27.2629 13.7335L16.9065 3.4082V11.8213C16.9035 12.3253 17.1007 12.8097 17.4548 13.1684C17.8088 13.527 18.2907 13.7304 18.7947 13.7338L27.2629 13.7335Z" fill="#83B9F9"/>
|
||||
<path d="M17.744 16.4092H4.76108C4.47196 16.4092 4.23608 16.5543 4.23608 16.7332V17.5323C4.23608 17.7112 4.47046 17.8559 4.76108 17.8559H17.744C18.0331 17.8559 18.269 17.7112 18.269 17.5323V16.7332C18.2675 16.5543 18.0331 16.4092 17.744 16.4092Z" fill="#83B9F9"/>
|
||||
<path d="M17.744 20.7498H4.76108C4.47196 20.7498 4.23608 20.8945 4.23608 21.0734V21.8725C4.23608 22.0514 4.47046 22.1965 4.76108 22.1965H17.744C18.0331 22.1965 18.269 22.0514 18.269 21.8725V21.0749C18.2675 20.8945 18.0331 20.7498 17.744 20.7498Z" fill="#83B9F9"/>
|
||||
<path d="M17.744 25.0906H4.76108C4.47196 25.0906 4.23608 25.2353 4.23608 25.4142V26.2126C4.23608 26.3915 4.47046 26.5366 4.76108 26.5366H17.744C18.0331 26.5366 18.269 26.3915 18.269 26.2126V25.4142C18.2675 25.2353 18.0331 25.0906 17.744 25.0906Z" fill="#83B9F9"/>
|
||||
<path d="M12.4729 29.4312H4.90167C4.53492 29.4312 4.23755 29.5759 4.23755 29.7548V30.5539C4.23755 30.7328 4.53492 30.8779 4.90167 30.8779H12.4729C12.8397 30.8779 13.137 30.7328 13.137 30.5539V29.7548C13.1374 29.5759 12.8397 29.4312 12.4729 29.4312Z" fill="#83B9F9"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1311_8669" x1="17.8925" y1="2.19225" x2="17.8925" y2="35.1225" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DCDCDC"/>
|
||||
<stop offset="1" stop-color="#AAAAAA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1311_8669" x1="14.2831" y1="5.35582" x2="14.2831" y2="38.2857" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D7"/>
|
||||
<stop offset="0.327" stop-color="#0076D4"/>
|
||||
<stop offset="0.576" stop-color="#0071CA"/>
|
||||
<stop offset="0.799" stop-color="#0068BA"/>
|
||||
<stop offset="1" stop-color="#005BA4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6457 18.9089H0.67782C0.520018 18.9215 0.361625 18.8893 0.235445 18.819C0.109264 18.7487 0.0249634 18.6456 0 18.5312C0.000766863 18.4748 0.0212497 18.4196 0.0595033 18.3706L14.4179 0.229509C14.4859 0.156313 14.5784 0.0970502 14.6866 0.0573734C14.7949 0.0176966 14.9152 -0.00107025 15.0362 0.00286318H29.1877C29.3456 -0.0102086 29.5043 0.0218054 29.6306 0.0922084C29.757 0.162611 29.8411 0.26595 29.8655 0.380607C29.8655 0.463721 29.823 0.54385 29.7465 0.605364L12.8941 14.7991H29.3222C29.48 14.7865 29.6384 14.8187 29.7646 14.889C29.8907 14.9593 29.975 15.0624 30 15.1768C29.998 15.2265 29.9814 15.2752 29.9516 15.3198C29.9217 15.3645 29.8791 15.4039 29.8267 15.4356L2.51725 33.5994C2.25854 33.6957 0.447568 34.6608 1.33236 33.1952L12.6457 18.9089Z" fill="url(#paint0_radial_480_182017)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_480_182017" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.0039 17.0718) scale(23.6442 13.4446)">
|
||||
<stop offset="0.196" stop-color="#FFD70F"/>
|
||||
<stop offset="0.438" stop-color="#FFCB12"/>
|
||||
<stop offset="0.873" stop-color="#FEAC19"/>
|
||||
<stop offset="1" stop-color="#FEA11B"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,8 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path
|
||||
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
|
||||
transform="scale(0.5)" fill="#000" stroke="#000">
|
||||
<path
|
||||
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
|
||||
transform="scale(0.5)"
|
||||
fill="#000"
|
||||
stroke="#CCC"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 503 B |
@@ -37,8 +37,8 @@ module.exports = {
|
||||
global: {
|
||||
branches: 25,
|
||||
functions: 25,
|
||||
lines: 29,
|
||||
statements: 29,
|
||||
lines: 30,
|
||||
statements: 30,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -129,8 +129,6 @@ module.exports = {
|
||||
// The test environment that will be used for testing
|
||||
// testEnvironment: "jest-environment-jsdom",
|
||||
|
||||
modulePaths: ["node_modules", "<rootDir>/src"],
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
|
||||
@@ -2077,7 +2077,7 @@ a:link {
|
||||
.resourceTreeAndTabs {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: clip;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -2245,7 +2245,7 @@ a:link {
|
||||
}
|
||||
|
||||
.refreshColHeader {
|
||||
padding: 3px 6px 10px 0px !important;
|
||||
padding: 3px 6px 6px 6px;
|
||||
}
|
||||
|
||||
.refreshColHeader:hover {
|
||||
@@ -2357,8 +2357,6 @@ a:link {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -2834,8 +2832,6 @@ a:link {
|
||||
|
||||
#explorerNotificationConsole {
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.uniqueIndexesContainer {
|
||||
@@ -2869,39 +2865,31 @@ a:link {
|
||||
}
|
||||
}
|
||||
|
||||
.settingsSection {
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
margin-right: 24px;
|
||||
padding: @MediumSpace 0px;
|
||||
settings-pane {
|
||||
.settingsSection {
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
margin-right: 24px;
|
||||
padding: @MediumSpace 0px;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
&:first-child {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settingsSectionPart {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.settingsSectionPart {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.settingsSectionLabel {
|
||||
margin-bottom: @DefaultSpace;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.settingsSectionLabel {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
|
||||
.pageOptionsPart {
|
||||
padding-bottom: @MediumSpace;
|
||||
}
|
||||
|
||||
.legendLabel {
|
||||
border-bottom: 0px;
|
||||
width: auto;
|
||||
font-size: @mediumFontSize;
|
||||
display: inline !important;
|
||||
float: left;
|
||||
.pageOptionsPart {
|
||||
padding-bottom: @MediumSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32195
package-lock.json
generated
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.16.1",
|
||||
"@azure/cosmos": "3.10.5",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.2.1",
|
||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||
@@ -91,7 +91,6 @@
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"react-splitter-layout": "4.0.0",
|
||||
"react-youtube": "9.0.1",
|
||||
"redux": "4.0.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
@@ -131,7 +130,6 @@
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/youtube-player": "5.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "4.22.0",
|
||||
"@typescript-eslint/parser": "4.22.0",
|
||||
"@webpack-cli/serve": "1.5.2",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
|
||||
"deploy": "az webapp up -n cosmos-explorer-preview --subscription cosmosdb-portalteam-generaldemo -g stfaul",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -15,4 +15,4 @@
|
||||
"http-proxy-middleware": "^1.1.0",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
{
|
||||
"databaseId": "SampleDB",
|
||||
"offerThroughput": 400,
|
||||
"databaseLevelThroughput": false,
|
||||
"collectionId": "Persons",
|
||||
"createNewDatabase": true,
|
||||
"partitionKey": { "kind": "Hash", "paths": ["/firstname"], "version": 1 },
|
||||
"data": [
|
||||
{ "address": "2007, NE 37TH PL" },
|
||||
{ "address": "11635, SE MAY CREEK PARK DR" },
|
||||
{ "address": "8923, 133RD AVE SE" },
|
||||
{ "address": "1124, N 33RD ST" },
|
||||
{ "address": "4288, 131ST PL SE" },
|
||||
{ "address": "10900, SE 66TH ST" },
|
||||
{ "address": "6260, 139TH AVE NE" },
|
||||
{ "address": "13427, NE SPRING BLVD" },
|
||||
{ "address": "13812, NE SPRING BLVD" },
|
||||
{ "address": "5029, 159TH PL SE" },
|
||||
{ "address": "8604, 117TH AVE SE" },
|
||||
{ "address": "1561, 139TH LN NE" },
|
||||
{ "address": "1575, 139TH CT NE" },
|
||||
{ "address": "13901, NE 15TH CT" },
|
||||
{ "address": "16365, NE 12TH PL" },
|
||||
{ "address": "12226, NE 37TH ST" },
|
||||
{ "address": "4021, 129TH CT SE" },
|
||||
{ "address": "1455, 159TH PL NE" },
|
||||
{ "address": "15825, NE 14TH RD" },
|
||||
{ "address": "1418, 157TH CT NE" },
|
||||
{ "address": "889, 131ST PL NE" }
|
||||
{
|
||||
"firstname": "Eva",
|
||||
"age": 44
|
||||
},
|
||||
{
|
||||
"firstname": "Véronique",
|
||||
"age": 50
|
||||
},
|
||||
{
|
||||
"firstname": "亜妃子",
|
||||
"age": 5
|
||||
},
|
||||
{
|
||||
"firstname": "John",
|
||||
"age": 23
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import { userContext } from "../UserContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface CollapsedResourceTreeProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
@@ -12,21 +11,6 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
toggleLeftPaneExpanded,
|
||||
isLeftPaneExpanded,
|
||||
}: CollapsedResourceTreeProps): JSX.Element => {
|
||||
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
|
||||
useEffect(() => {
|
||||
if (focusButton.current) {
|
||||
focusButton.current.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
toggleLeftPaneExpanded();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
||||
<div className="main-nav nav">
|
||||
@@ -37,14 +21,11 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Expand Tree"
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
ref={focusButton}
|
||||
>
|
||||
<span className="leftarrowCollapsed">
|
||||
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}>
|
||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||
</span>
|
||||
<span className="collectionCollapsed">
|
||||
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
|
||||
<span>{userContext.apiType} API</span>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -96,10 +96,6 @@ export class Flights {
|
||||
public static readonly AutoscaleTest = "autoscaletest";
|
||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||
public static readonly PhoenixNotebooks = "phoenixnotebooks";
|
||||
public static readonly PhoenixFeatures = "phoenixfeatures";
|
||||
public static readonly NotebooksDownBanner = "notebooksdownbanner";
|
||||
public static readonly PublicGallery = "publicgallery";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
@@ -341,23 +337,6 @@ export enum ConflictOperationType {
|
||||
Delete = "delete",
|
||||
}
|
||||
|
||||
export enum ConnectionStatusType {
|
||||
Connect = "Connect",
|
||||
Connecting = "Connecting",
|
||||
Connected = "Connected",
|
||||
Failed = "Connection Failed",
|
||||
Reconnect = "Reconnect",
|
||||
}
|
||||
|
||||
export enum ContainerStatusType {
|
||||
Active = "Active",
|
||||
Disconnected = "Disconnected",
|
||||
}
|
||||
|
||||
export enum PoolIdType {
|
||||
DefaultPoolId = "default",
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
@@ -367,37 +346,15 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 60000;
|
||||
public static readonly containerStatusHeartbeatDelayMs = 30000;
|
||||
public static readonly heartbeatDelayMs = 5000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
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 autoSaveIntervalMs = 120000;
|
||||
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
||||
public static readonly mongoShellTemporarilyDownMsg =
|
||||
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||
public static readonly cassandraShellTemporarilyDownMsg =
|
||||
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||
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 {
|
||||
@@ -418,11 +375,3 @@ export class TerminalQueryParams {
|
||||
public static readonly SubscriptionId = "subscriptionId";
|
||||
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,4 +1,4 @@
|
||||
import { ResourceType } from "@azure/cosmos";
|
||||
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
|
||||
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -7,7 +8,7 @@ import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||
|
||||
const _global = typeof self === "undefined" ? window : self;
|
||||
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
|
||||
@@ -18,13 +19,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (userContext.masterKey) {
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
@@ -76,21 +77,10 @@ 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;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
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
|
||||
key: userContext.masterKey,
|
||||
@@ -99,7 +89,6 @@ export function client(): Cosmos.CosmosClient {
|
||||
enableEndpointDiscovery: false,
|
||||
},
|
||||
userAgentSuffix: "Azure Portal",
|
||||
defaultHeaders: _defaultHeaders,
|
||||
};
|
||||
|
||||
if (configContext.PROXY_PATH !== undefined) {
|
||||
|
||||
@@ -3,16 +3,8 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@@ -25,12 +17,12 @@ const fetchMock = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const partitionKeyProperties = ["pk"];
|
||||
const partitionKeyProperty = "pk";
|
||||
|
||||
const collection = {
|
||||
id: () => "testCollection",
|
||||
rid: "testCollectionrid",
|
||||
partitionKeyProperties,
|
||||
partitionKeyProperty,
|
||||
partitionKey: {
|
||||
paths: ["/pk"],
|
||||
kind: "Hash",
|
||||
@@ -41,7 +33,7 @@ const collection = {
|
||||
const documentId = ({
|
||||
partitionKeyHeader: () => "[]",
|
||||
self: "db/testDB/db/testCollection/docs/testId",
|
||||
partitionKeyProperties,
|
||||
partitionKeyProperty,
|
||||
partitionKey: {
|
||||
paths: ["/pk"],
|
||||
kind: "Hash",
|
||||
@@ -236,12 +228,13 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
const endpoint = getEndpoint();
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
const endpoint = getEndpoint("https://localhost:1234");
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
const endpoint = getEndpoint();
|
||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||
});
|
||||
|
||||
@@ -249,35 +242,8 @@ describe("MongoProxyClient", () => {
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
});
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
const endpoint = getEndpoint();
|
||||
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,13 +1,11 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import queryString from "querystring";
|
||||
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
@@ -76,11 +74,11 @@ export function queryDocuments(
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
||||
? collection.partitionKeyProperties?.[0]
|
||||
? collection.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||
const endpoint = getEndpoint() || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -139,12 +137,11 @@ export function readDocument(
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "GET",
|
||||
@@ -184,7 +181,7 @@ export function createDocument(
|
||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
const endpoint = getEndpoint();
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||
@@ -225,10 +222,10 @@ export function updateDocument(
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
const endpoint = getEndpoint();
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -266,10 +263,10 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
const endpoint = getEndpoint();
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -312,7 +309,7 @@ export function createMongoCollectionWithProxy(
|
||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
const endpoint = getEndpoint();
|
||||
|
||||
return window
|
||||
.fetch(
|
||||
@@ -336,18 +333,8 @@ export function createMongoCollectionWithProxy(
|
||||
});
|
||||
}
|
||||
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
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";
|
||||
export function getEndpoint(): string {
|
||||
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
||||
@@ -149,10 +149,10 @@ export class QueriesClient {
|
||||
const documentId = new DocumentId(
|
||||
{
|
||||
partitionKey: QueriesClient.PartitionKey,
|
||||
partitionKeyProperties: ["id"],
|
||||
partitionKeyProperty: "id",
|
||||
} as DocumentsTab,
|
||||
query,
|
||||
[query.queryName]
|
||||
query.queryName
|
||||
); // TODO: Remove DocumentId's dependency on DocumentsTab
|
||||
const options: any = { partitionKey: query.resourceId };
|
||||
return deleteDocument(queriesCollection, documentId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||
import { AuthType } from "../AuthType";
|
||||
@@ -6,7 +6,6 @@ import Explorer from "../Explorer/Explorer";
|
||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||
import { userContext } from "../UserContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface ResourceTreeContainerProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
@@ -19,22 +18,6 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
isLeftPaneExpanded,
|
||||
container,
|
||||
}: ResourceTreeContainerProps): JSX.Element => {
|
||||
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLeftPaneExpanded) {
|
||||
if (focusButton.current) {
|
||||
focusButton.current.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
toggleLeftPaneExpanded();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||
{/* Collections Window - - Start */}
|
||||
@@ -60,11 +43,9 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
id="expandToggleLeftPaneButton"
|
||||
role="button"
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
tabIndex={0}
|
||||
aria-label="Collapse Tree"
|
||||
title="Collapse Tree"
|
||||
ref={focusButton}
|
||||
>
|
||||
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
||||
</span>
|
||||
@@ -73,8 +54,6 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
</div>
|
||||
{userContext.authType === AuthType.ResourceToken ? (
|
||||
<ResourceTokenTree />
|
||||
) : userContext.features.enableKoResourceTree ? (
|
||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||
) : (
|
||||
<ResourceTree container={container} />
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
||||
return (
|
||||
<span>
|
||||
<TooltipHost content={children}>
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||
import { ContainerResponse, DatabaseResponse } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DatabaseRequest, DatabaseResponse } from "@azure/cosmos";
|
||||
import { DatabaseResponse } from "@azure/cosmos";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
|
||||
const entityName: string = getEntityName();
|
||||
@@ -13,7 +13,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.delete();
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { Item, RequestOptions } from "@azure/cosmos";
|
||||
import { Item } from "@azure/cosmos";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const options: RequestOptions =
|
||||
documentId.partitionKey.kind === "MultiHash"
|
||||
? {
|
||||
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
||||
}
|
||||
: {};
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
// use undefined if the partitionKeyValue is empty
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.read(options);
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.read();
|
||||
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readOffers } from "./readOffers";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { ContainerDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Item, RequestOptions } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { Item } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const updateDocument = async (
|
||||
collection: CollectionBase,
|
||||
@@ -16,17 +15,11 @@ export const updateDocument = async (
|
||||
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const options: RequestOptions =
|
||||
documentId.partitionKey.kind === "MultiHash"
|
||||
? {
|
||||
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
||||
}
|
||||
: {};
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.replace(newDocument, options);
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.replace(newDocument);
|
||||
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
return response?.resource;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedArmEndpoints,
|
||||
allowedBackendEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointValidation";
|
||||
|
||||
export enum Platform {
|
||||
Portal = "Portal",
|
||||
Hosted = "Hosted",
|
||||
@@ -20,7 +6,7 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
allowedParentFrameOrigins: string[];
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
@@ -37,12 +23,10 @@ export interface ConfigContext {
|
||||
PROXY_PATH?: string;
|
||||
JUNO_ENDPOINT: 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.
|
||||
isTerminalEnabled: boolean;
|
||||
isPhoenixEnabled: boolean;
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
allowedJunoOrigins: string[];
|
||||
msalRedirectURI?: string;
|
||||
}
|
||||
|
||||
@@ -52,11 +36,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||
], // Webpack injects this at build time
|
||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
|
||||
],
|
||||
// Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
@@ -67,12 +52,16 @@ let configContext: Readonly<ConfigContext> = {
|
||||
GRAPH_API_VERSION: "1.6",
|
||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||
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
|
||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
allowedJunoOrigins: [
|
||||
"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 {
|
||||
@@ -83,50 +72,6 @@ export function resetConfigContext(): 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);
|
||||
}
|
||||
|
||||
@@ -150,8 +95,18 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
});
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const { ...externalConfig } = await response.json();
|
||||
updateConfigContext(externalConfig);
|
||||
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
|
||||
Object.assign(configContext, externalConfig);
|
||||
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
|
||||
});
|
||||
}
|
||||
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to parse json in config file");
|
||||
console.error(error);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||
|
||||
export interface DatabaseAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -7,11 +5,6 @@ export interface DatabaseAccount {
|
||||
type: string;
|
||||
kind: string;
|
||||
properties: DatabaseAccountExtendedProperties;
|
||||
systemData?: DatabaseAccountSystemData;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountSystemData {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountExtendedProperties {
|
||||
@@ -31,8 +24,6 @@ export interface DatabaseAccountExtendedProperties {
|
||||
isVirtualNetworkFilterEnabled?: boolean;
|
||||
ipRules?: IpRule[];
|
||||
privateEndpointConnections?: unknown[];
|
||||
capacity?: { totalThroughputLimit: number };
|
||||
locations?: DatabaseAccountResponseLocation[];
|
||||
}
|
||||
|
||||
export interface DatabaseAccountResponseLocation {
|
||||
@@ -433,57 +424,6 @@ export interface OperationStatus {
|
||||
export interface NotebookWorkspaceConnectionInfo {
|
||||
authToken: string;
|
||||
notebookServerEndpoint: string;
|
||||
forwardingId: string;
|
||||
}
|
||||
|
||||
export interface ContainerInfo {
|
||||
durationLeftInMinutes: number;
|
||||
phoenixServerInfo: NotebookWorkspaceConnectionInfo;
|
||||
status: ContainerStatusType;
|
||||
}
|
||||
|
||||
export interface IProvisionData {
|
||||
cosmosEndpoint: string;
|
||||
poolId: string;
|
||||
}
|
||||
|
||||
export interface IContainerData {
|
||||
forwardingId: string;
|
||||
}
|
||||
|
||||
export interface IDbAccountAllow {
|
||||
status: number;
|
||||
message?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface IResponse<T> {
|
||||
status: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface IPhoenixError {
|
||||
message: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IMaxAllocationTimeExceeded extends IPhoenixError {
|
||||
earliestAllocationTimestamp: string;
|
||||
maxAllocationTimePerDayPerUserInMinutes: string;
|
||||
}
|
||||
|
||||
export interface IMaxDbAccountsPerUserExceeded extends IPhoenixError {
|
||||
maxSimultaneousConnectionsPerUser: string;
|
||||
}
|
||||
|
||||
export interface IMaxUsersPerDbAccountExceeded extends IPhoenixError {
|
||||
maxSimultaneousUsersPerDbAccount: string;
|
||||
}
|
||||
|
||||
export interface IPhoenixConnectionInfoResult {
|
||||
readonly authToken?: string;
|
||||
readonly phoenixServiceUrl?: string;
|
||||
readonly forwardingId?: string;
|
||||
}
|
||||
|
||||
export interface NotebookWorkspaceFeedResponse {
|
||||
@@ -556,19 +496,3 @@ export interface MemoryUsageInfo {
|
||||
freeKB: number;
|
||||
totalKB: number;
|
||||
}
|
||||
|
||||
export interface ContainerConnectionInfo {
|
||||
status: ConnectionStatusType;
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
||||
export enum PhoenixErrorType {
|
||||
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
|
||||
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
|
||||
MaxUsersPerDbAccountExceeded = "MaxUsersPerDbAccountExceeded",
|
||||
AllocationValidationResult = "AllocationValidationResult",
|
||||
RegionNotServicable = "RegionNotServicable",
|
||||
SubscriptionNotAllowed = "SubscriptionNotAllowed",
|
||||
UnknownError = "UnknownError",
|
||||
PhoenixFlightFallback = "PhoenixFlightFallback",
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ export enum MessageTypes {
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
CloseTab,
|
||||
OpenQuickstartBlade,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -86,7 +86,6 @@ export interface Database extends TreeNode {
|
||||
offer: ko.Observable<DataModels.Offer>;
|
||||
isDatabaseExpanded: ko.Observable<boolean>;
|
||||
isDatabaseShared: ko.Computed<boolean>;
|
||||
isSampleDB?: boolean;
|
||||
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
|
||||
@@ -107,13 +106,12 @@ export interface CollectionBase extends TreeNode {
|
||||
self: string;
|
||||
rawDataModel: DataModels.Collection;
|
||||
partitionKey: DataModels.PartitionKey;
|
||||
partitionKeyProperties: string[];
|
||||
partitionKeyPropertyHeaders: string[];
|
||||
partitionKeyProperty: string;
|
||||
partitionKeyPropertyHeader: string;
|
||||
id: ko.Observable<string>;
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
children: ko.ObservableArray<TreeNode>;
|
||||
isCollectionExpanded: ko.Observable<boolean>;
|
||||
isSampleCollection?: boolean;
|
||||
|
||||
onDocumentDBDocumentsClick(): void;
|
||||
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("The Heatmap Control", () => {
|
||||
};
|
||||
|
||||
let heatmap: Heatmap;
|
||||
const theme: PortalTheme = 1;
|
||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||
let theme: PortalTheme = 1;
|
||||
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
|
||||
|
||||
describe("drawHeatmap rendering", () => {
|
||||
beforeEach(() => {
|
||||
@@ -100,7 +100,7 @@ describe("iframe rendering when there is no data", () => {
|
||||
});
|
||||
|
||||
it("should show a no data message with a dark theme", () => {
|
||||
const data = {
|
||||
let data = {
|
||||
data: {
|
||||
signature: "pcIframe",
|
||||
data: {
|
||||
@@ -111,7 +111,7 @@ describe("iframe rendering when there is no data", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
|
||||
document.body.innerHTML = divElement;
|
||||
|
||||
handleMessage(data as MessageEvent);
|
||||
@@ -120,7 +120,7 @@ describe("iframe rendering when there is no data", () => {
|
||||
});
|
||||
|
||||
it("should show a no data message with a white theme", () => {
|
||||
const data = {
|
||||
let data = {
|
||||
data: {
|
||||
signature: "pcIframe",
|
||||
data: {
|
||||
@@ -131,7 +131,7 @@ describe("iframe rendering when there is no data", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
|
||||
document.body.innerHTML = divElement;
|
||||
|
||||
handleMessage(data as MessageEvent);
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Heatmap {
|
||||
}
|
||||
}
|
||||
|
||||
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
|
||||
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color: string = "#838383"): FontSettings {
|
||||
return {
|
||||
family: StyleConstants.DataExplorerFont,
|
||||
size,
|
||||
@@ -78,9 +78,9 @@ export class Heatmap {
|
||||
// go thru all rows and create 2d matrix for heatmap...
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
output.yAxisPoints.push(rows[i]);
|
||||
const dataPoints: number[] = [];
|
||||
let dataPoints: number[] = [];
|
||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
||||
const row: PartitionTimeStampToData = data[rows[i]];
|
||||
let row: PartitionTimeStampToData = data[rows[i]];
|
||||
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
||||
}
|
||||
output.dataPoints.push(dataPoints);
|
||||
@@ -193,7 +193,7 @@ export class Heatmap {
|
||||
this._getLayoutSettings(),
|
||||
this._getChartDisplaySettings()
|
||||
);
|
||||
const plotDiv: any = document.getElementById(Heatmap.elementId);
|
||||
let plotDiv: any = document.getElementById(Heatmap.elementId);
|
||||
plotDiv.on("plotly_click", (data: any) => {
|
||||
let timeSelected: string = data.points[0].x;
|
||||
timeSelected = timeSelected.replace(" ", "T");
|
||||
@@ -205,7 +205,7 @@ export class Heatmap {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const output = [];
|
||||
let output = [];
|
||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
||||
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked({ databaseId }),
|
||||
onClick: () => container.onNewCollectionClicked(databaseId),
|
||||
label: `New ${getCollectionName()}`,
|
||||
},
|
||||
];
|
||||
@@ -83,6 +83,7 @@ export const createCollectionContextMenuButton = (
|
||||
|
||||
items.push({
|
||||
iconSrc: HostedTerminalIcon,
|
||||
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Icon, Label, Stack } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
|
||||
export interface CollapsibleSectionProps {
|
||||
@@ -31,13 +30,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyPress = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
this.toggleCollapsed();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
@@ -47,11 +39,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
verticalAlign="center"
|
||||
tokens={accordionStackTokens}
|
||||
onClick={this.toggleCollapsed}
|
||||
onKeyPress={this.onKeyPress}
|
||||
tabIndex={0}
|
||||
aria-name="Advanced"
|
||||
role="button"
|
||||
aria-expanded={this.state.isExpanded}
|
||||
>
|
||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||
<Label>{this.props.title}</Label>
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
exports[`CollapsibleSectionComponent renders 1`] = `
|
||||
<Fragment>
|
||||
<Stack
|
||||
aria-expanded={true}
|
||||
aria-name="Advanced"
|
||||
className="collapsibleSection"
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
|
||||
@@ -30,7 +30,6 @@ export interface DialogState {
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
contentHtml?: JSX.Element,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean
|
||||
@@ -59,7 +58,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
contentHtml?: JSX.Element,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean
|
||||
@@ -78,7 +76,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
get().closeDialog();
|
||||
onCancel && onCancel();
|
||||
},
|
||||
contentHtml,
|
||||
choiceGroupProps,
|
||||
textFieldProps,
|
||||
primaryButtonDisabled,
|
||||
@@ -127,7 +124,6 @@ export interface DialogProps {
|
||||
type?: DialogType;
|
||||
showCloseButton?: boolean;
|
||||
onDismiss?: () => void;
|
||||
contentHtml?: JSX.Element;
|
||||
}
|
||||
|
||||
const DIALOG_MIN_WIDTH = "400px";
|
||||
@@ -154,7 +150,6 @@ export const Dialog: FC = () => {
|
||||
type,
|
||||
showCloseButton,
|
||||
onDismiss,
|
||||
contentHtml,
|
||||
} = props || {};
|
||||
|
||||
const dialogProps: IDialogProps = {
|
||||
@@ -186,7 +181,8 @@ export const Dialog: FC = () => {
|
||||
text: secondaryButtonText,
|
||||
onClick: onSecondaryButtonClick,
|
||||
}
|
||||
: undefined;
|
||||
: {};
|
||||
|
||||
return visible ? (
|
||||
<FluentDialog {...dialogProps}>
|
||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||
@@ -196,7 +192,6 @@ export const Dialog: FC = () => {
|
||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
)}
|
||||
{contentHtml}
|
||||
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||
<DialogFooter>
|
||||
<PrimaryButton {...primaryButtonProps} />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as UrlUtility from "../../../Common/UrlUtility";
|
||||
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||
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 { 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 {
|
||||
container: Explorer;
|
||||
@@ -27,6 +27,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
||||
private static readonly ButtonText = "Add";
|
||||
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
|
||||
private static readonly TextFieldErrorMessage = "Invalid url";
|
||||
private static readonly DefaultBranchName = "master";
|
||||
|
||||
constructor(props: AddRepoComponentProps) {
|
||||
super(props);
|
||||
@@ -77,7 +78,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
||||
});
|
||||
let enteredUrl = this.state.textFieldValue;
|
||||
if (enteredUrl.indexOf("/tree/") === -1) {
|
||||
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/`);
|
||||
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`);
|
||||
}
|
||||
|
||||
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
|
||||
@@ -92,7 +93,11 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
||||
const item: RepoListItem = {
|
||||
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
||||
repo,
|
||||
branches: repoInfo.branch ? [{ name: repoInfo.branch }] : [],
|
||||
branches: [
|
||||
{
|
||||
name: repoInfo.branch,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -24,11 +24,11 @@ import { RepoListItem } from "./GitHubReposComponent";
|
||||
import {
|
||||
BranchesDropdownCheckboxStyles,
|
||||
BranchesDropdownOptionContainerStyle,
|
||||
BranchesDropdownStyles,
|
||||
BranchesDropdownWidth,
|
||||
ReposListBranchesColumnWidth,
|
||||
ReposListCheckboxStyles,
|
||||
ReposListRepoColumnMinWidth,
|
||||
ReposListBranchesColumnWidth,
|
||||
BranchesDropdownWidth,
|
||||
BranchesDropdownStyles,
|
||||
} from "./GitHubStyleConstants";
|
||||
|
||||
export interface ReposListComponentProps {
|
||||
@@ -44,7 +44,6 @@ export interface BranchesProps {
|
||||
lastPageInfo?: IGitHubPageInfo;
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
defaultBranchName: string;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
||||
private static readonly BranchesColumnName = "Branches";
|
||||
private static readonly LoadingText = "Loading...";
|
||||
private static readonly LoadMoreText = "Load more";
|
||||
private static readonly DefaultBranchNames = "master/main";
|
||||
private static readonly DefaultBranchName = "master";
|
||||
private static readonly FooterIndex = -1;
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -156,10 +155,6 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
||||
}
|
||||
|
||||
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) => ({
|
||||
key: branch.name,
|
||||
text: branch.name,
|
||||
@@ -203,7 +198,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
||||
const dropdownProps: IDropdownProps = {
|
||||
styles: BranchesDropdownStyles,
|
||||
options: [],
|
||||
placeholder: ReposListComponent.DefaultBranchNames,
|
||||
placeholder: ReposListComponent.DefaultBranchName,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
@@ -277,7 +272,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
||||
styles: ReposListCheckboxStyles,
|
||||
onChange: () => {
|
||||
const repoListItem = { ...item };
|
||||
repoListItem.branches = [];
|
||||
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
|
||||
this.props.pinRepo(repoListItem);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
.input-type-head-text-field {
|
||||
width: 100%;
|
||||
}
|
||||
.input-query-form {
|
||||
width: 100%;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
|
||||
@@ -160,21 +160,18 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
return (
|
||||
<div className="input-typeahead-container">
|
||||
<Stack horizontal>
|
||||
<form aria-labelledby="input" className="input-query-form">
|
||||
<TextField
|
||||
multiline={useTextarea}
|
||||
rows={1}
|
||||
id="input"
|
||||
defaultValue={defaultValue}
|
||||
ariaLabel="Input query"
|
||||
placeholder={placeholder}
|
||||
className="input-type-head-text-field"
|
||||
value={defaultValue}
|
||||
onKeyDown={this.onSubmit}
|
||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||
/>
|
||||
</form>
|
||||
<TextField
|
||||
multiline={useTextarea}
|
||||
rows={1}
|
||||
defaultValue={defaultValue}
|
||||
ariaLabel="Input query"
|
||||
placeholder={placeholder}
|
||||
className="input-type-head-text-field"
|
||||
value={defaultValue}
|
||||
onKeyDown={this.onSubmit}
|
||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||
/>
|
||||
{this.props.showCancelButton && (
|
||||
<IconButton
|
||||
styles={iconButtonStyles}
|
||||
|
||||
@@ -7,22 +7,16 @@ exports[`inputTypeahead renders <input /> 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<form
|
||||
aria-labelledby="input"
|
||||
className="input-query-form"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</form>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
@@ -34,22 +28,16 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<form
|
||||
aria-labelledby="input"
|
||||
className="input-query-form"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</form>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -35,19 +35,16 @@ const testCassandraAccount: DataModels.DatabaseAccount = {
|
||||
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
||||
forwardingId: "Id",
|
||||
};
|
||||
|
||||
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
forwardingId: "Id",
|
||||
};
|
||||
|
||||
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||
forwardingId: "Id",
|
||||
};
|
||||
|
||||
describe("NotebookTerminalComponent", () => {
|
||||
@@ -55,7 +52,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testAccount,
|
||||
notebookServerInfo: testNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -66,7 +62,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo32Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -77,7 +72,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo36Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -88,7 +82,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testCassandraAccount,
|
||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as StringUtils from "../../../Utils/StringUtils";
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||
@@ -56,7 +55,6 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
apiType: userContext.apiType,
|
||||
authType: userContext.authType,
|
||||
databaseAccount: userContext.databaseAccount,
|
||||
tabId: this.props.tabId,
|
||||
};
|
||||
|
||||
postRobot.send(this.terminalWindow, "props", props, {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
@@ -149,23 +148,18 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
public render(): JSX.Element {
|
||||
this.traceViewGallery();
|
||||
|
||||
const tabs: GalleryTabInfo[] = [];
|
||||
if (userContext.features.publicGallery) {
|
||||
tabs.push(
|
||||
this.createPublicGalleryTab(
|
||||
GalleryTab.PublicGallery,
|
||||
this.state.publicNotebooks,
|
||||
this.state.isCodeOfConductAccepted
|
||||
)
|
||||
);
|
||||
}
|
||||
tabs.push(this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks));
|
||||
const tabs: GalleryTabInfo[] = [
|
||||
this.createPublicGalleryTab(
|
||||
GalleryTab.PublicGallery,
|
||||
this.state.publicNotebooks,
|
||||
this.state.isCodeOfConductAccepted
|
||||
),
|
||||
this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks),
|
||||
];
|
||||
|
||||
if (this.props.container) {
|
||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
if (userContext.features.publicGallery) {
|
||||
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
}
|
||||
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
|
||||
@@ -8,6 +8,95 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
onLinkClick={[Function]}
|
||||
selectedKey="OfficialSamples"
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Public gallery"
|
||||
itemKey="PublicGallery"
|
||||
key="PublicGallery"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="publicGalleryTabContainer"
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
wrap={true}
|
||||
>
|
||||
<StackItem
|
||||
grow={true}
|
||||
>
|
||||
<StyledSearchBox
|
||||
onChange={[Function]}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledLabelBase>
|
||||
Sort by
|
||||
</StyledLabelBase>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"minWidth": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Dropdown
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": 0,
|
||||
"text": "Most viewed",
|
||||
},
|
||||
Object {
|
||||
"key": 1,
|
||||
"text": "Most downloaded",
|
||||
},
|
||||
Object {
|
||||
"key": 3,
|
||||
"text": "Most recent",
|
||||
},
|
||||
Object {
|
||||
"key": 2,
|
||||
"text": "Most favorited",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey={0}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<StackItem>
|
||||
<StyledSpinnerBase
|
||||
size={3}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Official samples"
|
||||
itemKey="OfficialSamples"
|
||||
|
||||
@@ -17,7 +17,6 @@ import Explorer from "../../Explorer";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
@@ -52,7 +51,7 @@ export class NotebookViewerComponent
|
||||
super(props);
|
||||
|
||||
this.clientManager = new NotebookClientV2({
|
||||
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined, forwardingId: undefined },
|
||||
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined },
|
||||
databaseAccountName: undefined,
|
||||
defaultExperience: "NotebookViewer",
|
||||
isReadOnly: true,
|
||||
@@ -147,7 +146,7 @@ export class NotebookViewerComponent
|
||||
<NotebookMetadataComponent
|
||||
data={this.state.galleryItem}
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
|
||||
downloadButtonText={this.props.container && "Download to my notebooks"}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
@@ -72,7 +71,6 @@ export interface SettingsComponentState {
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
isScaleSaveable: boolean;
|
||||
isScaleDiscardable: boolean;
|
||||
throughputError: string;
|
||||
|
||||
timeToLive: TtlType;
|
||||
timeToLiveBaseline: TtlType;
|
||||
@@ -126,7 +124,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private changeFeedPolicyVisible: boolean;
|
||||
private isFixedContainer: boolean;
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
constructor(props: SettingsComponentProps) {
|
||||
@@ -149,7 +146,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.offer = this.database?.offer();
|
||||
}
|
||||
|
||||
const initialState: SettingsComponentState = {
|
||||
this.state = {
|
||||
throughput: undefined,
|
||||
throughputBaseline: undefined,
|
||||
autoPilotThroughput: undefined,
|
||||
@@ -158,7 +155,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
wasAutopilotOriginallySet: false,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughputError: undefined,
|
||||
|
||||
timeToLive: undefined,
|
||||
timeToLiveBaseline: undefined,
|
||||
@@ -199,12 +195,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
...initialState,
|
||||
...this.getBaselineValues(),
|
||||
...this.getAutoscaleBaselineValues(),
|
||||
};
|
||||
|
||||
this.saveSettingsButton = {
|
||||
isEnabled: this.isSaveSettingsButtonEnabled,
|
||||
isVisible: () => {
|
||||
@@ -218,11 +208,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
if (throughputCap && throughputCap !== -1) {
|
||||
this.calculateTotalThroughputUsed();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -231,6 +216,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.loadMongoIndexes();
|
||||
}
|
||||
|
||||
this.setAutoPilotStates();
|
||||
this.setBaseline();
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
@@ -268,10 +254,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.throughputError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.state.isScaleSaveable ||
|
||||
this.state.isSubSettingsSaveable ||
|
||||
@@ -291,24 +273,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
);
|
||||
};
|
||||
|
||||
private getAutoscaleBaselineValues = (): Partial<SettingsComponentState> => {
|
||||
private setAutoPilotStates = (): void => {
|
||||
const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
|
||||
|
||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
return {
|
||||
this.setState({
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isAutoPilotSelected: false,
|
||||
wasAutopilotOriginallySet: false,
|
||||
autoPilotThroughput: undefined,
|
||||
autoPilotThroughputBaseline: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
public hasProvisioningTypeChanged = (): boolean =>
|
||||
@@ -506,26 +481,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
||||
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 => {
|
||||
if (this.isAnalyticalStorageEnabled) {
|
||||
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
|
||||
@@ -573,25 +528,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
public setBaseline = (): void => {
|
||||
const baselineValues = this.getBaselineValues();
|
||||
const autoscaleBaselineValues = this.getAutoscaleBaselineValues();
|
||||
this.setState({ ...baselineValues, ...autoscaleBaselineValues } as SettingsComponentState);
|
||||
};
|
||||
|
||||
private getBaselineValues = (): Partial<SettingsComponentState> => {
|
||||
const offerThroughput = this.offer?.manualThroughput;
|
||||
|
||||
if (!this.isCollectionSettingsTab) {
|
||||
return {
|
||||
this.setState({
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultTtl = this.collection.defaultTtl();
|
||||
|
||||
let timeToLive: TtlType;
|
||||
let timeToLiveSeconds: number;
|
||||
let timeToLive: TtlType = this.state.timeToLive;
|
||||
let timeToLiveSeconds = this.state.timeToLiveSeconds;
|
||||
switch (defaultTtl) {
|
||||
case undefined:
|
||||
case 0:
|
||||
@@ -636,7 +587,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
||||
|
||||
return {
|
||||
this.setState({
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
@@ -659,7 +610,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||
geospatialConfigType: geoSpatialConfigType,
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
private getTabsButtons = (): CommandButtonComponentProps[] => {
|
||||
@@ -692,31 +643,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
return buttons;
|
||||
};
|
||||
|
||||
private onMaxAutoPilotThroughputChange = (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.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 onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
|
||||
this.setState({ autoPilotThroughput: 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 onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput });
|
||||
|
||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||
@@ -963,7 +893,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||
initialNotification: this.props.settingsTab.pendingNotification(),
|
||||
throughputError: this.state.throughputError,
|
||||
};
|
||||
|
||||
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 {
|
||||
getPriceCurrency,
|
||||
getCurrencySign,
|
||||
getAutoscalePricePerRu,
|
||||
getMultimasterMultiplier,
|
||||
computeRUUsagePriceHourly,
|
||||
getPricePerRu,
|
||||
estimatedCostDisclaimer,
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
DetailsRow,
|
||||
ICheckboxStyles,
|
||||
IChoiceGroupStyles,
|
||||
IColumn,
|
||||
IDetailsColumnStyles,
|
||||
IDetailsListStyles,
|
||||
IDetailsRowProps,
|
||||
IDetailsRowStyles,
|
||||
IDropdownStyles,
|
||||
IMessageBarStyles,
|
||||
ISeparatorStyles,
|
||||
IStackProps,
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
ITextFieldStyles,
|
||||
ITextStyles,
|
||||
ICheckboxStyles,
|
||||
IStackProps,
|
||||
IStackTokens,
|
||||
IChoiceGroupStyles,
|
||||
Link,
|
||||
Text,
|
||||
IMessageBarStyles,
|
||||
ITextStyles,
|
||||
IDetailsRowStyles,
|
||||
IStackStyles,
|
||||
IDetailsListStyles,
|
||||
IDropdownStyles,
|
||||
ISeparatorStyles,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
SelectionMode,
|
||||
Stack,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Stack,
|
||||
Text,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles,
|
||||
} from "@fluentui/react";
|
||||
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";
|
||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||
|
||||
export interface EstimatedSpendingDisplayProps {
|
||||
costType: JSX.Element;
|
||||
@@ -65,7 +65,7 @@ export interface PriceBreakdown {
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
@@ -223,15 +223,14 @@ export const getRuPriceBreakdown = (
|
||||
multimasterEnabled: isMultimaster,
|
||||
isAutoscale: isAutoscale,
|
||||
});
|
||||
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
|
||||
const pricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
|
||||
: getPricePerRu(serverId, multimasterMultiplier);
|
||||
const basePricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
||||
: getPricePerRu(serverId);
|
||||
return {
|
||||
hourlyPrice,
|
||||
hourlyPrice: hourlyPrice,
|
||||
dailyPrice: hourlyPrice * 24,
|
||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||
pricePerRu,
|
||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
||||
currency: getPriceCurrency(serverId),
|
||||
currencySign: getCurrencySign(serverId),
|
||||
};
|
||||
@@ -272,7 +271,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
||||
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
|
||||
<Link href={Urls.autoscaleMigration}>Learn more</Link>
|
||||
<a href={Urls.autoscaleMigration}>Learn more</a>
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ export interface ScaleComponentProps {
|
||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
initialNotification: DataModels.Notification;
|
||||
throughputError?: string;
|
||||
}
|
||||
|
||||
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
@@ -190,7 +189,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
||||
throughputError={this.props.throughputError}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
this.geospatialVisible = userContext.apiType === "SQL";
|
||||
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
|
||||
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
this.partitionKeyValue = this.getPartitionKeyValue();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -291,14 +291,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
};
|
||||
|
||||
private getPartitionKeyValue = (): string => {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return this.props.collection.partitionKeyProperties?.[0] || "";
|
||||
}
|
||||
|
||||
return (this.props.collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
||||
};
|
||||
|
||||
private getPartitionKeyComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
{this.getPartitionKeyVisible() && (
|
||||
@@ -318,8 +310,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
if (
|
||||
userContext.apiType === "Cassandra" ||
|
||||
userContext.apiType === "Tables" ||
|
||||
!this.props.collection.partitionKeyProperties ||
|
||||
this.props.collection.partitionKeyProperties.length === 0 ||
|
||||
!this.props.collection.partitionKeyProperty ||
|
||||
(userContext.apiType === "Mongo" && this.props.collection.partitionKey.systemKey)
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/Telemet
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
@@ -75,7 +75,6 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
getThroughputWarningMessage: () => JSX.Element;
|
||||
usageSizeInKB: number;
|
||||
throughputError?: string;
|
||||
}
|
||||
|
||||
interface ThroughputInputAutoPilotV3State {
|
||||
@@ -540,8 +539,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
errorMessage={this.props.throughputError}
|
||||
min={minAutoPilotThroughput}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
@@ -581,7 +579,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
min={this.props.minimum}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
{this.state.exceedFreeTierThroughput && (
|
||||
<MessageBar
|
||||
|
||||
@@ -20,7 +20,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -40,7 +39,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -75,7 +73,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -83,11 +80,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.
|
||||
|
||||
<StyledLinkBase
|
||||
<a
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</a>
|
||||
</Text>
|
||||
</StyledMessageBar>
|
||||
<StyledChoiceGroup
|
||||
@@ -145,7 +142,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Max RU/s"
|
||||
min={1000}
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
@@ -189,7 +186,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -464,7 +460,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -136,7 +136,6 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -413,7 +412,6 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -954,7 +952,6 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -1231,7 +1228,6 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const collection = ({
|
||||
kind: "hash",
|
||||
version: 2,
|
||||
},
|
||||
partitionKeyProperties: ["partitionKey"],
|
||||
partitionKeyProperty: "partitionKey",
|
||||
readSettings: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -34,13 +34,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"retryOptions": Object {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
"retries": 3,
|
||||
},
|
||||
},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -64,9 +57,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"paths": Array [],
|
||||
"version": 2,
|
||||
},
|
||||
"partitionKeyProperties": Array [
|
||||
"partitionKey",
|
||||
],
|
||||
"partitionKeyProperty": "partitionKey",
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": Object {},
|
||||
"usageSizeInKB": [Function],
|
||||
@@ -110,13 +101,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"retryOptions": Object {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
"retries": 3,
|
||||
},
|
||||
},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -140,9 +124,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"paths": Array [],
|
||||
"version": 2,
|
||||
},
|
||||
"partitionKeyProperties": Array [
|
||||
"partitionKey",
|
||||
],
|
||||
"partitionKeyProperty": "partitionKey",
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": Object {},
|
||||
"usageSizeInKB": [Function],
|
||||
|
||||
@@ -159,7 +159,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -167,17 +166,16 @@ 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.
|
||||
|
||||
<StyledLinkBase
|
||||
<a
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</a>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -197,7 +195,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -210,7 +207,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -223,7 +219,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -235,7 +230,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -255,7 +249,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -271,7 +264,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -286,7 +278,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -300,7 +291,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -312,7 +302,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -332,7 +321,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -375,7 +363,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -391,7 +378,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -408,7 +394,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
const descriptionElement = (
|
||||
<Stack>
|
||||
{labelElement}
|
||||
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId} style={{ whiteSpace: "pre-line" }}>
|
||||
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
|
||||
{this.props.getTranslation(description.textTKey)}{" "}
|
||||
{description.link && (
|
||||
<Link target="_blank" href={description.link.href}>
|
||||
|
||||
@@ -27,11 +27,6 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
||||
<Text
|
||||
aria-labelledby="description-label"
|
||||
id="description-text-display"
|
||||
style={
|
||||
Object {
|
||||
"whiteSpace": "pre-line",
|
||||
}
|
||||
}
|
||||
>
|
||||
this is an example description text.
|
||||
|
||||
@@ -346,11 +341,6 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
||||
<Text
|
||||
aria-labelledby="description-label"
|
||||
id="description-text-display"
|
||||
style={
|
||||
Object {
|
||||
"whiteSpace": "pre-line",
|
||||
}
|
||||
}
|
||||
>
|
||||
this is an example description text.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.tabComponentContainer {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
@@ -6,7 +6,6 @@ import { userContext } from "../../../../UserContext";
|
||||
import {
|
||||
calculateEstimateNumber,
|
||||
computeRUUsagePriceHourly,
|
||||
estimatedCostDisclaimer,
|
||||
getAutoscalePricePerRu,
|
||||
getCurrencySign,
|
||||
getMultimasterMultiplier,
|
||||
@@ -43,9 +42,11 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||
const pricePerRu = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
|
||||
: getPricePerRu(serverId) * multiplier;
|
||||
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
|
||||
|
||||
if (isAutoscale) {
|
||||
return (
|
||||
|
||||
@@ -5,10 +5,8 @@ const props = {
|
||||
isDatabase: false,
|
||||
showFreeTierExceedThroughputTooltip: true,
|
||||
isSharded: true,
|
||||
isFreeTier: false,
|
||||
setThroughputValue: () => jest.fn(),
|
||||
setIsAutoscale: () => jest.fn(),
|
||||
setIsThroughputCapExceeded: () => jest.fn(),
|
||||
onCostAcknowledgeChange: () => jest.fn(),
|
||||
};
|
||||
describe("ThroughputInput Pane", () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
@@ -14,85 +13,28 @@ import "./ThroughputInput.less";
|
||||
export interface ThroughputInputProps {
|
||||
isDatabase: boolean;
|
||||
isSharded: boolean;
|
||||
isFreeTier: boolean;
|
||||
showFreeTierExceedThroughputTooltip: boolean;
|
||||
isQuickstart?: boolean;
|
||||
setThroughputValue: (throughput: number) => void;
|
||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
|
||||
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
|
||||
}
|
||||
|
||||
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
isDatabase,
|
||||
isSharded,
|
||||
isFreeTier,
|
||||
showFreeTierExceedThroughputTooltip,
|
||||
setThroughputValue,
|
||||
setIsAutoscale,
|
||||
setIsThroughputCapExceeded,
|
||||
isSharded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K
|
||||
);
|
||||
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
|
||||
setIsAutoscale(isAutoscaleSelected);
|
||||
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 => {
|
||||
let throughputHeaderText: string;
|
||||
if (isAutoscaleSelected) {
|
||||
@@ -118,17 +60,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
const newThroughput = parseInt(newInput);
|
||||
setThroughput(newThroughput);
|
||||
setThroughputValue(newThroughput);
|
||||
|
||||
if (!isSharded && newThroughput > 10000) {
|
||||
setThroughputError("Unsharded collections support up to 10,000 RUs");
|
||||
return;
|
||||
} else {
|
||||
setThroughputError("");
|
||||
}
|
||||
|
||||
if (!checkThroughputCap(newThroughput)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setThroughputError("");
|
||||
};
|
||||
|
||||
const getAutoScaleTooltip = (): string => {
|
||||
@@ -156,20 +92,15 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
const defaultThroughput = isFreeTier
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(defaultThroughput);
|
||||
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||
setIsAutoscale(true);
|
||||
checkThroughputCap(defaultThroughput);
|
||||
} else {
|
||||
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||
setIsAutoScaleSelected(false);
|
||||
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||
setIsAutoscale(false);
|
||||
checkThroughputCap(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -187,7 +118,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
<input
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={isAutoscaleSelected}
|
||||
type="radio"
|
||||
role="radio"
|
||||
@@ -201,7 +131,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
aria-label="Manual mode"
|
||||
checked={!isAutoscaleSelected}
|
||||
type="radio"
|
||||
aria-required={true}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||
@@ -227,7 +156,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
id="autoscaleRUValueField"
|
||||
type="number"
|
||||
styles={{
|
||||
fieldGroup: { width: 300, height: 27 },
|
||||
@@ -235,7 +163,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
}}
|
||||
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
value={throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
required={true}
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isFreeTier={false}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
showFreeTierExceedThroughputTooltip={true}
|
||||
>
|
||||
@@ -347,14 +345,12 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -634,7 +630,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
@@ -656,7 +651,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.0"
|
||||
@@ -673,7 +667,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</span>
|
||||
<input
|
||||
aria-label="Manual mode"
|
||||
aria-required={true}
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.2"
|
||||
@@ -1334,14 +1327,12 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -1621,7 +1612,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
@@ -1637,9 +1627,8 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<StyledTextFieldBase
|
||||
aria-label="Max request units per second"
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
key=".0:$.2"
|
||||
min={1000}
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
@@ -1661,8 +1650,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-label="Max request units per second"
|
||||
deferredValidationTime={200}
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
min={1000}
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
resizable={true}
|
||||
@@ -1957,8 +1945,8 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<input
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-64"
|
||||
id="autoscaleRUValueField"
|
||||
min={1000}
|
||||
id="TextField2"
|
||||
min={4000}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
@@ -173,7 +173,6 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
|
||||
role="treeitem"
|
||||
id={node.id}
|
||||
>
|
||||
<div
|
||||
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
|
||||
@@ -244,7 +243,6 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||
<IconButton
|
||||
name="More"
|
||||
title="More"
|
||||
className="treeMenuEllipsis"
|
||||
ariaLabel={menuItemLabel}
|
||||
menuIconProps={{
|
||||
|
||||
@@ -137,7 +137,6 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
@@ -212,7 +211,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -360,7 +358,6 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
@@ -426,7 +423,6 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,10 +68,11 @@ export class ContainerSampleGenerator {
|
||||
return database.findCollectionWithId(this.sampleDataFile.collectionId);
|
||||
}
|
||||
|
||||
public async populateContainerAsync(collection: ViewModels.Collection): Promise<void> {
|
||||
private async populateContainerAsync(collection: ViewModels.Collection): Promise<void> {
|
||||
if (!collection) {
|
||||
throw new Error("No container to populate");
|
||||
}
|
||||
const promises: Q.Promise<any>[] = [];
|
||||
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
// For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import shallow from "zustand/shallow";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import {
|
||||
ContainerConnectionInfo,
|
||||
IPhoenixConnectionInfoResult,
|
||||
IProvisionData,
|
||||
IResponse,
|
||||
} from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import {
|
||||
get as getWorkspace,
|
||||
listByDatabaseAccount,
|
||||
listConnectionInfo,
|
||||
start,
|
||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
@@ -47,12 +41,13 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import type NotebookManager from "./Notebook/NotebookManager";
|
||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
@@ -92,13 +87,12 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||
private phoenixClient: PhoenixClient;
|
||||
|
||||
constructor() {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
this._isInitializingNotebooks = false;
|
||||
this.phoenixClient = new PhoenixClient();
|
||||
useNotebook.subscribe(
|
||||
() => this.refreshCommandBarButtons(),
|
||||
(state) => state.isNotebooksEnabledForAccount
|
||||
@@ -166,23 +160,33 @@ export default class Explorer {
|
||||
);
|
||||
|
||||
useNotebook.subscribe(
|
||||
async () => this.initiateAndRefreshNotebookList(),
|
||||
(state) => [state.isNotebookEnabled, state.isRefreshed],
|
||||
shallow
|
||||
async () => {
|
||||
if (!this.notebookManager) {
|
||||
const NotebookManager = await (
|
||||
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
|
||||
).default;
|
||||
this.notebookManager = new NotebookManager();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList(),
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
},
|
||||
(state) => state.isNotebookEnabled
|
||||
);
|
||||
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
|
||||
// Override notebook server parameters from URL parameters
|
||||
if (
|
||||
userContext.features.notebookServerUrl &&
|
||||
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerToken
|
||||
) {
|
||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken,
|
||||
forwardingId: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,24 +194,20 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
public async initiateAndRefreshNotebookList(): Promise<void> {
|
||||
if (!this.notebookManager) {
|
||||
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
|
||||
.default;
|
||||
this.notebookManager = new NotebookManager();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList(),
|
||||
if (userContext.features.livyEndpoint) {
|
||||
useNotebook.getState().setSparkClusterConnectionInfo({
|
||||
userName: undefined,
|
||||
password: undefined,
|
||||
endpoints: [
|
||||
{
|
||||
endpoint: userContext.features.livyEndpoint,
|
||||
kind: DataModels.SparkClusterEndpointKind.Livy,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
public openEnableSynapseLinkDialog(): void {
|
||||
@@ -298,7 +298,7 @@ export default class Explorer {
|
||||
db1.id().localeCompare(db2.id())
|
||||
);
|
||||
useDatabases.setState({ databases: updatedDatabases });
|
||||
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases);
|
||||
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, currentDatabases);
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
@@ -343,95 +343,28 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
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._isInitializingNotebooks = false;
|
||||
}
|
||||
|
||||
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,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
let connectionInfo;
|
||||
try {
|
||||
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||
throw new Error(`PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
status: error.status,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Connection Failed",
|
||||
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
|
||||
);
|
||||
}
|
||||
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.phoenixServiceUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
this.notebookManager?.notebookClient
|
||||
.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace(): void {
|
||||
public resetNotebookWorkspace() {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||
handleError(
|
||||
"Attempt to reset notebook workspace, but notebook is not enabled",
|
||||
@@ -439,14 +372,11 @@ export default class Explorer {
|
||||
);
|
||||
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 = {
|
||||
isModal: true,
|
||||
title: "Reset Workspace",
|
||||
subText: dialogContent,
|
||||
subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?",
|
||||
primaryButtonText: "OK",
|
||||
secondaryButtonText: "Cancel",
|
||||
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
||||
@@ -459,6 +389,7 @@ export default class Explorer {
|
||||
if (!databaseAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { value: workspaces } = await listByDatabaseAccount(
|
||||
userContext.subscriptionId,
|
||||
@@ -472,57 +403,48 @@ 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 () => {
|
||||
useDialog.getState().closeDialog();
|
||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||
let connectionStatus: ContainerConnectionInfo;
|
||||
try {
|
||||
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?.phoenixServiceUrl) {
|
||||
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
await this.notebookManager?.notebookClient.resetWorkspace();
|
||||
logConsoleInfo("Successfully reset notebook workspace");
|
||||
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
||||
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
connectionStatus = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearInProgressMessage();
|
||||
@@ -714,9 +636,6 @@ export default class Explorer {
|
||||
if (!notebookContentItem || !notebookContentItem.path) {
|
||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||
}
|
||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.allocateContainer();
|
||||
}
|
||||
|
||||
const notebookTabs = useTabs
|
||||
.getState()
|
||||
@@ -932,54 +851,15 @@ export default class Explorer {
|
||||
/**
|
||||
* This creates a new notebook file, then opens the notebook
|
||||
*/
|
||||
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||
public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||
handleError(error, "Explorer/onNewNotebookClicked");
|
||||
throw new Error(error);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (isGithubTree) {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
} else {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookModalTitle,
|
||||
undefined,
|
||||
"Create",
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
}
|
||||
}
|
||||
|
||||
private getNewNoteWarningText(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.newNotebookModalContent1}</p>
|
||||
<br />
|
||||
<p>
|
||||
{Notebook.newNotebookModalContent2}
|
||||
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
||||
{Notebook.learnMore}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
|
||||
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
@@ -1026,26 +906,7 @@ export default class Explorer {
|
||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||
}
|
||||
|
||||
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 {
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
||||
let title: string;
|
||||
|
||||
switch (kind) {
|
||||
@@ -1067,7 +928,7 @@ export default class Explorer {
|
||||
|
||||
const terminalTabs: TerminalTab[] = useTabs
|
||||
.getState()
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
|
||||
|
||||
let index = 1;
|
||||
if (terminalTabs.length > 0) {
|
||||
@@ -1096,7 +957,7 @@ export default class Explorer {
|
||||
notebookUrl?: string,
|
||||
galleryItem?: IGalleryItem,
|
||||
isFavorite?: boolean
|
||||
): Promise<void> {
|
||||
) {
|
||||
const title = "Gallery";
|
||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||
const galleryTab = useTabs
|
||||
@@ -1130,12 +991,7 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public async onNewCollectionClicked(
|
||||
options: {
|
||||
databaseId?: string;
|
||||
isQuickstart?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
public async onNewCollectionClicked(databaseId?: string): Promise<void> {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
useSidePanel
|
||||
.getState()
|
||||
@@ -1144,13 +1000,10 @@ export default class Explorer {
|
||||
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
|
||||
);
|
||||
} else {
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
throughputCap && throughputCap !== -1
|
||||
? await useDatabases.getState().loadAllOffers()
|
||||
: await useDatabases.getState().loadDatabaseOffers();
|
||||
await useDatabases.getState().loadDatabaseOffers();
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} {...options} />);
|
||||
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1163,12 +1016,18 @@ 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> {
|
||||
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
|
||||
await useNotebook.getState().getPhoenixStatus();
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.allocateContainer();
|
||||
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
}
|
||||
|
||||
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
||||
@@ -1199,27 +1058,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
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;
|
||||
this.uploadFilePanel(parent);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText()
|
||||
);
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.uploadFilePanel(parent);
|
||||
}
|
||||
}
|
||||
|
||||
private uploadFilePanel(parent?: NotebookContentItem): void {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
@@ -1228,47 +1067,30 @@ 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> {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
const isNotebookEnabled =
|
||||
userContext.features.notebooksDownBanner ||
|
||||
useNotebook.getState().isPhoenixNotebooks ||
|
||||
useNotebook.getState().isPhoenixFeatures;
|
||||
const isNotebookEnabled: boolean =
|
||||
userContext.authType !== AuthType.ResourceToken &&
|
||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
||||
userContext.features.enableNotebooks);
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook
|
||||
.getState()
|
||||
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
|
||||
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
|
||||
|
||||
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
||||
isNotebookEnabled,
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
if (!userContext.features.notebooksTemporarilyDown) {
|
||||
if (isNotebookEnabled) {
|
||||
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,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import * as Q from "q";
|
||||
import * as React from "react";
|
||||
@@ -296,6 +294,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.setGremlinParams();
|
||||
}
|
||||
|
||||
const selectedNode = this.state.highlightedNode;
|
||||
|
||||
props.onGraphAccessorCreated({
|
||||
applyFilter: this.submitQuery.bind(this),
|
||||
addVertex: this.addVertex.bind(this),
|
||||
@@ -303,7 +303,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
});
|
||||
} // constructor
|
||||
|
||||
public shareIGraphConfig(igraphConfig: IGraphConfig): void {
|
||||
public shareIGraphConfig(igraphConfig: IGraphConfig) {
|
||||
this.setState({
|
||||
igraphConfig: { ...igraphConfig },
|
||||
});
|
||||
@@ -330,10 +330,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
|
||||
|
||||
// aggregate all the properties, remove dropped ones
|
||||
const finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
|
||||
let finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
|
||||
|
||||
// Compose the query
|
||||
const pkId = editedProperties.pkId;
|
||||
let pkId = editedProperties.pkId;
|
||||
let updateQueryFragment = "";
|
||||
|
||||
finalProperties.forEach((p) => {
|
||||
@@ -422,7 +422,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Called from ko binding
|
||||
* @param id
|
||||
*/
|
||||
public selectNode(id: string): void {
|
||||
public selectNode(id: string) {
|
||||
if (!this.d3ForceGraph) {
|
||||
console.warn("Attempting to select node, but d3ForceGraph not initialized, yet.");
|
||||
return;
|
||||
@@ -431,7 +431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.d3ForceGraph.selectNode(id);
|
||||
}
|
||||
|
||||
public deleteHighlightedNode(): void {
|
||||
public deleteHighlightedNode() {
|
||||
if (!this.state.highlightedNode) {
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, "No highlighted node to remove.");
|
||||
return;
|
||||
@@ -467,23 +467,23 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Is of type: {e: GremlinEdge, v: GremlinVertex}[]
|
||||
* @param data
|
||||
*/
|
||||
public static isEdgeVertexPairArray(data: any): boolean {
|
||||
public static isEdgeVertexPairArray(data: any) {
|
||||
if (!(data instanceof Array)) {
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result not an array", data);
|
||||
return false;
|
||||
}
|
||||
|
||||
const pairs: any[] = data;
|
||||
let pairs: any[] = data;
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const item = pairs[i];
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(item, "e") ||
|
||||
!Object.prototype.hasOwnProperty.call(item, "v") ||
|
||||
!Object.prototype.hasOwnProperty.call(item["e"], "id") ||
|
||||
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
|
||||
!item.hasOwnProperty("e") ||
|
||||
!item.hasOwnProperty("v") ||
|
||||
!item["e"].hasOwnProperty("id") ||
|
||||
!item["e"].hasOwnProperty("type") ||
|
||||
item["e"].type !== "edge" ||
|
||||
!Object.prototype.hasOwnProperty.call(item["v"], "id") ||
|
||||
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
|
||||
!item["v"].hasOwnProperty("id") ||
|
||||
!item["v"].hasOwnProperty("type") ||
|
||||
item["v"].type !== "vertex"
|
||||
) {
|
||||
return false;
|
||||
@@ -514,7 +514,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
// Try hitting cache first
|
||||
const cache = outE ? this.outECache : this.inECache;
|
||||
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}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Info, msg);
|
||||
return Q.resolve(pairs);
|
||||
@@ -588,6 +588,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
vertex._outEAllLoaded &&
|
||||
vertex._inEAllLoaded
|
||||
) {
|
||||
console.info("No more edges to load for vertex " + vertex.id);
|
||||
updateGraphData();
|
||||
return Q.resolve(graphData);
|
||||
}
|
||||
@@ -667,7 +668,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
}
|
||||
);
|
||||
|
||||
return promise.then(() => {
|
||||
return promise.then((nbPairsFetched: number) => {
|
||||
if (offsetIndex >= GraphExplorer.LOAD_PAGE_SIZE || !vertex._outEAllLoaded || !vertex._inEAllLoaded) {
|
||||
vertex._pagination = {
|
||||
total:
|
||||
@@ -753,7 +754,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Create a new edge in docdb and update graph
|
||||
* @param e
|
||||
*/
|
||||
public createNewEdge(e: GraphNewEdgeData): Q.Promise<unknown> {
|
||||
public createNewEdge(e: GraphNewEdgeData): Q.Promise<any> {
|
||||
const q = `g.V('${GraphUtil.escapeSingleQuotes(e.inputOutV)}').addE('${GraphUtil.escapeSingleQuotes(
|
||||
e.label
|
||||
)}').To(g.V('${GraphUtil.escapeSingleQuotes(e.inputInV)}'))`;
|
||||
@@ -771,8 +772,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return;
|
||||
}
|
||||
|
||||
const edge = edges[0];
|
||||
const graphData = this.originalGraphData;
|
||||
let edge = edges[0];
|
||||
let graphData = this.originalGraphData;
|
||||
graphData.addEdge(edge);
|
||||
|
||||
// Allow loadNeighbors to load list new edge
|
||||
@@ -799,10 +800,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Manually update in-memory graph.
|
||||
* @param edgeId
|
||||
*/
|
||||
public removeEdge(edgeId: string): Q.Promise<unknown> {
|
||||
public removeEdge(edgeId: string): Q.Promise<any> {
|
||||
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
|
||||
() => {
|
||||
const graphData = this.originalGraphData;
|
||||
let graphData = this.originalGraphData;
|
||||
graphData.removeEdge(edgeId, false);
|
||||
this.updateGraphData(graphData, this.state.igraphConfig);
|
||||
},
|
||||
@@ -825,14 +826,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return false;
|
||||
}
|
||||
|
||||
const vertices: any[] = data;
|
||||
let vertices: any[] = data;
|
||||
if (vertices.length > 0) {
|
||||
const v0 = vertices[0];
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(v0, "id") ||
|
||||
!Object.prototype.hasOwnProperty.call(v0, "type") ||
|
||||
v0.type !== "vertex"
|
||||
) {
|
||||
let v0 = vertices[0];
|
||||
if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -840,7 +837,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
}
|
||||
|
||||
public processGremlinQueryResults(result: GremlinClient.GremlinRequestResult): void {
|
||||
const data = result.data as GraphData.GremlinVertex[];
|
||||
const data = result.data as any;
|
||||
this.setFilterQueryStatus(FilterQueryStatus.GraphEmptyResult);
|
||||
|
||||
if (data === null) {
|
||||
@@ -930,13 +927,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
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)";
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, err, vertices);
|
||||
throw { title: err };
|
||||
}
|
||||
|
||||
const vertex = vertices[0];
|
||||
let vertex = vertices[0];
|
||||
const graphData = this.originalGraphData;
|
||||
graphData.addVertex(vertex);
|
||||
this.updateGraphData(graphData, this.state.igraphConfig);
|
||||
@@ -1025,7 +1022,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.gremlinClient.destroy();
|
||||
}
|
||||
public componentDidMount(): void {
|
||||
if (this.props.onLoadStartKey !== null && this.props.onLoadStartKey !== undefined) {
|
||||
if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
@@ -1085,7 +1082,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.Error, msg: string, ...errorData: any[]): void;
|
||||
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
|
||||
let errorDataStr = "";
|
||||
let errorDataStr: string = "";
|
||||
if (errorData && errorData.length > 0) {
|
||||
console.error(msg, errorData);
|
||||
errorDataStr = ": " + JSON.stringify(errorData);
|
||||
@@ -1164,15 +1161,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
)}"`
|
||||
).then(
|
||||
(documents: DataModels.DocumentId[]) => {
|
||||
$.each(
|
||||
documents,
|
||||
(index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => {
|
||||
newIconsMap[doc["_graph_icon_property_value"]] = {
|
||||
data: doc["icon"],
|
||||
format: doc["format"],
|
||||
};
|
||||
}
|
||||
);
|
||||
$.each(documents, (index: number, doc: any) => {
|
||||
newIconsMap[doc["_graph_icon_property_value"]] = {
|
||||
data: doc["icon"],
|
||||
format: doc["format"],
|
||||
};
|
||||
});
|
||||
|
||||
// Update graph configuration
|
||||
this.setState({
|
||||
@@ -1229,8 +1223,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const key = this.state.igraphConfig.nodeCaption;
|
||||
return $.map(
|
||||
this.state.rootMap,
|
||||
(value: any): LeftPane.CaptionId => {
|
||||
const result = GraphData.GraphData.getNodePropValue(value, key);
|
||||
(value: any, index: number): LeftPane.CaptionId => {
|
||||
let result = GraphData.GraphData.getNodePropValue(value, key);
|
||||
return {
|
||||
caption: result !== undefined ? result : value.id,
|
||||
id: value.id,
|
||||
@@ -1243,7 +1237,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Selecting a root node means
|
||||
* @param node
|
||||
*/
|
||||
private selectRootNode(id: string): Q.Promise<unknown> {
|
||||
private selectRootNode(id: string): Q.Promise<any> {
|
||||
if (!this.d3ForceGraph) {
|
||||
console.warn("Attempting to reset zoom, but d3ForceGraph not initialized, yet.");
|
||||
} else {
|
||||
@@ -1288,7 +1282,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.collectNodeProperties(this.originalGraphData.vertices);
|
||||
this.updatePropertiesPane(id);
|
||||
},
|
||||
(reason: string) => {
|
||||
(reason: any) => {
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to select root node. Reason:${reason}`);
|
||||
}
|
||||
);
|
||||
@@ -1355,10 +1349,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
private getPkIdFromVertex(v: GraphData.GremlinVertex): string {
|
||||
if (
|
||||
this.props.collectionPartitionKeyProperty &&
|
||||
Object.prototype.hasOwnProperty.call(v, "properties") &&
|
||||
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty) &&
|
||||
v.hasOwnProperty("properties") &&
|
||||
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty) &&
|
||||
v.properties[this.props.collectionPartitionKeyProperty].length > 0 &&
|
||||
Object.prototype.hasOwnProperty.call(v.properties[this.props.collectionPartitionKeyProperty][0], "value")
|
||||
v.properties[this.props.collectionPartitionKeyProperty][0].hasOwnProperty("value")
|
||||
) {
|
||||
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
|
||||
return GraphExplorer.generatePkIdPair(pk, v.id);
|
||||
@@ -1376,8 +1370,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
private getPkIdFromNodeData(v: GraphHighlightedNodeData): string {
|
||||
if (
|
||||
this.props.collectionPartitionKeyProperty &&
|
||||
Object.prototype.hasOwnProperty.call(v, "properties") &&
|
||||
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty)
|
||||
v.hasOwnProperty("properties") &&
|
||||
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty)
|
||||
) {
|
||||
const pk = v.properties[this.props.collectionPartitionKeyProperty];
|
||||
return GraphExplorer.generatePkIdPair(pk[0] as PartitionKeyValueType, v.id);
|
||||
@@ -1394,14 +1388,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @return id
|
||||
*/
|
||||
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
|
||||
const { id } = d;
|
||||
let { id } = d;
|
||||
if (typeof id !== "string") {
|
||||
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
|
||||
logConsoleError(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (collectionPartitionKeyProperty && Object.prototype.hasOwnProperty.call(d, collectionPartitionKeyProperty)) {
|
||||
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
|
||||
let pk = (d as any)[collectionPartitionKeyProperty];
|
||||
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
|
||||
if (Array.isArray(pk) && pk.length > 0) {
|
||||
@@ -1431,7 +1425,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
|
||||
return this.executeNonPagedDocDbQuery(q).then(
|
||||
(documents: DataModels.DocumentId[]) => {
|
||||
const possibleVertices = [] as PossibleVertex[];
|
||||
let possibleVertices = [] as PossibleVertex[];
|
||||
$.each(documents, (index: number, item: any) => {
|
||||
if (highlightedNodeId && item.id === highlightedNodeId) {
|
||||
// Exclude highlighed node in the list
|
||||
@@ -1445,7 +1439,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
caption: item.p,
|
||||
});
|
||||
} else {
|
||||
if (Object.prototype.hasOwnProperty.call(item, "p")) {
|
||||
if (item.hasOwnProperty("p")) {
|
||||
possibleVertices.push({
|
||||
value: item.id,
|
||||
caption: item.p[0]["_value"],
|
||||
@@ -1468,17 +1462,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @param addedEdges
|
||||
* @return promise when done
|
||||
*/
|
||||
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<unknown> {
|
||||
const promises = [];
|
||||
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> {
|
||||
let promises = [];
|
||||
// Drop edges
|
||||
for (let i = 0; i < editedEdges.droppedIds.length; i++) {
|
||||
const id = editedEdges.droppedIds[i];
|
||||
let id = editedEdges.droppedIds[i];
|
||||
promises.push(this.removeEdge(id));
|
||||
}
|
||||
|
||||
// Add edges
|
||||
for (let i = 0; i < editedEdges.addedEdges.length; i++) {
|
||||
const e = editedEdges.addedEdges[i];
|
||||
let e = editedEdges.addedEdges[i];
|
||||
promises.push(
|
||||
this.createNewEdge(e).then(() => {
|
||||
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph
|
||||
@@ -1531,9 +1525,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* For unit testing purposes
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public onGraphUpdated(_timestamp: number): void {}
|
||||
public onGraphUpdated(timestamp: number): void {}
|
||||
|
||||
/**
|
||||
* Get node properties for styling purposes. Result is the union of all properties of all nodes.
|
||||
@@ -1541,17 +1533,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
|
||||
const props = {} as any; // Hashset
|
||||
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
|
||||
for (const p in item) {
|
||||
for (var p in item) {
|
||||
// DocDB: Exclude type because it's always 'vertex'
|
||||
if (p !== "type" && typeof (item as any)[p] === "string") {
|
||||
props[p] = true;
|
||||
}
|
||||
}
|
||||
// Inspect properties
|
||||
if (Object.prototype.hasOwnProperty.call(item, "properties")) {
|
||||
if (item.hasOwnProperty("properties")) {
|
||||
// TODO This is DocDB-graph specific
|
||||
// Assume each property value is [{value:... }]
|
||||
for (const f in item.properties) {
|
||||
for (var f in item.properties) {
|
||||
props[f] = true;
|
||||
}
|
||||
}
|
||||
@@ -1578,21 +1570,21 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.originalGraphData.getVertexById(id);
|
||||
let data = this.originalGraphData.getVertexById(id);
|
||||
|
||||
// A bit of translation to make it easier to display
|
||||
const props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
|
||||
for (const p in data.properties) {
|
||||
let props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
|
||||
for (let p in data.properties) {
|
||||
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
|
||||
}
|
||||
|
||||
// update neighbors
|
||||
const sources: NeighborVertexBasicInfo[] = [];
|
||||
const targets: NeighborVertexBasicInfo[] = [];
|
||||
let sources: NeighborVertexBasicInfo[] = [];
|
||||
let targets: NeighborVertexBasicInfo[] = [];
|
||||
this.props.onResetDefaultGraphConfigValues();
|
||||
const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
|
||||
let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
|
||||
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
|
||||
const sData: GraphHighlightedNodeData = {
|
||||
let sData: GraphHighlightedNodeData = {
|
||||
id: data.id,
|
||||
label: data.label,
|
||||
properties: props,
|
||||
@@ -1619,16 +1611,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
targets: NeighborVertexBasicInfo[]
|
||||
): void {
|
||||
// update neighbors
|
||||
const gd = this.originalGraphData;
|
||||
const v = gd.getVertexById(id);
|
||||
let gd = this.originalGraphData;
|
||||
let v = gd.getVertexById(id);
|
||||
|
||||
// Clear the array while keeping the references
|
||||
sources.length = 0;
|
||||
targets.length = 0;
|
||||
|
||||
const possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
|
||||
let possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
|
||||
|
||||
for (const p in v.inE) {
|
||||
for (let p in v.inE) {
|
||||
possibleEdgeLabels[p] = true;
|
||||
const edges = v.inE[p];
|
||||
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
|
||||
@@ -1637,7 +1629,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
|
||||
return;
|
||||
}
|
||||
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
sources.push({
|
||||
name: caption,
|
||||
id: neighborId,
|
||||
@@ -1647,7 +1639,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
});
|
||||
}
|
||||
|
||||
for (const p in v.outE) {
|
||||
for (let p in v.outE) {
|
||||
possibleEdgeLabels[p] = true;
|
||||
const edges = v.outE[p];
|
||||
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
|
||||
@@ -1656,7 +1648,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
|
||||
return;
|
||||
}
|
||||
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
targets.push({
|
||||
name: caption,
|
||||
id: neighborId,
|
||||
@@ -1668,7 +1660,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
|
||||
this.setState({
|
||||
possibleEdgeLabels: Object.keys(possibleEdgeLabels).map(
|
||||
(value: string): InputTypeaheadComponent.Item => {
|
||||
(value: string, index: number, array: string[]): InputTypeaheadComponent.Item => {
|
||||
return { caption: value, value: value };
|
||||
}
|
||||
),
|
||||
@@ -1689,20 +1681,20 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedVertex = vertices[0];
|
||||
let updatedVertex = vertices[0];
|
||||
if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
|
||||
const currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
|
||||
let currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
|
||||
// Copy updated properties
|
||||
if (Object.prototype.hasOwnProperty.call(currentVertex, "properties")) {
|
||||
if (currentVertex.hasOwnProperty("properties")) {
|
||||
delete currentVertex["properties"];
|
||||
}
|
||||
for (const p in updatedVertex) {
|
||||
for (var p in updatedVertex) {
|
||||
(currentVertex as any)[p] = updatedVertex[p];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This kind of assumes saveVertexProperty is done from property panes.
|
||||
const hn = this.state.highlightedNode;
|
||||
let hn = this.state.highlightedNode;
|
||||
if (hn && hn.id === updatedVertex.id) {
|
||||
this.updatePropertiesPane(hn.id);
|
||||
}
|
||||
@@ -1716,7 +1708,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
igraphConfig?: IGraphConfig
|
||||
) {
|
||||
this.originalGraphData = graphData;
|
||||
const gd = JSON.parse(JSON.stringify(this.originalGraphData));
|
||||
let gd = JSON.parse(JSON.stringify(this.originalGraphData));
|
||||
if (!this.d3ForceGraph) {
|
||||
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
|
||||
return;
|
||||
@@ -1881,7 +1873,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
|
||||
promise
|
||||
.then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result))
|
||||
.catch((error: Error) => {
|
||||
.catch((error: any) => {
|
||||
const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||
this.setState({
|
||||
|
||||
@@ -58,7 +58,7 @@ export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> {
|
||||
className={className}
|
||||
as="tr"
|
||||
aria-label={node.caption}
|
||||
onActivated={() => this.props.onRootNodeSelected(node.id)}
|
||||
onActivated={(e) => this.props.onRootNodeSelected(node.id)}
|
||||
key={node.id}
|
||||
>
|
||||
<td className="resultItem">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
|
||||
import ExpandIcon from "../../../../images/Expand_14x14.svg";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||
|
||||
interface MiddlePaneComponentProps {
|
||||
isTabsContentExpanded: boolean;
|
||||
@@ -17,14 +17,7 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
|
||||
<div className="middlePane">
|
||||
<div className="graphTitle">
|
||||
<span className="paneTitle">Graph</span>
|
||||
<span
|
||||
className="graphExpandCollapseBtn pull-right"
|
||||
onClick={this.props.toggleExpandGraph}
|
||||
role="button"
|
||||
aria-expanded={this.props.isTabsContentExpanded}
|
||||
aria-name="View graph in full screen"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className="graphExpandCollapseBtn pull-right" onClick={this.props.toggleExpandGraph}>
|
||||
<img
|
||||
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
||||
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import * as Q from "q";
|
||||
import React from "react";
|
||||
import { GraphHighlightedNodeData, PossibleVertex } from "./GraphExplorer";
|
||||
import { Mode, NodePropertiesComponent, NodePropertiesComponentProps } from "./NodePropertiesComponent";
|
||||
import { NodePropertiesComponent, NodePropertiesComponentProps, Mode } from "./NodePropertiesComponent";
|
||||
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
|
||||
|
||||
describe("Property pane", () => {
|
||||
const title = "My Title";
|
||||
@@ -37,18 +37,17 @@ describe("Property pane", () => {
|
||||
return {
|
||||
expandedTitle: title,
|
||||
isCollapsed: false,
|
||||
onCollapsedChanged: jest.fn(),
|
||||
onCollapsedChanged: (newValue: boolean): void => {},
|
||||
node: highlightedNode,
|
||||
getPkIdFromNodeData: (): string => undefined,
|
||||
collectionPartitionKeyProperty: undefined,
|
||||
updateVertexProperties: (): Q.Promise<void> => Q.resolve(),
|
||||
selectNode: jest.fn(),
|
||||
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(undefined),
|
||||
possibleEdgeLabels: undefined,
|
||||
//eslint-disable-next-line
|
||||
editGraphEdges: (): Q.Promise<any> => Q.resolve(),
|
||||
deleteHighlightedNode: jest.fn(),
|
||||
onModeChanged: jest.fn(),
|
||||
getPkIdFromNodeData: (v: GraphHighlightedNodeData): string => null,
|
||||
collectionPartitionKeyProperty: null,
|
||||
updateVertexProperties: (editedProperties: EditedProperties): Q.Promise<void> => Q.resolve(),
|
||||
selectNode: (id: string): void => {},
|
||||
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(null),
|
||||
possibleEdgeLabels: null,
|
||||
editGraphEdges: (editedEdges: EditedEdges): Q.Promise<any> => Q.resolve(),
|
||||
deleteHighlightedNode: (): void => {},
|
||||
onModeChanged: (newMode: Mode): void => {},
|
||||
viewMode: Mode.READONLY_PROP,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ export class NodePropertiesComponent extends React.Component<
|
||||
super(props);
|
||||
this.state = {
|
||||
editedProperties: {
|
||||
pkId: undefined,
|
||||
pkId: null,
|
||||
readOnlyProperties: [],
|
||||
existingProperties: [],
|
||||
addedProperties: [],
|
||||
@@ -98,12 +98,15 @@ export class NodePropertiesComponent extends React.Component<
|
||||
};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps(props: NodePropertiesComponentProps): Partial<NodePropertiesComponentState> {
|
||||
public static getDerivedStateFromProps(
|
||||
props: NodePropertiesComponentProps,
|
||||
state: NodePropertiesComponentState
|
||||
): Partial<NodePropertiesComponentState> {
|
||||
if (props.viewMode !== Mode.READONLY_PROP) {
|
||||
return { isDeleteConfirm: false };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -135,10 +138,10 @@ export class NodePropertiesComponent extends React.Component<
|
||||
* @param value
|
||||
*/
|
||||
private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString {
|
||||
if (value === undefined) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
const type = typeof value;
|
||||
let type = typeof value;
|
||||
switch (type) {
|
||||
case "number":
|
||||
case "boolean":
|
||||
@@ -169,9 +172,10 @@ export class NodePropertiesComponent extends React.Component<
|
||||
];
|
||||
|
||||
const existingProps: ViewModels.InputProperty[] = [];
|
||||
|
||||
if (this.props.node.hasOwnProperty("properties")) {
|
||||
const hProps = this.props.node["properties"];
|
||||
for (const p in hProps) {
|
||||
for (let p in hProps) {
|
||||
const propValues = hProps[p];
|
||||
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({
|
||||
key: p,
|
||||
@@ -433,7 +437,7 @@ export class NodePropertiesComponent extends React.Component<
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
@@ -53,8 +54,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
|
||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,13 +31,28 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Button should be visible", () => {
|
||||
it("Account is not serverless - button should be visible", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
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", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import GitHubIcon from "../../../../images/github.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 ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
@@ -24,6 +25,7 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -34,6 +36,7 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
|
||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
|
||||
@@ -75,12 +78,10 @@ export function createStaticCommandBarButtons(
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
}
|
||||
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
if (
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
@@ -96,19 +97,22 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
|
||||
notebookButtons.forEach((btn) => {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
if (userContext.features.notebooksTemporarilyDown) {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
} else {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
@@ -163,7 +167,9 @@ export function createContextCommandBarButtons(
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
if (!userContext.features.notebooksTemporarilyDown) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
}
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
@@ -171,6 +177,13 @@ export function createContextCommandBarButtons(
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
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);
|
||||
}
|
||||
@@ -266,6 +279,10 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -292,13 +309,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
return {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: async () => {
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
if (throughputCap && throughputCap !== -1) {
|
||||
await useDatabases.getState().loadAllOffers();
|
||||
}
|
||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
|
||||
},
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
@@ -309,7 +321,6 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
const label = "New SQL Query";
|
||||
return {
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -324,7 +335,6 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
const label = "New Query";
|
||||
return {
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -411,7 +421,6 @@ function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentP
|
||||
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
id: "newNotebookBtn",
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.onNewNotebookClicked(),
|
||||
@@ -462,6 +471,33 @@ 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 {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
@@ -479,6 +515,9 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
"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 =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@@ -487,6 +526,13 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -501,6 +547,9 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
const label = "Open Cassandra Shell";
|
||||
const tooltip =
|
||||
"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 =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@@ -509,6 +558,13 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -539,7 +595,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
@@ -549,8 +605,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>
|
||||
);
|
||||
},
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
|
||||
@@ -13,8 +13,6 @@ import { StyleConstants } from "../../../Common/Constants";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { ConnectionStatus } from "./ConnectionStatusComponent";
|
||||
import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
|
||||
/**
|
||||
@@ -203,10 +201,3 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
||||
onRender: () => <MemoryTracker />,
|
||||
};
|
||||
};
|
||||
|
||||
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <ConnectionStatus container={container} />,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
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"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<img
|
||||
@@ -205,9 +205,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
||||
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
||||
<span className="date">{item.date}</span>
|
||||
<span className="message" role="alert" aria-live="assertive">
|
||||
{item.message}
|
||||
</span>
|
||||
<span className="message">{item.message}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
aria-label="console button expanded"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -236,7 +236,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
aria-label="console button expanded"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -340,9 +340,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
date
|
||||
</span>
|
||||
<span
|
||||
aria-live="assertive"
|
||||
className="message"
|
||||
role="alert"
|
||||
>
|
||||
message
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Link } from "@fluentui/react";
|
||||
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
|
||||
// Vendor modules
|
||||
import {
|
||||
@@ -15,15 +13,13 @@ import "@nteract/styles/editor-overrides.css";
|
||||
import "@nteract/styles/global-variables.css";
|
||||
import "codemirror/addon/hint/show-hint.css";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import { Notebook } from "Common/Constants";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import * as Immutable from "immutable";
|
||||
import * as React from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import "react-table/react-table.css";
|
||||
import { AnyAction, Store } from "redux";
|
||||
import { NotebookClientV2 } from "../NotebookClientV2";
|
||||
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import * as NteractUtil from "../NTeractUtil";
|
||||
import * as CdbActions from "./actions";
|
||||
import { NotebookComponent } from "./NotebookComponent";
|
||||
@@ -34,19 +30,6 @@ export interface NotebookComponentBootstrapperOptions {
|
||||
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 {
|
||||
public contentRef: ContentRef;
|
||||
protected renderExtraComponent: () => JSX.Element;
|
||||
@@ -58,7 +41,7 @@ export class NotebookComponentBootstrapper {
|
||||
this.contentRef = options.contentRef;
|
||||
}
|
||||
|
||||
protected static wrapModelIntoContent(name: string, path: string, content: unknown): IWrapModel {
|
||||
protected static wrapModelIntoContent(name: string, path: string, content: any) {
|
||||
return {
|
||||
name,
|
||||
path,
|
||||
@@ -66,7 +49,7 @@ export class NotebookComponentBootstrapper {
|
||||
created: "",
|
||||
content,
|
||||
format: "json",
|
||||
mimetype: undefined,
|
||||
mimetype: null as any,
|
||||
size: 0,
|
||||
writeable: false,
|
||||
type: "notebook",
|
||||
@@ -102,11 +85,7 @@ export class NotebookComponentBootstrapper {
|
||||
};
|
||||
}
|
||||
|
||||
public getNotebookPath(): string {
|
||||
return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath;
|
||||
}
|
||||
|
||||
public setContent(name: string, content: unknown): void {
|
||||
public setContent(name: string, content: any): void {
|
||||
this.getStore().dispatch(
|
||||
actions.fetchContentFulfilled({
|
||||
filepath: undefined,
|
||||
@@ -137,32 +116,11 @@ export class NotebookComponentBootstrapper {
|
||||
|
||||
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
|
||||
public notebookSave(): void {
|
||||
if (
|
||||
NotebookUtil.getContentProviderType(this.getNotebookPath()) ===
|
||||
NotebookContentProviderType.JupyterContentProviderType
|
||||
) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.saveNotebookModalTitle,
|
||||
undefined,
|
||||
"Save",
|
||||
async () => {
|
||||
this.getStore().dispatch(
|
||||
actions.save({
|
||||
contentRef: this.contentRef,
|
||||
})
|
||||
);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getSaveNotebookSubText()
|
||||
);
|
||||
} else {
|
||||
this.getStore().dispatch(
|
||||
actions.save({
|
||||
contentRef: this.contentRef,
|
||||
})
|
||||
);
|
||||
}
|
||||
this.getStore().dispatch(
|
||||
actions.save({
|
||||
contentRef: this.contentRef,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public notebookChangeKernel(kernelSpecName: string): void {
|
||||
@@ -312,6 +270,7 @@ export class NotebookComponentBootstrapper {
|
||||
public isContentDirty(): boolean {
|
||||
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
||||
if (!content) {
|
||||
console.log("No error");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -369,19 +328,4 @@ export class NotebookComponentBootstrapper {
|
||||
protected getStore(): Store<AppState, AnyAction> {
|
||||
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,12 +12,11 @@ import {
|
||||
ServerConfig as JupyterServerConfig,
|
||||
} from "@nteract/core";
|
||||
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { RecordOf } from "immutable";
|
||||
import { Action, AnyAction } from "redux";
|
||||
import { AnyAction } from "redux";
|
||||
import { ofType, StateObservable } from "redux-observable";
|
||||
import { kernels, sessions } from "rx-jupyter";
|
||||
import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||
import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
@@ -42,7 +41,7 @@ import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationCons
|
||||
import { useDialog } from "../../Controls/Dialog";
|
||||
import * as FileSystemUtil from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import * as CdbActions from "./actions";
|
||||
import * as TextFile from "./contents/file/text-file";
|
||||
import { CdbAppState } from "./types";
|
||||
@@ -110,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
|
||||
const q = params.toString();
|
||||
const suffix = q !== "" ? `?${q}` : "";
|
||||
|
||||
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||
|
||||
return url.replace(/^http(s)?/, "ws$1");
|
||||
};
|
||||
@@ -949,54 +948,6 @@ 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 = [
|
||||
addInitialCodeCellEpic,
|
||||
focusInitialCodeCellEpic,
|
||||
@@ -1014,5 +965,4 @@ export const allEpics = [
|
||||
traceNotebookInfoEpic,
|
||||
traceNotebookKernelEpic,
|
||||
resetCellStatusOnExecuteCanceledEpic,
|
||||
autoSaveCurrentContentEpic,
|
||||
];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
|
||||
import { AppState, epics as coreEpics, reducers, IContentProvider } 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 { 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";
|
||||
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
@@ -81,6 +81,7 @@ export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] =>
|
||||
// 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.
|
||||
const filteredCoreEpics = [
|
||||
coreEpics.autoSaveCurrentContentEpic,
|
||||
coreEpics.executeCellEpic,
|
||||
coreEpics.executeFocusedCellEpic,
|
||||
coreEpics.executeCellAfterKernelLaunchEpic,
|
||||
|
||||
@@ -1,65 +1,48 @@
|
||||
/**
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { IPhoenixConnectionInfoResult, IProvisionData, IResponse } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
export class NotebookContainerClient {
|
||||
private clearReconnectionAttemptMessage? = () => {};
|
||||
private isResettingWorkspace: boolean;
|
||||
private phoenixClient: PhoenixClient;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private scheduleTimerId: NodeJS.Timeout;
|
||||
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
this.phoenixClient = new PhoenixClient();
|
||||
this.retryOptions = {
|
||||
retries: Notebook.retryAttempts,
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
|
||||
this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
|
||||
private initHeartbeat(delayMs: number): void {
|
||||
this.scheduleHeartbeat(delayMs);
|
||||
|
||||
useNotebook.subscribe(
|
||||
() => this.scheduleHeartbeat(delayMs),
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
}
|
||||
|
||||
private scheduleHeartbeat(delayMs: number) {
|
||||
if (this.scheduleTimerId) {
|
||||
clearInterval(this.scheduleTimerId);
|
||||
}
|
||||
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleTimerId = setInterval(async () => {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
const memoryUsageInfo = await this.getMemoryUsage();
|
||||
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||
}
|
||||
}, delayMs);
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
} else {
|
||||
const unsub = useNotebook.subscribe(
|
||||
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
if (newServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
unsub();
|
||||
},
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
/**
|
||||
* Heartbeat: each ping schedules another ping
|
||||
*/
|
||||
private scheduleHeartbeat(delayMs: number): void {
|
||||
setTimeout(() => {
|
||||
this.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
|
||||
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
@@ -73,28 +56,7 @@ export class NotebookContainerClient {
|
||||
|
||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||
try {
|
||||
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`, {
|
||||
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: authToken,
|
||||
@@ -113,36 +75,31 @@ export class NotebookContainerClient {
|
||||
freeKB: memoryUsageInfo.free,
|
||||
};
|
||||
}
|
||||
} else if (response.status === HttpStatusCodes.NotFound) {
|
||||
throw new AbortError(response.statusText);
|
||||
}
|
||||
throw new Error(response.statusText);
|
||||
} else {
|
||||
return undefined;
|
||||
} 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 shouldExecuteMemoryCall(): boolean {
|
||||
return (
|
||||
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
|
||||
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
|
||||
);
|
||||
}
|
||||
|
||||
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||
public async resetWorkspace(): Promise<void> {
|
||||
this.isResettingWorkspace = true;
|
||||
let response: IResponse<IPhoenixConnectionInfoResult>;
|
||||
try {
|
||||
response = await this._resetWorkspace();
|
||||
await this._resetWorkspace();
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
return response;
|
||||
}
|
||||
this.isResettingWorkspace = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
private async _resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||
private async _resetWorkspace(): Promise<void> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
@@ -150,28 +107,15 @@ export class NotebookContainerClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||
try {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
return await this.phoenixClient.resetContainer(provisionData);
|
||||
}
|
||||
return null;
|
||||
await fetch(`${notebookServerEndpoint}/api/shutdown`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: authToken },
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Connection Failed",
|
||||
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
await this.recreateNotebookWorkspaceAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,11 +129,22 @@ export class NotebookContainerClient {
|
||||
};
|
||||
}
|
||||
|
||||
private getHeaders(): HeadersInit {
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
return {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
};
|
||||
private async recreateNotebookWorkspaceAsync(): Promise<void> {
|
||||
const { databaseAccount } = userContext;
|
||||
if (!databaseAccount?.id) {
|
||||
throw new Error("DataExplorer not initialized");
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||