mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 17:30:46 +00:00
Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migration/move-document-tab-to-react
This commit is contained in:
15
.env.example
15
.env.example
@@ -1,16 +1 @@
|
|||||||
PORTAL_RUNNER_USERNAME=
|
|
||||||
PORTAL_RUNNER_PASSWORD=
|
|
||||||
PORTAL_RUNNER_SUBSCRIPTION=
|
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
|
||||||
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT=
|
|
||||||
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY=
|
|
||||||
PORTAL_RUNNER_CONNECTION_STRING=
|
|
||||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
|
||||||
CASSANDRA_CONNECTION_STRING=
|
|
||||||
MONGO_CONNECTION_STRING=
|
|
||||||
TABLES_CONNECTION_STRING=
|
|
||||||
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
|
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
|
||||||
@@ -71,7 +71,6 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
|
|||||||
src/Explorer/DataSamples/ContainerSampleGenerator.ts
|
src/Explorer/DataSamples/ContainerSampleGenerator.ts
|
||||||
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
||||||
src/Explorer/DataSamples/DataSamplesUtil.ts
|
src/Explorer/DataSamples/DataSamplesUtil.ts
|
||||||
src/Explorer/Explorer.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
|
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
|
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
|
||||||
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
|
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
|
||||||
@@ -83,11 +82,6 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts
|
|||||||
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
|
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
|
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
|
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
|
||||||
# src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts
|
|
||||||
# src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts
|
|
||||||
|
|
||||||
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
|
|
||||||
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
|
|
||||||
src/Explorer/Menus/ContextMenu.ts
|
src/Explorer/Menus/ContextMenu.ts
|
||||||
src/Explorer/MostRecentActivity/MostRecentActivity.ts
|
src/Explorer/MostRecentActivity/MostRecentActivity.ts
|
||||||
src/Explorer/Notebook/NotebookClientV2.ts
|
src/Explorer/Notebook/NotebookClientV2.ts
|
||||||
@@ -141,7 +135,6 @@ src/Explorer/Tabs/TabsBase.ts
|
|||||||
src/Explorer/Tabs/TriggerTab.ts
|
src/Explorer/Tabs/TriggerTab.ts
|
||||||
src/Explorer/Tabs/UserDefinedFunctionTab.ts
|
src/Explorer/Tabs/UserDefinedFunctionTab.ts
|
||||||
src/Explorer/Tree/AccessibleVerticalList.ts
|
src/Explorer/Tree/AccessibleVerticalList.ts
|
||||||
src/Explorer/Tree/Collection.test.ts
|
|
||||||
src/Explorer/Tree/Collection.ts
|
src/Explorer/Tree/Collection.ts
|
||||||
src/Explorer/Tree/ConflictId.ts
|
src/Explorer/Tree/ConflictId.ts
|
||||||
src/Explorer/Tree/DocumentId.ts
|
src/Explorer/Tree/DocumentId.ts
|
||||||
@@ -150,65 +143,32 @@ src/Explorer/Tree/ResourceTokenCollection.ts
|
|||||||
src/Explorer/Tree/StoredProcedure.ts
|
src/Explorer/Tree/StoredProcedure.ts
|
||||||
src/Explorer/Tree/TreeComponents.ts
|
src/Explorer/Tree/TreeComponents.ts
|
||||||
src/Explorer/Tree/Trigger.ts
|
src/Explorer/Tree/Trigger.ts
|
||||||
src/Explorer/Tree/UserDefinedFunction.ts
|
|
||||||
src/Explorer/WaitsForTemplateViewModel.ts
|
src/Explorer/WaitsForTemplateViewModel.ts
|
||||||
src/GitHub/GitHubClient.test.ts
|
src/GitHub/GitHubClient.test.ts
|
||||||
src/GitHub/GitHubClient.ts
|
src/GitHub/GitHubClient.ts
|
||||||
src/GitHub/GitHubConnector.ts
|
src/GitHub/GitHubConnector.ts
|
||||||
src/GitHub/GitHubContentProvider.test.ts
|
|
||||||
src/GitHub/GitHubContentProvider.ts
|
|
||||||
src/GitHub/GitHubOAuthService.ts
|
src/GitHub/GitHubOAuthService.ts
|
||||||
src/Index.ts
|
src/Index.ts
|
||||||
src/Juno/JunoClient.test.ts
|
src/Juno/JunoClient.test.ts
|
||||||
src/Juno/JunoClient.ts
|
src/Juno/JunoClient.ts
|
||||||
src/Platform/Hosted/Authorization.ts
|
src/Platform/Hosted/Authorization.ts
|
||||||
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
|
|
||||||
src/ReactDevTools.ts
|
src/ReactDevTools.ts
|
||||||
src/Shared/Constants.ts
|
src/Shared/Constants.ts
|
||||||
src/Shared/DefaultExperienceUtility.test.ts
|
src/Shared/DefaultExperienceUtility.test.ts
|
||||||
src/Shared/DefaultExperienceUtility.ts
|
src/Shared/DefaultExperienceUtility.ts
|
||||||
src/Shared/ExplorerSettings.ts
|
|
||||||
src/Shared/PriceEstimateCalculator.ts
|
|
||||||
src/Shared/StorageUtility.test.ts
|
|
||||||
src/Shared/StorageUtility.ts
|
|
||||||
src/Shared/appInsights.ts
|
src/Shared/appInsights.ts
|
||||||
src/SparkClusterManager/ArcadiaResourceManager.ts
|
src/SparkClusterManager/ArcadiaResourceManager.ts
|
||||||
src/SparkClusterManager/SparkClusterManager.ts
|
src/SparkClusterManager/SparkClusterManager.ts
|
||||||
src/Terminal/JupyterLabAppFactory.ts
|
src/Terminal/JupyterLabAppFactory.ts
|
||||||
src/Terminal/NotebookAppContracts.d.ts
|
src/Terminal/NotebookAppContracts.d.ts
|
||||||
src/Terminal/index.ts
|
|
||||||
src/TokenProviders/PortalTokenProvider.ts
|
|
||||||
src/TokenProviders/TokenProviderFactory.ts
|
|
||||||
src/Utils/PricingUtils.test.ts
|
|
||||||
src/Utils/QueryUtils.test.ts
|
|
||||||
src/applyExplorerBindings.ts
|
src/applyExplorerBindings.ts
|
||||||
src/global.d.ts
|
src/global.d.ts
|
||||||
src/setupTests.ts
|
src/setupTests.ts
|
||||||
src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx
|
|
||||||
src/Explorer/Controls/Accordion/AccordionComponent.tsx
|
|
||||||
src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx
|
|
||||||
src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx
|
|
||||||
src/Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter.tsx
|
|
||||||
src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx
|
|
||||||
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanel.tsx
|
|
||||||
src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx
|
|
||||||
src/Explorer/Controls/DialogReactComponent/DialogComponent.tsx
|
|
||||||
src/Explorer/Controls/DialogReactComponent/DialogComponentAdapter.tsx
|
|
||||||
src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx
|
|
||||||
src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx
|
|
||||||
src/Explorer/Controls/Directory/DirectoryComponentAdapter.tsx
|
|
||||||
src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx
|
|
||||||
src/Explorer/Controls/Directory/DirectoryListComponent.tsx
|
|
||||||
src/Explorer/Controls/Editor/EditorReact.tsx
|
|
||||||
src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
|
src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
|
||||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
|
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
|
||||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
||||||
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx
|
|
||||||
src/NotebookViewer/NotebookViewer.tsx
|
|
||||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||||
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
|
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
|
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
|
||||||
@@ -216,43 +176,19 @@ src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
|
|||||||
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
|
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
|
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
|
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx
|
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
|
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
|
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
|
||||||
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
|
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
|
||||||
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx
|
|
||||||
src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx
|
|
||||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
|
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
|
||||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
|
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
|
||||||
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
|
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
|
||||||
src/Explorer/Notebook/NotebookComponent/contents/file/index.tsx
|
|
||||||
src/Explorer/Notebook/NotebookComponent/contents/file/text-file.tsx
|
|
||||||
src/Explorer/Notebook/NotebookComponent/contents/index.tsx
|
src/Explorer/Notebook/NotebookComponent/contents/index.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
|
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
|
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/Prompt.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/StatusBar.test.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/Toolbar.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/HoverableCell.tsx
|
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
|
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx
|
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx
|
||||||
src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
|
src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
|
||||||
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
||||||
src/Explorer/Notebook/temp/inputs/editor.tsx
|
|
||||||
src/Explorer/Notebook/temp/markdown-cell.tsx
|
|
||||||
src/Explorer/Notebook/temp/source.tsx
|
|
||||||
src/Explorer/Notebook/temp/syntax-highlighter/index.tsx
|
|
||||||
src/Explorer/SplashScreen/SplashScreen.tsx
|
|
||||||
src/Explorer/Tabs/GalleryTab.tsx
|
|
||||||
src/Explorer/Tabs/NotebookViewerTab.tsx
|
|
||||||
src/Explorer/Tabs/TerminalTab.tsx
|
|
||||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
|
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
src/Explorer/Tree/ResourceTree.tsx
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
||||||
"enableSchemaAnalyzer": true
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
.dataResourceTree {
|
.dataResourceTree {
|
||||||
margin-left: @MediumSpace;
|
margin-left: @MediumSpace;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
.databaseHeader {
|
.databaseHeader {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
@import "./Common/Constants";
|
@import "./Common/Constants";
|
||||||
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 20%;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
.main {
|
.main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -46,14 +44,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contextmenushowing {
|
.contextmenushowing {
|
||||||
background-color: #EEE;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collectionstree {
|
.collectionstree {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: @DefaultSpace;
|
margin-top: @DefaultSpace;
|
||||||
|
|
||||||
|
|
||||||
.databaseList {
|
.databaseList {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
@@ -192,7 +189,7 @@ img.collectionsTreeCollapseExpand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.expanded::before {
|
.expanded::before {
|
||||||
content: '\23F7';
|
content: "\23F7";
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
@@ -211,7 +208,7 @@ img.collectionsTreeCollapseExpand {
|
|||||||
transform: rotate(-90deg) translateX(-100%);
|
transform: rotate(-90deg) translateX(-100%);
|
||||||
-webkit-transform: rotate(-90deg) translateX(-100%);
|
-webkit-transform: rotate(-90deg) translateX(-100%);
|
||||||
-ms-transform: rotate(-90deg) translateX(-100%);
|
-ms-transform: rotate(-90deg) translateX(-100%);
|
||||||
border-bottom: 1px solid #CCC;
|
border-bottom: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav-img {
|
.main-nav-img {
|
||||||
|
|||||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -5583,6 +5583,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
|
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
|
||||||
},
|
},
|
||||||
|
"@types/lodash": {
|
||||||
|
"version": "4.14.171",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz",
|
||||||
|
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg=="
|
||||||
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||||
@@ -20618,9 +20623,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"version": "1.10.0",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.13.0.tgz",
|
||||||
"integrity": "sha512-b7SGBcCPq4W3pb4ImEDmNXtO0ZkJbZMuWiShsaNJd+rGfY/6fqwgllsAojmxGSgFmijYw7WxCoPiAIEDIH16Kw==",
|
"integrity": "sha512-GA5OyEeKx1v/pRcANmYncCT67Y7Y4N5zLRU5E690dn/Id10sooR5hQZmCDYsjXlutZb/1q0R3sITALnvhEjCjg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"commander": "^6.1.0",
|
"commander": "^6.1.0",
|
||||||
@@ -20635,7 +20640,8 @@
|
|||||||
"proxy-from-env": "^1.1.0",
|
"proxy-from-env": "^1.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stack-utils": "^2.0.3",
|
"stack-utils": "^2.0.3",
|
||||||
"ws": "^7.3.1"
|
"ws": "^7.4.6",
|
||||||
|
"yazl": "^2.5.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": {
|
"commander": {
|
||||||
@@ -20667,6 +20673,12 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"escape-string-regexp": "^2.0.0"
|
"escape-string-regexp": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "7.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
|
||||||
|
"integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -26157,6 +26169,15 @@
|
|||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"yazl": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"buffer-crc32": "~0.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"yocto-queue": {
|
"yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"@octokit/rest": "17.9.2",
|
"@octokit/rest": "17.9.2",
|
||||||
"@phosphor/widgets": "1.9.3",
|
"@phosphor/widgets": "1.9.3",
|
||||||
"@testing-library/jest-dom": "5.11.9",
|
"@testing-library/jest-dom": "5.11.9",
|
||||||
|
"@types/lodash": "4.14.171",
|
||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
@@ -163,7 +164,7 @@
|
|||||||
"mini-css-extract-plugin": "0.4.3",
|
"mini-css-extract-plugin": "0.4.3",
|
||||||
"monaco-editor-webpack-plugin": "1.7.0",
|
"monaco-editor-webpack-plugin": "1.7.0",
|
||||||
"node-fetch": "2.6.1",
|
"node-fetch": "2.6.1",
|
||||||
"playwright": "1.10.0",
|
"playwright": "1.13.0",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"react-dev-utils": "11.0.4",
|
"react-dev-utils": "11.0.4",
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export class Flights {
|
|||||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||||
public static readonly MongoIndexing = "mongoindexing";
|
public static readonly MongoIndexing = "mongoindexing";
|
||||||
public static readonly AutoscaleTest = "autoscaletest";
|
public static readonly AutoscaleTest = "autoscaletest";
|
||||||
public static readonly SchemaAnalyzer = "schemaanalyzer";
|
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AfecFeatures {
|
export class AfecFeatures {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as HeadersUtility from "./HeadersUtility";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||||
|
import * as HeadersUtility from "./HeadersUtility";
|
||||||
|
|
||||||
describe("Headers Utility", () => {
|
describe("Headers Utility", () => {
|
||||||
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {
|
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ import React, { FunctionComponent } from "react";
|
|||||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
|
import Explorer from "../Explorer/Explorer";
|
||||||
|
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||||
|
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export interface ResourceTreeProps {
|
export interface ResourceTreeContainerProps {
|
||||||
toggleLeftPaneExpanded: () => void;
|
toggleLeftPaneExpanded: () => void;
|
||||||
isLeftPaneExpanded: boolean;
|
isLeftPaneExpanded: boolean;
|
||||||
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
|
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
|
||||||
toggleLeftPaneExpanded,
|
toggleLeftPaneExpanded,
|
||||||
isLeftPaneExpanded,
|
isLeftPaneExpanded,
|
||||||
}: ResourceTreeProps): JSX.Element => {
|
container,
|
||||||
|
}: ResourceTreeContainerProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||||
{/* Collections Window - - Start */}
|
{/* Collections Window - - Start */}
|
||||||
@@ -48,9 +53,11 @@ export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{userContext.authType === AuthType.ResourceToken ? (
|
{userContext.authType === AuthType.ResourceToken ? (
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
|
<ResourceTokenTree />
|
||||||
) : (
|
) : userContext.features.enableKOResourceTree ? (
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||||
|
) : (
|
||||||
|
<ResourceTree container={container} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Collections Window - End */}
|
{/* Collections Window - End */}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
jest.mock("../../Utils/arm/request");
|
jest.mock("../../Utils/arm/request");
|
||||||
jest.mock("../CosmosClient");
|
jest.mock("../CosmosClient");
|
||||||
|
import ko from "knockout";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
|
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { Database } from "../../Contracts/ViewModels";
|
||||||
|
import { useDatabases } from "../../Explorer/useDatabases";
|
||||||
import { updateUserContext } from "../../UserContext";
|
import { updateUserContext } from "../../UserContext";
|
||||||
import { armRequest } from "../../Utils/arm/request";
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
@@ -23,6 +26,15 @@ describe("createCollection", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
apiType: "SQL",
|
apiType: "SQL",
|
||||||
});
|
});
|
||||||
|
useDatabases.setState({
|
||||||
|
databases: [
|
||||||
|
{
|
||||||
|
id: ko.observable("testDatabase"),
|
||||||
|
loadCollections: () => undefined,
|
||||||
|
collections: ko.observableArray([]),
|
||||||
|
} as Database,
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call ARM if logged in with AAD", async () => {
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
|
|||||||
@@ -4,20 +4,16 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai
|
|||||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { useDatabases } from "../../Explorer/useDatabases";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import {
|
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||||
createUpdateCassandraTable,
|
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
getCassandraTable,
|
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import {
|
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||||
createUpdateMongoDBCollection,
|
|
||||||
getMongoDBCollection,
|
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
|
||||||
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
|
||||||
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
|
||||||
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
@@ -59,6 +55,16 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||||
|
if (!params.createNewDatabase) {
|
||||||
|
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
|
||||||
|
if (!isValid) {
|
||||||
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||||
|
throw new Error(
|
||||||
|
`Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { apiType } = userContext;
|
const { apiType } = userContext;
|
||||||
switch (apiType) {
|
switch (apiType) {
|
||||||
case "SQL":
|
case "SQL":
|
||||||
@@ -77,23 +83,6 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||||
try {
|
|
||||||
const getResponse = await getSqlContainer(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId,
|
|
||||||
params.collectionId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||||
const resource: ARMTypes.SqlContainerResource = {
|
const resource: ARMTypes.SqlContainerResource = {
|
||||||
id: params.collectionId,
|
id: params.collectionId,
|
||||||
@@ -131,23 +120,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
|||||||
|
|
||||||
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||||
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }];
|
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }];
|
||||||
try {
|
|
||||||
const getResponse = await getMongoDBCollection(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId,
|
|
||||||
params.collectionId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||||
const resource: ARMTypes.MongoDBCollectionResource = {
|
const resource: ARMTypes.MongoDBCollectionResource = {
|
||||||
id: params.collectionId,
|
id: params.collectionId,
|
||||||
@@ -189,23 +161,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||||
try {
|
|
||||||
const getResponse = await getCassandraTable(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId,
|
|
||||||
params.collectionId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||||
const resource: ARMTypes.CassandraTableResource = {
|
const resource: ARMTypes.CassandraTableResource = {
|
||||||
id: params.collectionId,
|
id: params.collectionId,
|
||||||
@@ -233,23 +188,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams):
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||||
try {
|
|
||||||
const getResponse = await getGremlinGraph(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId,
|
|
||||||
params.collectionId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||||
const resource: ARMTypes.GremlinGraphResource = {
|
const resource: ARMTypes.GremlinGraphResource = {
|
||||||
id: params.collectionId,
|
id: params.collectionId,
|
||||||
@@ -284,22 +222,6 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise<D
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||||
try {
|
|
||||||
const getResponse = await getTable(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.collectionId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||||
const resource: ARMTypes.TableResource = {
|
const resource: ARMTypes.TableResource = {
|
||||||
id: params.collectionId,
|
id: params.collectionId,
|
||||||
|
|||||||
@@ -2,20 +2,13 @@ import { DatabaseResponse } from "@azure/cosmos";
|
|||||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { useDatabases } from "../../Explorer/useDatabases";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import {
|
import { getDatabaseName } from "../../Utils/APITypeUtils";
|
||||||
createUpdateCassandraKeyspace,
|
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
getCassandraKeyspace,
|
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import {
|
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
createUpdateGremlinDatabase,
|
|
||||||
getGremlinDatabase,
|
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
|
||||||
import {
|
|
||||||
createUpdateMongoDBDatabase,
|
|
||||||
getMongoDBDatabase,
|
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
|
||||||
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
|
||||||
import {
|
import {
|
||||||
CassandraKeyspaceCreateUpdateParameters,
|
CassandraKeyspaceCreateUpdateParameters,
|
||||||
CreateUpdateOptions,
|
CreateUpdateOptions,
|
||||||
@@ -48,6 +41,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
|
||||||
|
const databaseName = getDatabaseName().toLocaleLowerCase();
|
||||||
|
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
const { apiType } = userContext;
|
const { apiType } = userContext;
|
||||||
|
|
||||||
switch (apiType) {
|
switch (apiType) {
|
||||||
@@ -65,22 +63,6 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
try {
|
|
||||||
const getResponse = await getSqlDatabase(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -101,22 +83,6 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
try {
|
|
||||||
const getResponse = await getMongoDBDatabase(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
|
const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -137,22 +103,6 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
try {
|
|
||||||
const getResponse = await getCassandraKeyspace(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
|
const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -173,22 +123,6 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
try {
|
|
||||||
const getResponse = await getGremlinDatabase(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId
|
|
||||||
);
|
|
||||||
if (getResponse?.properties?.resource) {
|
|
||||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
const rpPayload: GremlinDatabaseCreateUpdateParameters = {
|
const rpPayload: GremlinDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export interface ConfigContext {
|
|||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
allowedJunoOrigins: string[];
|
allowedJunoOrigins: string[];
|
||||||
enableSchemaAnalyzer: boolean;
|
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
"https://tools-staging.cosmos.azure.com",
|
"https://tools-staging.cosmos.azure.com",
|
||||||
"https://localhost",
|
"https://localhost",
|
||||||
],
|
],
|
||||||
enableSchemaAnalyzer: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface DatabaseAccount {
|
|||||||
|
|
||||||
export interface DatabaseAccountExtendedProperties {
|
export interface DatabaseAccountExtendedProperties {
|
||||||
documentEndpoint?: string;
|
documentEndpoint?: string;
|
||||||
|
disableLocalAuth?: boolean;
|
||||||
tableEndpoint?: string;
|
tableEndpoint?: string;
|
||||||
gremlinEndpoint?: string;
|
gremlinEndpoint?: string;
|
||||||
cassandraEndpoint?: string;
|
cassandraEndpoint?: string;
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const createCollectionContextMenuButton = (
|
|||||||
iconSrc: AddUdfIcon,
|
iconSrc: AddUdfIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined);
|
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||||
},
|
},
|
||||||
label: "New UDF",
|
label: "New UDF",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
|||||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
|
|
||||||
export interface AccordionComponentProps {}
|
export interface AccordionComponentProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export class AccordionComponent extends React.Component<AccordionComponentProps> {
|
export class AccordionComponent extends React.Component<AccordionComponentProps> {
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
@@ -78,7 +80,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onHeaderClick = (_event: React.MouseEvent<HTMLDivElement>): void => {
|
private onHeaderClick = (): void => {
|
||||||
this.setState({ isExpanded: !this.state.isExpanded });
|
this.setState({ isExpanded: !this.state.isExpanded });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -121,8 +121,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
|
|||||||
if (!this.dropdownElt || !this.expandButtonElt) {
|
if (!this.dropdownElt || !this.expandButtonElt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||||
const dropdownElt = $(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeyPress(event: React.KeyboardEvent): boolean {
|
private onKeyPress(event: React.KeyboardEvent): boolean {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
const queryEditorModel = this.editor.getModel();
|
const queryEditorModel = this.editor.getModel();
|
||||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||||
queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => {
|
queryEditorModel.onDidChangeContent(() => {
|
||||||
const queryEditorModel = this.editor.getModel();
|
const queryEditorModel = this.editor.getModel();
|
||||||
this.props.onContentChanged(queryEditorModel.getValue());
|
this.props.onContentChanged(queryEditorModel.getValue());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={"paneMainContent"}>{content}</div>
|
<div>{content}</div>
|
||||||
{!this.props.showAuthorizeAccess && (
|
{!this.props.showAuthorizeAccess && (
|
||||||
<>
|
<>
|
||||||
<div className={"paneFooter"} style={ContentFooterStyle}>
|
<div className={"paneFooter"} style={ContentFooterStyle}>
|
||||||
|
|||||||
@@ -1,154 +1,90 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import { NotebookTerminalComponent } from "./NotebookTerminalComponent";
|
import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent";
|
||||||
|
|
||||||
const createTestDatabaseAccount = (): DataModels.DatabaseAccount => {
|
const testAccount: DataModels.DatabaseAccount = {
|
||||||
return {
|
id: "id",
|
||||||
id: "testId",
|
kind: "kind",
|
||||||
kind: "testKind",
|
location: "location",
|
||||||
location: "testLocation",
|
name: "name",
|
||||||
name: "testName",
|
|
||||||
properties: {
|
properties: {
|
||||||
cassandraEndpoint: null,
|
|
||||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||||
gremlinEndpoint: null,
|
|
||||||
tableEndpoint: null,
|
|
||||||
},
|
},
|
||||||
type: "testType",
|
type: "type",
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTestMongo32DatabaseAccount = (): DataModels.DatabaseAccount => {
|
const testMongo32Account: DataModels.DatabaseAccount = {
|
||||||
return {
|
...testAccount,
|
||||||
id: "testId",
|
|
||||||
kind: "testKind",
|
|
||||||
location: "testLocation",
|
|
||||||
name: "testName",
|
|
||||||
properties: {
|
|
||||||
cassandraEndpoint: null,
|
|
||||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
|
||||||
gremlinEndpoint: null,
|
|
||||||
tableEndpoint: null,
|
|
||||||
},
|
|
||||||
type: "testType",
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTestMongo36DatabaseAccount = (): DataModels.DatabaseAccount => {
|
const testMongo36Account: DataModels.DatabaseAccount = {
|
||||||
return {
|
...testAccount,
|
||||||
id: "testId",
|
|
||||||
kind: "testKind",
|
|
||||||
location: "testLocation",
|
|
||||||
name: "testName",
|
|
||||||
properties: {
|
properties: {
|
||||||
cassandraEndpoint: null,
|
|
||||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
|
||||||
gremlinEndpoint: null,
|
|
||||||
tableEndpoint: null,
|
|
||||||
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
|
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
|
||||||
},
|
},
|
||||||
type: "testType",
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => {
|
const testCassandraAccount: DataModels.DatabaseAccount = {
|
||||||
return {
|
...testAccount,
|
||||||
id: "testId",
|
|
||||||
kind: "testKind",
|
|
||||||
location: "testLocation",
|
|
||||||
name: "testName",
|
|
||||||
properties: {
|
properties: {
|
||||||
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
|
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
|
||||||
documentEndpoint: null,
|
|
||||||
gremlinEndpoint: null,
|
|
||||||
tableEndpoint: null,
|
|
||||||
},
|
},
|
||||||
type: "testType",
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTerminal = (): NotebookTerminalComponent => {
|
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
return new NotebookTerminalComponent({
|
authToken: "authToken",
|
||||||
notebookServerInfo: {
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
||||||
authToken: "testAuthToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/",
|
|
||||||
},
|
|
||||||
databaseAccount: createTestDatabaseAccount(),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMongo32Terminal = (): NotebookTerminalComponent => {
|
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
return new NotebookTerminalComponent({
|
authToken: "authToken",
|
||||||
notebookServerInfo: {
|
|
||||||
authToken: "testAuthToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||||
},
|
|
||||||
databaseAccount: createTestMongo32DatabaseAccount(),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMongo36Terminal = (): NotebookTerminalComponent => {
|
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
return new NotebookTerminalComponent({
|
authToken: "authToken",
|
||||||
notebookServerInfo: {
|
|
||||||
authToken: "testAuthToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
|
||||||
},
|
|
||||||
databaseAccount: createTestMongo36DatabaseAccount(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createCassandraTerminal = (): NotebookTerminalComponent => {
|
|
||||||
return new NotebookTerminalComponent({
|
|
||||||
notebookServerInfo: {
|
|
||||||
authToken: "testAuthToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||||
},
|
|
||||||
databaseAccount: createTestCassandraDatabaseAccount(),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("NotebookTerminalComponent", () => {
|
describe("NotebookTerminalComponent", () => {
|
||||||
it("getTerminalParams: Test for terminal", () => {
|
it("renders terminal", () => {
|
||||||
const terminal: NotebookTerminalComponent = createTerminal();
|
const props: NotebookTerminalComponentProps = {
|
||||||
const params: Map<string, string> = terminal.getTerminalParams();
|
databaseAccount: testAccount,
|
||||||
|
notebookServerInfo: testNotebookServerInfo,
|
||||||
|
};
|
||||||
|
|
||||||
expect(params).toEqual(
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
new Map<string, string>([["terminal", "true"]])
|
expect(wrapper).toMatchSnapshot();
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getTerminalParams: Test for Mongo 3.2 terminal", () => {
|
it("renders mongo 3.2 shell", () => {
|
||||||
const terminal: NotebookTerminalComponent = createMongo32Terminal();
|
const props: NotebookTerminalComponentProps = {
|
||||||
const params: Map<string, string> = terminal.getTerminalParams();
|
databaseAccount: testMongo32Account,
|
||||||
|
notebookServerInfo: testMongoNotebookServerInfo,
|
||||||
|
};
|
||||||
|
|
||||||
expect(params).toEqual(
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
new Map<string, string>([
|
expect(wrapper).toMatchSnapshot();
|
||||||
["terminal", "true"],
|
|
||||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getTerminalParams: Test for Mongo 3.6 terminal", () => {
|
it("renders mongo 3.6 shell", () => {
|
||||||
const terminal: NotebookTerminalComponent = createMongo36Terminal();
|
const props: NotebookTerminalComponentProps = {
|
||||||
const params: Map<string, string> = terminal.getTerminalParams();
|
databaseAccount: testMongo36Account,
|
||||||
|
notebookServerInfo: testMongoNotebookServerInfo,
|
||||||
|
};
|
||||||
|
|
||||||
expect(params).toEqual(
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
new Map<string, string>([
|
expect(wrapper).toMatchSnapshot();
|
||||||
["terminal", "true"],
|
|
||||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getTerminalParams: Test for Cassandra terminal", () => {
|
it("renders cassandra shell", () => {
|
||||||
const terminal: NotebookTerminalComponent = createCassandraTerminal();
|
const props: NotebookTerminalComponentProps = {
|
||||||
const params: Map<string, string> = terminal.getTerminalParams();
|
databaseAccount: testCassandraAccount,
|
||||||
|
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||||
|
};
|
||||||
|
|
||||||
expect(params).toEqual(
|
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||||
new Map<string, string>([
|
expect(wrapper).toMatchSnapshot();
|
||||||
["terminal", "true"],
|
|
||||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* Wrapper around Notebook server terminal
|
* Wrapper around Notebook server terminal
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import postRobot from "post-robot";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as StringUtils from "../../../Utils/StringUtils";
|
import { TerminalProps } from "../../../Terminal/TerminalProps";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { TerminalQueryParams } from "../../../Common/Constants";
|
import * as StringUtils from "../../../Utils/StringUtils";
|
||||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
|
||||||
|
|
||||||
export interface NotebookTerminalComponentProps {
|
export interface NotebookTerminalComponentProps {
|
||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||||
@@ -15,79 +15,69 @@ export interface NotebookTerminalComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||||
|
private terminalWindow: Window;
|
||||||
|
|
||||||
constructor(props: NotebookTerminalComponentProps) {
|
constructor(props: NotebookTerminalComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.sendPropsToTerminalFrame();
|
||||||
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="notebookTerminalContainer">
|
<div className="notebookTerminalContainer">
|
||||||
<iframe
|
<iframe
|
||||||
title="Terminal to Notebook Server"
|
title="Terminal to Notebook Server"
|
||||||
src={NotebookTerminalComponent.createNotebookAppSrc(this.props.notebookServerInfo, this.getTerminalParams())}
|
onLoad={(event) => this.handleFrameLoad(event)}
|
||||||
|
src="terminal.html"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalParams(): Map<string, string> {
|
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
||||||
let params: Map<string, string> = new Map<string, string>();
|
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
|
||||||
params.set(TerminalQueryParams.Terminal, "true");
|
this.sendPropsToTerminalFrame();
|
||||||
|
|
||||||
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
|
|
||||||
if (terminalEndpoint) {
|
|
||||||
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
sendPropsToTerminalFrame(): void {
|
||||||
|
if (!this.terminalWindow) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public tryGetTerminalEndpoint(): string | null {
|
const props: TerminalProps = {
|
||||||
let terminalEndpoint: string | null;
|
terminalEndpoint: this.tryGetTerminalEndpoint(),
|
||||||
|
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
|
||||||
|
authToken: this.props.notebookServerInfo?.authToken,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
apiType: userContext.apiType,
|
||||||
|
authType: userContext.authType,
|
||||||
|
databaseAccount: userContext.databaseAccount,
|
||||||
|
};
|
||||||
|
|
||||||
const notebookServerEndpoint: string = this.props.notebookServerInfo.notebookServerEndpoint;
|
postRobot.send(this.terminalWindow, "props", props, {
|
||||||
|
domain: window.location.origin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public tryGetTerminalEndpoint(): string | undefined {
|
||||||
|
let terminalEndpoint: string | undefined;
|
||||||
|
|
||||||
|
const notebookServerEndpoint = this.props.notebookServerInfo?.notebookServerEndpoint;
|
||||||
if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) {
|
if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) {
|
||||||
let mongoShellEndpoint: string = this.props.databaseAccount.properties.mongoEndpoint;
|
// mongoEndpoint is only available for Mongo 3.6 and higher, fallback to documentEndpoint otherwise
|
||||||
if (!mongoShellEndpoint) {
|
terminalEndpoint =
|
||||||
// mongoEndpoint is only available for Mongo 3.6 and higher.
|
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint;
|
||||||
// Fallback to documentEndpoint otherwise.
|
|
||||||
mongoShellEndpoint = this.props.databaseAccount.properties.documentEndpoint;
|
|
||||||
}
|
|
||||||
terminalEndpoint = mongoShellEndpoint;
|
|
||||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
|
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
|
||||||
terminalEndpoint = this.props.databaseAccount.properties.cassandraEndpoint;
|
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terminalEndpoint) {
|
if (terminalEndpoint) {
|
||||||
return new URL(terminalEndpoint).host;
|
return new URL(terminalEndpoint).host;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createNotebookAppSrc(
|
return undefined;
|
||||||
serverInfo: DataModels.NotebookWorkspaceConnectionInfo,
|
|
||||||
params: Map<string, string>
|
|
||||||
): string {
|
|
||||||
if (!serverInfo.notebookServerEndpoint) {
|
|
||||||
handleError(
|
|
||||||
"Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.",
|
|
||||||
"NotebookTerminalComponent/createNotebookAppSrc"
|
|
||||||
);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
|
|
||||||
if (serverInfo.authToken && serverInfo.authToken.length > 0) {
|
|
||||||
params.set(TerminalQueryParams.Token, serverInfo.authToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
params.set(TerminalQueryParams.SubscriptionId, userContext.subscriptionId);
|
|
||||||
|
|
||||||
let result: string = "terminal.html?";
|
|
||||||
for (let key of params.keys()) {
|
|
||||||
result += `${key}=${encodeURIComponent(params.get(key))}&`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`NotebookTerminalComponent renders cassandra shell 1`] = `
|
||||||
|
<div
|
||||||
|
className="notebookTerminalContainer"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
onLoad={[Function]}
|
||||||
|
src="terminal.html"
|
||||||
|
title="Terminal to Notebook Server"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NotebookTerminalComponent renders mongo 3.2 shell 1`] = `
|
||||||
|
<div
|
||||||
|
className="notebookTerminalContainer"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
onLoad={[Function]}
|
||||||
|
src="terminal.html"
|
||||||
|
title="Terminal to Notebook Server"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NotebookTerminalComponent renders mongo 3.6 shell 1`] = `
|
||||||
|
<div
|
||||||
|
className="notebookTerminalContainer"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
onLoad={[Function]}
|
||||||
|
src="terminal.html"
|
||||||
|
title="Terminal to Notebook Server"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NotebookTerminalComponent renders terminal 1`] = `
|
||||||
|
<div
|
||||||
|
className="notebookTerminalContainer"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
onLoad={[Function]}
|
||||||
|
src="terminal.html"
|
||||||
|
title="Terminal to Notebook Server"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -44,10 +44,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"copyNotebook": [Function],
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
@@ -115,10 +111,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"copyNotebook": [Function],
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
|
|||||||
@@ -11,18 +11,17 @@ import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"
|
|||||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
import { configContext } from "../ConfigContext";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
import { IGalleryItem } from "../Juno/JunoClient";
|
||||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import {
|
import {
|
||||||
get as getWorkspace,
|
get as getWorkspace,
|
||||||
@@ -35,7 +34,7 @@ import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
|||||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
@@ -47,12 +46,8 @@ import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
|||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||||
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
|
|
||||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
|
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
|
|
||||||
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
|
|
||||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||||
@@ -64,14 +59,11 @@ import TerminalTab from "./Tabs/TerminalTab";
|
|||||||
import Database from "./Tree/Database";
|
import Database from "./Tree/Database";
|
||||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||||
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
||||||
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
|
|
||||||
import StoredProcedure from "./Tree/StoredProcedure";
|
import StoredProcedure from "./Tree/StoredProcedure";
|
||||||
import { useDatabases } from "./useDatabases";
|
import { useDatabases } from "./useDatabases";
|
||||||
import { useSelectedNode } from "./useSelectedNode";
|
import { useSelectedNode } from "./useSelectedNode";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
|
||||||
var tmp = ComponentRegisterer;
|
|
||||||
|
|
||||||
export default class Explorer {
|
export default class Explorer {
|
||||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||||
@@ -81,9 +73,6 @@ export default class Explorer {
|
|||||||
// Resource Tree
|
// Resource Tree
|
||||||
private resourceTree: ResourceTreeAdapter;
|
private resourceTree: ResourceTreeAdapter;
|
||||||
|
|
||||||
// Resource Token
|
|
||||||
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
|
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
public isTabsContentExpanded: ko.Observable<boolean>;
|
public isTabsContentExpanded: ko.Observable<boolean>;
|
||||||
|
|
||||||
@@ -143,13 +132,13 @@ export default class Explorer {
|
|||||||
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"contextmenu",
|
"contextmenu",
|
||||||
function (e) {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
$(function () {
|
$(() => {
|
||||||
$(document.body).click(() => $(".commandDropdownContainer").hide());
|
$(document.body).click(() => $(".commandDropdownContainer").hide());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,6 +149,7 @@ export default class Explorer {
|
|||||||
case "Cassandra":
|
case "Cassandra":
|
||||||
this.tableDataClient = new CassandraAPIDataClient();
|
this.tableDataClient = new CassandraAPIDataClient();
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
this._initSettings();
|
this._initSettings();
|
||||||
@@ -192,7 +182,6 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.resourceTree = new ResourceTreeAdapter(this);
|
this.resourceTree = new ResourceTreeAdapter(this);
|
||||||
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
|
|
||||||
|
|
||||||
// Override notebook server parameters from URL parameters
|
// Override notebook server parameters from URL parameters
|
||||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||||
@@ -219,10 +208,6 @@ export default class Explorer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configContext.enableSchemaAnalyzer) {
|
|
||||||
userContext.features.enableSchemaAnalyzer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshExplorer();
|
this.refreshExplorer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,19 +315,15 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
public onRefreshDatabasesKeyPress = (source: string, event: KeyboardEvent): boolean => {
|
||||||
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
|
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
|
||||||
this.onRefreshResourcesClick(source, null);
|
this.onRefreshResourcesClick();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = (source: any, event: MouseEvent): void => {
|
public onRefreshResourcesClick = (): void => {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
|
|
||||||
description: "Refresh button clicked",
|
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
|
||||||
});
|
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
@@ -350,7 +331,7 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Facade
|
// Facade
|
||||||
public provideFeedbackEmail = () => {
|
public provideFeedbackEmail = (): void => {
|
||||||
window.open(Constants.Urls.feedbackEmail, "_blank");
|
window.open(Constants.Urls.feedbackEmail, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -376,6 +357,9 @@ export default class Explorer {
|
|||||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||||
|
|
||||||
this.refreshNotebookList();
|
this.refreshNotebookList();
|
||||||
|
|
||||||
this._isInitializingNotebooks = false;
|
this._isInitializingNotebooks = false;
|
||||||
@@ -564,7 +548,7 @@ export default class Explorer {
|
|||||||
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
|
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||||
promise
|
promise
|
||||||
.then(() => this.resourceTree.triggerRender())
|
.then(() => this.resourceTree.triggerRender())
|
||||||
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason));
|
.catch((reason) => this.showOkModalDialog("Unable to upload file", reason));
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,13 +694,13 @@ export default class Explorer {
|
|||||||
const options: NotebookTabOptions = {
|
const options: NotebookTabOptions = {
|
||||||
account: userContext.databaseAccount,
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||||
node: null,
|
node: undefined,
|
||||||
title: notebookContentItem.name,
|
title: notebookContentItem.name,
|
||||||
tabPath: notebookContentItem.path,
|
tabPath: notebookContentItem.path,
|
||||||
collection: null,
|
collection: undefined,
|
||||||
masterKey: userContext.masterKey || "",
|
masterKey: userContext.masterKey || "",
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
onLoadStartKey: null,
|
onLoadStartKey: undefined,
|
||||||
container: this,
|
container: this,
|
||||||
notebookContentItem,
|
notebookContentItem,
|
||||||
};
|
};
|
||||||
@@ -843,7 +827,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
clearMessage();
|
clearMessage();
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error) => {
|
||||||
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
|
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
|
||||||
clearMessage();
|
clearMessage();
|
||||||
}
|
}
|
||||||
@@ -856,6 +840,8 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.resourceTree.initialize();
|
await this.resourceTree.initialize();
|
||||||
|
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||||
|
|
||||||
this.notebookManager?.refreshPinnedRepos();
|
this.notebookManager?.refreshPinnedRepos();
|
||||||
if (this.notebookToImport) {
|
if (this.notebookToImport) {
|
||||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||||
@@ -895,7 +881,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
||||||
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
|
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
|
||||||
(reason: any) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
|
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,7 +916,7 @@ export default class Explorer {
|
|||||||
return this.openNotebook(newFile);
|
return this.openNotebook(newFile);
|
||||||
})
|
})
|
||||||
.then(() => this.resourceTree.triggerRender())
|
.then(() => this.resourceTree.triggerRender())
|
||||||
.catch((error: any) => {
|
.catch((error) => {
|
||||||
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
|
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
|
||||||
logConsoleError(errorMessage);
|
logConsoleError(errorMessage);
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
@@ -946,14 +932,15 @@ export default class Explorer {
|
|||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshContentItem(item: NotebookContentItem): Promise<void> {
|
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||||
|
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||||
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
||||||
handleError(error, "Explorer/refreshContentItem");
|
handleError(error, "Explorer/refreshContentItem");
|
||||||
return Promise.reject(new Error(error));
|
return Promise.reject(new Error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
|
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
||||||
@@ -988,12 +975,12 @@ export default class Explorer {
|
|||||||
const newTab = new TerminalTab({
|
const newTab = new TerminalTab({
|
||||||
account: userContext.databaseAccount,
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.Terminal,
|
tabKind: ViewModels.CollectionTabKind.Terminal,
|
||||||
node: null,
|
node: undefined,
|
||||||
title: `${title} ${index}`,
|
title: `${title} ${index}`,
|
||||||
tabPath: `${title} ${index}`,
|
tabPath: `${title} ${index}`,
|
||||||
collection: null,
|
collection: undefined,
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
onLoadStartKey: null,
|
onLoadStartKey: undefined,
|
||||||
container: this,
|
container: this,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
index: index,
|
index: index,
|
||||||
@@ -1013,7 +1000,7 @@ export default class Explorer {
|
|||||||
const galleryTab = useTabs
|
const galleryTab = useTabs
|
||||||
.getState()
|
.getState()
|
||||||
.getTabs(ViewModels.CollectionTabKind.Gallery)
|
.getTabs(ViewModels.CollectionTabKind.Gallery)
|
||||||
.find((tab) => tab.tabTitle() == title);
|
.find((tab) => tab.tabTitle() === title);
|
||||||
|
|
||||||
if (galleryTab instanceof GalleryTab) {
|
if (galleryTab instanceof GalleryTab) {
|
||||||
useTabs.getState().activateTab(galleryTab);
|
useTabs.getState().activateTab(galleryTab);
|
||||||
@@ -1024,7 +1011,7 @@ export default class Explorer {
|
|||||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||||
title,
|
title,
|
||||||
tabPath: title,
|
tabPath: title,
|
||||||
onLoadStartKey: null,
|
onLoadStartKey: undefined,
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1070,8 +1057,9 @@ export default class Explorer {
|
|||||||
const title = "Enable Notebooks (Preview)";
|
const title = "Enable Notebooks (Preview)";
|
||||||
const description =
|
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.";
|
"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
|
||||||
this.openSetupNotebooksPanel(title, description);
|
.getState()
|
||||||
|
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleOpenFileAction(path: string): Promise<void> {
|
public async handleOpenFileAction(path: string): Promise<void> {
|
||||||
@@ -1106,18 +1094,6 @@ export default class Explorer {
|
|||||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openAddDatabasePane(): void {
|
|
||||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openBrowseQueriesPanel(): void {
|
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openSaveQueryPanel(): void {
|
|
||||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||||
useSidePanel
|
useSidePanel
|
||||||
@@ -1128,25 +1104,6 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openGitHubReposPanel(header: string, junoClient?: JunoClient): void {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
header,
|
|
||||||
<GitHubReposPanel
|
|
||||||
explorer={this}
|
|
||||||
gitHubClientProp={this.notebookManager.gitHubClient}
|
|
||||||
junoClientProp={junoClient}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openSetupNotebooksPanel(title: string, description: string): void {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async refreshExplorer(): Promise<void> {
|
public async refreshExplorer(): Promise<void> {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from "react";
|
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { GraphHighlightedNodeData, EditedProperties } from "./GraphExplorer";
|
import React from "react";
|
||||||
|
|
||||||
import { EditorNodePropertiesComponent, EditorNodePropertiesComponentProps } from "./EditorNodePropertiesComponent";
|
import { EditorNodePropertiesComponent, EditorNodePropertiesComponentProps } from "./EditorNodePropertiesComponent";
|
||||||
|
|
||||||
describe("<EditorNodePropertiesComponent />", () => {
|
describe("<EditorNodePropertiesComponent />", () => {
|
||||||
// Tests that: single value prop is rendered with a textbox and a delete button
|
// Tests that: single value prop is rendered with a textbox and a delete button
|
||||||
// multi-value prop only a delete button (cannot be edited)
|
// multi-value prop only a delete button (cannot be edited)
|
||||||
|
const onUpdateProperties = jest.fn();
|
||||||
it("renders component", () => {
|
it("renders component", () => {
|
||||||
const props: EditorNodePropertiesComponentProps = {
|
const props: EditorNodePropertiesComponentProps = {
|
||||||
editedProperties: {
|
editedProperties: {
|
||||||
@@ -24,7 +23,6 @@ describe("<EditorNodePropertiesComponent />", () => {
|
|||||||
{ value: true, type: "boolean" },
|
{ value: true, type: "boolean" },
|
||||||
{ value: false, type: "boolean" },
|
{ value: false, type: "boolean" },
|
||||||
{ value: undefined, type: "null" },
|
{ value: undefined, type: "null" },
|
||||||
{ value: null, type: "null" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -41,14 +39,13 @@ describe("<EditorNodePropertiesComponent />", () => {
|
|||||||
{ value: true, type: "boolean" },
|
{ value: true, type: "boolean" },
|
||||||
{ value: false, type: "boolean" },
|
{ value: false, type: "boolean" },
|
||||||
{ value: undefined, type: "null" },
|
{ value: undefined, type: "null" },
|
||||||
{ value: null, type: "null" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
addedProperties: [],
|
addedProperties: [],
|
||||||
droppedKeys: [],
|
droppedKeys: [],
|
||||||
},
|
},
|
||||||
onUpdateProperties: (editedProperties: EditedProperties): void => {},
|
onUpdateProperties,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
@@ -81,7 +78,7 @@ describe("<EditorNodePropertiesComponent />", () => {
|
|||||||
addedProperties: [],
|
addedProperties: [],
|
||||||
droppedKeys: [],
|
droppedKeys: [],
|
||||||
},
|
},
|
||||||
onUpdateProperties: (editedProperties: EditedProperties): void => {},
|
onUpdateProperties,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import { EditedProperties } from "./GraphExplorer";
|
|
||||||
import DeleteIcon from "../../../../images/delete.svg";
|
|
||||||
import AddIcon from "../../../../images/Add-property.svg";
|
import AddIcon from "../../../../images/Add-property.svg";
|
||||||
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
|
import DeleteIcon from "../../../../images/delete.svg";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||||
|
import { EditedProperties } from "./GraphExplorer";
|
||||||
|
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
|
||||||
|
|
||||||
export interface EditorNodePropertiesComponentProps {
|
export interface EditorNodePropertiesComponentProps {
|
||||||
editedProperties: EditedProperties;
|
editedProperties: EditedProperties;
|
||||||
@@ -48,7 +48,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
const editedProperties = this.props.editedProperties;
|
const editedProperties = this.props.editedProperties;
|
||||||
// search for it
|
// search for it
|
||||||
for (let i = 0; i < editedProperties.existingProperties.length; i++) {
|
for (let i = 0; i < editedProperties.existingProperties.length; i++) {
|
||||||
let ip = editedProperties.existingProperties[i];
|
const ip = editedProperties.existingProperties[i];
|
||||||
if (ip.key === key) {
|
if (ip.key === key) {
|
||||||
editedProperties.existingProperties.splice(i, 1);
|
editedProperties.existingProperties.splice(i, 1);
|
||||||
editedProperties.droppedKeys.push(key);
|
editedProperties.droppedKeys.push(key);
|
||||||
@@ -60,7 +60,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
|
|
||||||
private removeAddedProperty(index: number): void {
|
private removeAddedProperty(index: number): void {
|
||||||
const editedProperties = this.props.editedProperties;
|
const editedProperties = this.props.editedProperties;
|
||||||
let ap = editedProperties.addedProperties;
|
const ap = editedProperties.addedProperties;
|
||||||
ap.splice(index, 1);
|
ap.splice(index, 1);
|
||||||
|
|
||||||
this.props.onUpdateProperties(editedProperties);
|
this.props.onUpdateProperties(editedProperties);
|
||||||
@@ -68,7 +68,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
|
|
||||||
private addProperty(): void {
|
private addProperty(): void {
|
||||||
const editedProperties = this.props.editedProperties;
|
const editedProperties = this.props.editedProperties;
|
||||||
let ap = editedProperties.addedProperties;
|
const ap = editedProperties.addedProperties;
|
||||||
ap.push({ key: "", values: [{ value: "", type: EditorNodePropertiesComponent.DEFAULT_PROPERTY_TYPE }] });
|
ap.push({ key: "", values: [{ value: "", type: EditorNodePropertiesComponent.DEFAULT_PROPERTY_TYPE }] });
|
||||||
this.props.onUpdateProperties(editedProperties);
|
this.props.onUpdateProperties(editedProperties);
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
singleValue.type = e.target.value as ViewModels.InputPropertyValueTypeString;
|
singleValue.type = e.target.value as ViewModels.InputPropertyValueTypeString;
|
||||||
if (singleValue.type === "null") {
|
if (singleValue.type === "null") {
|
||||||
singleValue.value = null;
|
singleValue.value = undefined;
|
||||||
}
|
}
|
||||||
this.props.onUpdateProperties(this.props.editedProperties);
|
this.props.onUpdateProperties(this.props.editedProperties);
|
||||||
}}
|
}}
|
||||||
@@ -144,7 +144,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
className="rightPaneTrashIcon rightPaneBtns"
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
as="span"
|
as="span"
|
||||||
aria-label="Delete property"
|
aria-label="Delete property"
|
||||||
onActivated={(e) => this.removeExistingProperty(key)}
|
onActivated={() => this.removeExistingProperty(key)}
|
||||||
>
|
>
|
||||||
<img src={DeleteIcon} alt="Delete" />
|
<img src={DeleteIcon} alt="Delete" />
|
||||||
</AccessibleElement>
|
</AccessibleElement>
|
||||||
@@ -166,7 +166,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
className="rightPaneTrashIcon rightPaneBtns"
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
as="span"
|
as="span"
|
||||||
aria-label="Remove existing property"
|
aria-label="Remove existing property"
|
||||||
onActivated={(e) => this.removeExistingProperty(nodeProp.key)}
|
onActivated={() => this.removeExistingProperty(nodeProp.key)}
|
||||||
>
|
>
|
||||||
<img src={DeleteIcon} alt="Delete" />
|
<img src={DeleteIcon} alt="Delete" />
|
||||||
</AccessibleElement>
|
</AccessibleElement>
|
||||||
@@ -206,7 +206,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
firstValue.value = e.target.value;
|
firstValue.value = e.target.value;
|
||||||
if (firstValue.type === "null") {
|
if (firstValue.type === "null") {
|
||||||
firstValue.value = null;
|
firstValue.value = undefined;
|
||||||
}
|
}
|
||||||
this.props.onUpdateProperties(this.props.editedProperties);
|
this.props.onUpdateProperties(this.props.editedProperties);
|
||||||
}}
|
}}
|
||||||
@@ -235,7 +235,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
|||||||
className="rightPaneTrashIcon rightPaneBtns"
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
as="span"
|
as="span"
|
||||||
aria-label="Remove property"
|
aria-label="Remove property"
|
||||||
onActivated={(e) => this.removeAddedProperty(index)}
|
onActivated={() => this.removeAddedProperty(index)}
|
||||||
>
|
>
|
||||||
<img src={DeleteIcon} alt="Delete" />
|
<img src={DeleteIcon} alt="Delete" />
|
||||||
</AccessibleElement>
|
</AccessibleElement>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class QueryContainerComponent extends React.Component<
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="filterbtnstyle queryButton"
|
className="filterbtnstyle queryButton"
|
||||||
onClick={(e) => this.props.onExecuteClick(this.state.query)}
|
onClick={() => this.props.onExecuteClick(this.state.query)}
|
||||||
disabled={this.props.isLoading || !QueryContainerComponent.isQueryValid(this.state.query)}
|
disabled={this.props.isLoading || !QueryContainerComponent.isQueryValid(this.state.query)}
|
||||||
>
|
>
|
||||||
Execute Gremlin Query
|
Execute Gremlin Query
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||||
import { GraphHighlightedNodeData, NeighborVertexBasicInfo } from "./GraphExplorer";
|
import { GraphHighlightedNodeData, NeighborVertexBasicInfo } from "./GraphExplorer";
|
||||||
import * as GraphUtil from "./GraphUtil";
|
import * as GraphUtil from "./GraphUtil";
|
||||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
|
||||||
|
|
||||||
export interface ReadOnlyNeighborsComponentProps {
|
export interface ReadOnlyNeighborsComponentProps {
|
||||||
node: GraphHighlightedNodeData;
|
node: GraphHighlightedNodeData;
|
||||||
@@ -48,7 +48,7 @@ export class ReadOnlyNeighborsComponent extends React.Component<ReadOnlyNeighbor
|
|||||||
className="clickableLink"
|
className="clickableLink"
|
||||||
as="a"
|
as="a"
|
||||||
aria-label={_neighbor.name}
|
aria-label={_neighbor.name}
|
||||||
onActivated={(e) => this.props.selectNode(_neighbor.id)}
|
onActivated={() => this.props.selectNode(_neighbor.id)}
|
||||||
title={GraphUtil.getNeighborTitle(_neighbor)}
|
title={GraphUtil.getNeighborTitle(_neighbor)}
|
||||||
>
|
>
|
||||||
{_neighbor.name}
|
{_neighbor.name}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { GraphHighlightedNodeData } from "./GraphExplorer";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { GraphHighlightedNodeData } from "./GraphExplorer";
|
||||||
|
|
||||||
export interface ReadOnlyNodePropertiesComponentProps {
|
export interface ReadOnlyNodePropertiesComponentProps {
|
||||||
node: GraphHighlightedNodeData;
|
node: GraphHighlightedNodeData;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
|||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="valueCol"
|
className="valueCol"
|
||||||
title="efgh, 1234, true, false, undefined, null"
|
title="efgh, 1234, true, false, undefined"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="propertyValue"
|
className="propertyValue"
|
||||||
@@ -69,12 +69,6 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
|||||||
>
|
>
|
||||||
undefined
|
undefined
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="propertyValue isNull"
|
|
||||||
key="null"
|
|
||||||
>
|
|
||||||
null
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -178,12 +172,6 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
|||||||
>
|
>
|
||||||
undefined
|
undefined
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="propertyValue isNull"
|
|
||||||
key="null"
|
|
||||||
>
|
|
||||||
null
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td />
|
<td />
|
||||||
<td
|
<td
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
console.log(mockExplorer);
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import * as Constants from "../../../Common/Constants";
|
|||||||
import { configContext, Platform } from "../../../ConfigContext";
|
import { configContext, Platform } from "../../../ConfigContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
|
import { JunoClient } from "../../../Juno/JunoClient";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||||
@@ -30,8 +31,12 @@ import { CommandButtonComponentProps } from "../../Controls/CommandButton/Comman
|
|||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||||
|
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||||
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
|
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||||
|
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { SelectedNodeState } from "../../useSelectedNode";
|
import { SelectedNodeState } from "../../useSelectedNode";
|
||||||
|
|
||||||
@@ -281,9 +286,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () =>
|
||||||
container.openAddDatabasePane();
|
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
|
||||||
},
|
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
@@ -415,7 +419,8 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
|||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openBrowseQueriesPanel(),
|
onCommandClick: () =>
|
||||||
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
@@ -448,7 +453,13 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
|
|||||||
return {
|
return {
|
||||||
iconSrc: EnableNotebooksIcon,
|
iconSrc: EnableNotebooksIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
|
onCommandClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
label,
|
||||||
|
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
||||||
|
),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||||
@@ -486,7 +497,12 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
|||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
} else {
|
} else {
|
||||||
container.openSetupNotebooksPanel(title, description);
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
title,
|
||||||
|
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
@@ -513,7 +529,12 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
|||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||||
} else {
|
} else {
|
||||||
container.openSetupNotebooksPanel(title, description);
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
title,
|
||||||
|
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
@@ -540,10 +561,21 @@ function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonC
|
|||||||
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
|
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
||||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||||
|
const junoClient = new JunoClient();
|
||||||
return {
|
return {
|
||||||
iconSrc: GitHubIcon,
|
iconSrc: GitHubIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openGitHubReposPanel(label),
|
onCommandClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
label,
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={container}
|
||||||
|
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={junoClient}
|
||||||
|
/>
|
||||||
|
),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||||
|
import * as React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import * as NteractUtil from "../NTeractUtil";
|
import * as NteractUtil from "../NTeractUtil";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { AppState, ContentRef, selectors } from "@nteract/core";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import NotebookRenderer from "../../../NotebookRenderer/NotebookRenderer";
|
import NotebookRenderer from "../../../NotebookRenderer/NotebookRenderer";
|
||||||
import * as TextFile from "./text-file";
|
import * as TextFile from "./text-file";
|
||||||
|
|
||||||
@@ -32,14 +31,14 @@ interface FileProps {
|
|||||||
|
|
||||||
export class File extends React.PureComponent<FileProps> {
|
export class File extends React.PureComponent<FileProps> {
|
||||||
getChoice = () => {
|
getChoice = () => {
|
||||||
let choice = null;
|
let choice;
|
||||||
|
|
||||||
// notebooks don't report a mimetype so we'll use the content.type
|
// notebooks don't report a mimetype so we'll use the content.type
|
||||||
if (this.props.type === "notebook") {
|
if (this.props.type === "notebook") {
|
||||||
choice = <NotebookRenderer contentRef={this.props.contentRef} />;
|
choice = <NotebookRenderer contentRef={this.props.contentRef} />;
|
||||||
} else if (this.props.type === "dummy") {
|
} else if (this.props.type === "dummy") {
|
||||||
choice = null;
|
choice = undefined;
|
||||||
} else if (this.props.mimetype == null || !TextFile.handles(this.props.mimetype)) {
|
} else if (this.props.mimetype === undefined || !TextFile.handles(this.props.mimetype)) {
|
||||||
// This should not happen as we intercept mimetype upstream, but just in case
|
// This should not happen as we intercept mimetype upstream, but just in case
|
||||||
choice = (
|
choice = (
|
||||||
<PaddedContainer>
|
<PaddedContainer>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as StringUtils from "../../../../../Utils/StringUtils";
|
|
||||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
||||||
import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor";
|
import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import * as StringUtils from "../../../../../Utils/StringUtils";
|
||||||
|
|
||||||
const EditorContainer = styled.div`
|
const EditorContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -37,7 +37,7 @@ interface TextFileState {
|
|||||||
class EditorPlaceholder extends React.PureComponent<MonacoEditorProps> {
|
class EditorPlaceholder extends React.PureComponent<MonacoEditorProps> {
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
// TODO: Show a little blocky placeholder
|
// TODO: Show a little blocky placeholder
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ function makeMapStateToTextFileProps(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
contentRef,
|
contentRef,
|
||||||
mimetype: content.mimetype != null ? content.mimetype : "text/plain",
|
mimetype: content.mimetype !== undefined ? content.mimetype : "text/plain",
|
||||||
text,
|
text,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { stringifyNotebook } from "@nteract/commutable";
|
import { stringifyNotebook } from "@nteract/commutable";
|
||||||
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
|
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
import { AjaxResponse } from "rxjs/ajax";
|
||||||
import * as StringUtils from "../../Utils/StringUtils";
|
import * as StringUtils from "../../Utils/StringUtils";
|
||||||
import * as FileSystemUtil from "./FileSystemUtil";
|
import * as FileSystemUtil from "./FileSystemUtil";
|
||||||
@@ -14,7 +15,17 @@ export class NotebookContentClient {
|
|||||||
* This updates the item and points all the children's parent to this item
|
* This updates the item and points all the children's parent to this item
|
||||||
* @param item
|
* @param item
|
||||||
*/
|
*/
|
||||||
public updateItemChildren(item: NotebookContentItem): Promise<void> {
|
public async updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
|
||||||
|
const subItems = await this.fetchNotebookFiles(item.path);
|
||||||
|
const clonedItem = cloneDeep(item);
|
||||||
|
subItems.forEach((subItem) => (subItem.parent = clonedItem));
|
||||||
|
clonedItem.children = subItems;
|
||||||
|
|
||||||
|
return clonedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||||
|
public async updateItemChildrenInPlace(item: NotebookContentItem): Promise<void> {
|
||||||
return this.fetchNotebookFiles(item.path).then((subItems) => {
|
return this.fetchNotebookFiles(item.path).then((subItems) => {
|
||||||
item.children = subItems;
|
item.children = subItems;
|
||||||
subItems.forEach((subItem) => (subItem.parent = item));
|
subItems.forEach((subItem) => (subItem.parent = item));
|
||||||
@@ -55,8 +66,11 @@ export class NotebookContentClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteContentItem(item: NotebookContentItem): Promise<void> {
|
public async deleteContentItem(item: NotebookContentItem): Promise<void> {
|
||||||
return this.deleteNotebookFile(item.path).then((path: string) => {
|
const path = await this.deleteNotebookFile(item.path);
|
||||||
|
useNotebook.getState().deleteNotebookItem(item);
|
||||||
|
|
||||||
|
// TODO: Delete once old resource tree is removed
|
||||||
if (!path || path !== item.path) {
|
if (!path || path !== item.path) {
|
||||||
throw new Error("No path provided");
|
throw new Error("No path provided");
|
||||||
}
|
}
|
||||||
@@ -66,7 +80,6 @@ export class NotebookContentClient {
|
|||||||
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
||||||
item.parent.children = newChildren;
|
item.parent.children = newChildren;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { userContext } from "../../UserContext";
|
|||||||
import { getFullName } from "../../Utils/UserUtils";
|
import { getFullName } from "../../Utils/UserUtils";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||||
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
||||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
|
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
|
||||||
@@ -88,7 +89,18 @@ export default class NotebookManager {
|
|||||||
this.gitHubClient.setToken(token?.access_token);
|
this.gitHubClient.setToken(token?.access_token);
|
||||||
if (this?.gitHubOAuthService.isLoggedIn()) {
|
if (this?.gitHubOAuthService.isLoggedIn()) {
|
||||||
useSidePanel.getState().closeSidePanel();
|
useSidePanel.getState().closeSidePanel();
|
||||||
this.params.container.openGitHubReposPanel("Manager GitHub settings", this.junoClient);
|
setTimeout(() => {
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Manage GitHub settings",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={this.params.container}
|
||||||
|
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={this.junoClient}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.params.refreshCommandBarButtons();
|
this.params.refreshCommandBarButtons();
|
||||||
@@ -129,6 +141,7 @@ export default class NotebookManager {
|
|||||||
notebookContentRef={notebookContentRef}
|
notebookContentRef={notebookContentRef}
|
||||||
onTakeSnapshot={onTakeSnapshot}
|
onTakeSnapshot={onTakeSnapshot}
|
||||||
/>,
|
/>,
|
||||||
|
"440px",
|
||||||
onClosePanel
|
onClosePanel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -161,7 +174,17 @@ export default class NotebookManager {
|
|||||||
undefined,
|
undefined,
|
||||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||||
"Connect to GitHub",
|
"Connect to GitHub",
|
||||||
() => this.params.container.openGitHubReposPanel("Connect to GitHub"),
|
() =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Connect to GitHub",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={this.params.container}
|
||||||
|
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={this.junoClient}
|
||||||
|
/>
|
||||||
|
),
|
||||||
"Cancel",
|
"Cancel",
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "./Prompt.less";
|
|||||||
|
|
||||||
export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
||||||
if (props.status === "busy") {
|
if (props.status === "busy") {
|
||||||
const stopButtonText: string = "Stop cell execution";
|
const stopButtonText = "Stop cell execution";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ position: "sticky", width: "100%", maxHeight: "100%", left: 0, top: 0, zIndex: 300 }}
|
style={{ position: "sticky", width: "100%", maxHeight: "100%", left: 0, top: 0, zIndex: 300 }}
|
||||||
@@ -23,7 +23,7 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (props.isHovered) {
|
} else if (props.isHovered) {
|
||||||
const playButtonText: string = "Run cell";
|
const playButtonText = "Run cell";
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
className="runCellButton"
|
className="runCellButton"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { StatusBar } from "./StatusBar";
|
import { StatusBar } from "./StatusBar";
|
||||||
|
|
||||||
describe("StatusBar", () => {
|
describe("StatusBar", () => {
|
||||||
@@ -28,8 +27,8 @@ describe("StatusBar", () => {
|
|||||||
kernelSpecDisplayName: "javascript",
|
kernelSpecDisplayName: "javascript",
|
||||||
kernelStatus: "kernelStatus",
|
kernelStatus: "kernelStatus",
|
||||||
},
|
},
|
||||||
null,
|
undefined,
|
||||||
null
|
undefined
|
||||||
);
|
);
|
||||||
expect(shouldUpdate).toBe(true);
|
expect(shouldUpdate).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -47,8 +46,8 @@ describe("StatusBar", () => {
|
|||||||
kernelSpecDisplayName: "python3",
|
kernelSpecDisplayName: "python3",
|
||||||
kernelStatus: "kernelStatus",
|
kernelStatus: "kernelStatus",
|
||||||
},
|
},
|
||||||
null,
|
undefined,
|
||||||
null
|
undefined
|
||||||
);
|
);
|
||||||
expect(shouldUpdate).toBe(true);
|
expect(shouldUpdate).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AppState, ContentRef, selectors } from "@nteract/core";
|
|||||||
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { StyleConstants } from "../../../Common/Constants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,8 +13,6 @@ interface Props {
|
|||||||
|
|
||||||
const NOT_CONNECTED = "not connected";
|
const NOT_CONNECTED = "not connected";
|
||||||
|
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
export const LeftStatus = styled.div`
|
export const LeftStatus = styled.div`
|
||||||
float: left;
|
float: left;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -80,7 +79,7 @@ interface InitialProps {
|
|||||||
contentRef: ContentRef;
|
contentRef: ContentRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
|
const makeMapStateToProps = (_initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
|
||||||
const { contentRef } = initialProps;
|
const { contentRef } = initialProps;
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
@@ -90,26 +89,26 @@ const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps)
|
|||||||
return {
|
return {
|
||||||
kernelStatus: NOT_CONNECTED,
|
kernelStatus: NOT_CONNECTED,
|
||||||
kernelSpecDisplayName: "no kernel",
|
kernelSpecDisplayName: "no kernel",
|
||||||
lastSaved: null,
|
lastSaved: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const kernelRef = content.model.kernelRef;
|
const kernelRef = content.model.kernelRef;
|
||||||
let kernel = null;
|
let kernel;
|
||||||
if (kernelRef) {
|
if (kernelRef) {
|
||||||
kernel = selectors.kernel(state, { kernelRef });
|
kernel = selectors.kernel(state, { kernelRef });
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSaved = content && content.lastSaved ? content.lastSaved : null;
|
const lastSaved = content && content.lastSaved ? content.lastSaved : undefined;
|
||||||
|
|
||||||
const kernelStatus = kernel != null && kernel.status != null ? kernel.status : NOT_CONNECTED;
|
const kernelStatus = kernel?.status || NOT_CONNECTED;
|
||||||
|
|
||||||
// TODO: We need kernels associated to the kernelspec they came from
|
// TODO: We need kernels associated to the kernelspec they came from
|
||||||
// so we can pluck off the display_name and provide it here
|
// so we can pluck off the display_name and provide it here
|
||||||
let kernelSpecDisplayName = " ";
|
let kernelSpecDisplayName = " ";
|
||||||
if (kernelStatus === NOT_CONNECTED) {
|
if (kernelStatus === NOT_CONNECTED) {
|
||||||
kernelSpecDisplayName = "no kernel";
|
kernelSpecDisplayName = "no kernel";
|
||||||
} else if (kernel != null && kernel.kernelSpecName != null) {
|
} else if (kernel?.kernelSpecName) {
|
||||||
kernelSpecDisplayName = kernel.kernelSpecName;
|
kernelSpecDisplayName = kernel.kernelSpecName;
|
||||||
} else if (content && content.type === "notebook") {
|
} else if (content && content.type === "notebook") {
|
||||||
kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " ";
|
kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " ";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ interface DispatchProps {
|
|||||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
moveCell: (destinationId: CellId, above: boolean) => void;
|
||||||
clearOutputs: () => void;
|
clearOutputs: () => void;
|
||||||
deleteCell: () => void;
|
deleteCell: () => void;
|
||||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
|
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) => void;
|
||||||
takeNotebookSnapshot: (payload: SnapshotRequest) => void;
|
takeNotebookSnapshot: (payload: SnapshotRequest) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ const mapDispatchToProps = (
|
|||||||
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
||||||
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
||||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
|
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
|
||||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
|
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) =>
|
||||||
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data })),
|
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data })),
|
||||||
takeNotebookSnapshot: (request: SnapshotRequest) => dispatch(cdbActions.takeNotebookSnapshot(request)),
|
takeNotebookSnapshot: (request: SnapshotRequest) => dispatch(cdbActions.takeNotebookSnapshot(request)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { ContentRef } from "@nteract/core";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
|
|
||||||
import { ContentRef } from "@nteract/core";
|
|
||||||
import * as actions from "../../NotebookComponent/actions";
|
import * as actions from "../../NotebookComponent/actions";
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
@@ -29,10 +28,7 @@ class HoverableCell extends React.Component<ComponentProps & DispatchProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
const mapDispatchToProps = (dispatch: Dispatch, { id }: { id: string }): DispatchProps => ({
|
||||||
dispatch: Dispatch,
|
|
||||||
{ id, contentRef }: { id: string; contentRef: ContentRef }
|
|
||||||
): DispatchProps => ({
|
|
||||||
hover: () => dispatch(actions.setHoveredCell({ cellId: id })),
|
hover: () => dispatch(actions.setHoveredCell({ cellId: id })),
|
||||||
unHover: () => dispatch(actions.setHoveredCell({ cellId: undefined })),
|
unHover: () => dispatch(actions.setHoveredCell({ cellId: undefined })),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cloneDeep } from "lodash";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -5,8 +6,12 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
|||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
|
import NotebookManager from "./NotebookManager";
|
||||||
|
|
||||||
interface NotebookState {
|
interface NotebookState {
|
||||||
isNotebookEnabled: boolean;
|
isNotebookEnabled: boolean;
|
||||||
@@ -18,6 +23,9 @@ interface NotebookState {
|
|||||||
isShellEnabled: boolean;
|
isShellEnabled: boolean;
|
||||||
notebookBasePath: string;
|
notebookBasePath: string;
|
||||||
isInitializingNotebooks: boolean;
|
isInitializingNotebooks: boolean;
|
||||||
|
myNotebooksContentRoot: NotebookContentItem;
|
||||||
|
gitHubNotebooksContentRoot: NotebookContentItem;
|
||||||
|
galleryContentRoot: NotebookContentItem;
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||||
@@ -27,9 +35,13 @@ interface NotebookState {
|
|||||||
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
||||||
setNotebookBasePath: (notebookBasePath: string) => void;
|
setNotebookBasePath: (notebookBasePath: string) => void;
|
||||||
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
||||||
|
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
||||||
|
updateNotebookItem: (item: NotebookContentItem) => void;
|
||||||
|
deleteNotebookItem: (item: NotebookContentItem) => void;
|
||||||
|
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||||
isNotebookEnabled: false,
|
isNotebookEnabled: false,
|
||||||
isNotebooksEnabledForAccount: false,
|
isNotebooksEnabledForAccount: false,
|
||||||
notebookServerInfo: {
|
notebookServerInfo: {
|
||||||
@@ -46,6 +58,9 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
|||||||
isShellEnabled: false,
|
isShellEnabled: false,
|
||||||
notebookBasePath: Constants.Notebook.defaultBasePath,
|
notebookBasePath: Constants.Notebook.defaultBasePath,
|
||||||
isInitializingNotebooks: false,
|
isInitializingNotebooks: false,
|
||||||
|
myNotebooksContentRoot: undefined,
|
||||||
|
gitHubNotebooksContentRoot: undefined,
|
||||||
|
galleryContentRoot: undefined,
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||||
@@ -103,4 +118,88 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
|||||||
set({ isNotebooksEnabledForAccount: false });
|
set({ isNotebooksEnabledForAccount: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
|
||||||
|
const currentItem = root || get().myNotebooksContentRoot;
|
||||||
|
|
||||||
|
if (currentItem) {
|
||||||
|
if (currentItem.path === item.path && currentItem.name === item.name) {
|
||||||
|
return currentItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentItem.children) {
|
||||||
|
for (const childItem of currentItem.children) {
|
||||||
|
const result = get().findItem(childItem, item);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
updateNotebookItem: (item: NotebookContentItem): void => {
|
||||||
|
const root = cloneDeep(get().myNotebooksContentRoot);
|
||||||
|
const parentItem = get().findItem(root, item.parent);
|
||||||
|
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||||
|
parentItem.children.push(item);
|
||||||
|
item.parent = parentItem;
|
||||||
|
set({ myNotebooksContentRoot: root });
|
||||||
|
},
|
||||||
|
deleteNotebookItem: (item: NotebookContentItem): void => {
|
||||||
|
const root = cloneDeep(get().myNotebooksContentRoot);
|
||||||
|
const parentItem = get().findItem(root, item.parent);
|
||||||
|
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||||
|
set({ myNotebooksContentRoot: root });
|
||||||
|
},
|
||||||
|
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||||
|
const myNotebooksContentRoot = {
|
||||||
|
name: "My Notebooks",
|
||||||
|
path: get().notebookBasePath,
|
||||||
|
type: NotebookContentItemType.Directory,
|
||||||
|
};
|
||||||
|
const galleryContentRoot = {
|
||||||
|
name: "Gallery",
|
||||||
|
path: "Gallery",
|
||||||
|
type: NotebookContentItemType.File,
|
||||||
|
};
|
||||||
|
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
|
||||||
|
? {
|
||||||
|
name: "GitHub repos",
|
||||||
|
path: "PsuedoDir",
|
||||||
|
type: NotebookContentItemType.Directory,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
set({
|
||||||
|
myNotebooksContentRoot,
|
||||||
|
galleryContentRoot,
|
||||||
|
gitHubNotebooksContentRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (get().notebookServerInfo?.notebookServerEndpoint) {
|
||||||
|
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
|
||||||
|
set({ myNotebooksContentRoot: updatedRoot });
|
||||||
|
|
||||||
|
if (updatedRoot?.children) {
|
||||||
|
// Count 1st generation children (tree is lazy-loaded)
|
||||||
|
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
|
||||||
|
updatedRoot.children.forEach((notebookItem) => {
|
||||||
|
switch (notebookItem.type) {
|
||||||
|
case NotebookContentItemType.File:
|
||||||
|
nodeCounts.files++;
|
||||||
|
break;
|
||||||
|
case NotebookContentItemType.Directory:
|
||||||
|
nodeCounts.directories++;
|
||||||
|
break;
|
||||||
|
case NotebookContentItemType.Notebook:
|
||||||
|
nodeCounts.notebooks++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -113,7 +113,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
collectionId: "",
|
collectionId: "",
|
||||||
enableIndexing: true,
|
enableIndexing: true,
|
||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: "",
|
partitionKey:
|
||||||
|
(userContext.features.partitionKeyDefault && userContext.apiType === "SQL") ||
|
||||||
|
(userContext.features.partitionKeyDefault && userContext.apiType === "Mongo")
|
||||||
|
? "/id"
|
||||||
|
: "",
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
|
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
|
||||||
useHashV2: false,
|
useHashV2: false,
|
||||||
@@ -413,6 +417,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<Text variant="small" aria-label="pkDescription">
|
||||||
|
{this.getPartitionKeySubtext()}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="addCollection-partitionKeyValue"
|
id="addCollection-partitionKeyValue"
|
||||||
@@ -807,6 +815,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return tooltipText;
|
return tooltipText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPartitionKeySubtext(): string {
|
||||||
|
if (
|
||||||
|
userContext.features.partitionKeyDefault &&
|
||||||
|
(userContext.apiType === "SQL" || userContext.apiType === "Mongo")
|
||||||
|
) {
|
||||||
|
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
|
||||||
|
return subtext;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useSidePanel.getState().closeSidePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetData(): void {
|
public resetData(): void {
|
||||||
@@ -144,11 +145,18 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
|
|
||||||
private setup(forceShowConnectToGitHub = false): void {
|
private setup(forceShowConnectToGitHub = false): void {
|
||||||
forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn()
|
forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn()
|
||||||
? this.setupForConnectToGitHub()
|
? this.setupForConnectToGitHub(forceShowConnectToGitHub)
|
||||||
: this.setupForManageRepos();
|
: this.setupForManageRepos();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupForConnectToGitHub(): void {
|
private setupForConnectToGitHub(forceShowConnectToGitHub: boolean): void {
|
||||||
|
if (forceShowConnectToGitHub) {
|
||||||
|
const newState = { ...this.state.gitHubReposState };
|
||||||
|
newState.showAuthorizeAccess = forceShowConnectToGitHub;
|
||||||
|
this.setState({
|
||||||
|
gitHubReposState: newState,
|
||||||
|
});
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
});
|
});
|
||||||
@@ -368,14 +376,17 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
|
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
|
||||||
};
|
};
|
||||||
|
this.loadMoreBranches(item.repo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
gitHubReposState: {
|
gitHubReposState: {
|
||||||
...this.state.gitHubReposState,
|
...this.state.gitHubReposState,
|
||||||
reposListProps: {
|
reposListProps: {
|
||||||
...this.state.gitHubReposState.reposListProps,
|
...this.state.gitHubReposState.reposListProps,
|
||||||
branchesProps: {
|
branchesProps: {
|
||||||
...this.state.gitHubReposState.reposListProps.branchesProps,
|
...this.branchesProps,
|
||||||
[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]: this.branchesProps[item.key],
|
|
||||||
},
|
},
|
||||||
pinnedReposProps: {
|
pinnedReposProps: {
|
||||||
repos: this.pinnedReposProps.repos,
|
repos: this.pinnedReposProps.repos,
|
||||||
@@ -387,27 +398,6 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.loadMoreBranches(item.repo);
|
|
||||||
} else {
|
|
||||||
if (this.isAddedRepo === false) {
|
|
||||||
this.setState({
|
|
||||||
gitHubReposState: {
|
|
||||||
...this.state.gitHubReposState,
|
|
||||||
reposListProps: {
|
|
||||||
...this.state.gitHubReposState.reposListProps,
|
|
||||||
pinnedReposProps: {
|
|
||||||
repos: this.pinnedReposProps.repos,
|
|
||||||
},
|
|
||||||
unpinnedReposProps: {
|
|
||||||
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
|
|
||||||
repos: this.unpinnedReposProps.repos,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.isAddedRepo = false;
|
this.isAddedRepo = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||||||
"copyNotebook": [Function],
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"getRepo": [Function],
|
"getRepo": [Function],
|
||||||
"pinRepo": [Function],
|
"pinRepo": [Function],
|
||||||
|
|||||||
@@ -150,9 +150,6 @@
|
|||||||
.backImageIcon {
|
.backImageIcon {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.entityValueTextField {
|
|
||||||
margin: 24px;
|
|
||||||
}
|
|
||||||
.addEntityDatePicker {
|
.addEntityDatePicker {
|
||||||
max-width: 145px;
|
max-width: 145px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
|||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
|
|
||||||
export interface PanelContainerProps {
|
export interface PanelContainerProps {
|
||||||
headerText: string;
|
headerText?: string;
|
||||||
panelContent: JSX.Element;
|
panelContent?: JSX.Element;
|
||||||
isConsoleExpanded: boolean;
|
isConsoleExpanded: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
panelWidth?: string;
|
panelWidth?: string;
|
||||||
@@ -66,8 +66,8 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDissmiss = (ev?: React.SyntheticEvent<HTMLElement>): void => {
|
private onDissmiss = (ev?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
|
||||||
if ((ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
} else {
|
} else {
|
||||||
useSidePanel.getState().closeSidePanel();
|
useSidePanel.getState().closeSidePanel();
|
||||||
@@ -85,11 +85,12 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
|
|
||||||
export const SidePanel: React.FC = () => {
|
export const SidePanel: React.FC = () => {
|
||||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||||
const { isOpen, panelContent, headerText } = useSidePanel((state) => {
|
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||||
return {
|
return {
|
||||||
isOpen: state.isOpen,
|
isOpen: state.isOpen,
|
||||||
panelContent: state.panelContent,
|
panelContent: state.panelContent,
|
||||||
headerText: state.headerText,
|
headerText: state.headerText,
|
||||||
|
panelWidth: state.panelWidth,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
// TODO Refactor PanelContainerComponent into a functional component and remove this wrapper
|
// TODO Refactor PanelContainerComponent into a functional component and remove this wrapper
|
||||||
@@ -100,6 +101,7 @@ export const SidePanel: React.FC = () => {
|
|||||||
panelContent={panelContent}
|
panelContent={panelContent}
|
||||||
headerText={headerText}
|
headerText={headerText}
|
||||||
isConsoleExpanded={isConsoleExpanded}
|
isConsoleExpanded={isConsoleExpanded}
|
||||||
|
panelWidth={panelWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
"copyNotebook": [Function],
|
"copyNotebook": [Function],
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inProgressMessage="Creating directory "
|
inProgressMessage="Creating directory "
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
|
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||||
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
||||||
|
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { TableEntity } from "../../../Common/TableEntity";
|
import { TableEntity } from "../../../Common/TableEntity";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as TableConstants from "../../Tables/Constants";
|
import * as TableConstants from "../../Tables/Constants";
|
||||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||||
@@ -15,7 +15,7 @@ import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../.
|
|||||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||||
import * as Utilities from "../../Tables/Utilities";
|
import * as Utilities from "../../Tables/Utilities";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||||
import { PanelContainerComponent } from "../PanelContainerComponent";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
import {
|
import {
|
||||||
attributeNameLabel,
|
attributeNameLabel,
|
||||||
attributeValueLabel,
|
attributeValueLabel,
|
||||||
@@ -30,9 +30,7 @@ import {
|
|||||||
getCassandraDefaultEntities,
|
getCassandraDefaultEntities,
|
||||||
getDefaultEntities,
|
getDefaultEntities,
|
||||||
getEntityValuePlaceholder,
|
getEntityValuePlaceholder,
|
||||||
getPanelTitle,
|
|
||||||
imageProps,
|
imageProps,
|
||||||
isValidEntities,
|
|
||||||
options,
|
options,
|
||||||
} from "./Validators/EntityTableHelper";
|
} from "./Validators/EntityTableHelper";
|
||||||
|
|
||||||
@@ -61,7 +59,6 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
tableEntityListViewModel,
|
tableEntityListViewModel,
|
||||||
cassandraApiClient,
|
cassandraApiClient,
|
||||||
}: AddTableEntityPanelProps): JSX.Element => {
|
}: AddTableEntityPanelProps): JSX.Element => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
|
||||||
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
||||||
const [selectedRow, setSelectedRow] = useState<number>(0);
|
const [selectedRow, setSelectedRow] = useState<number>(0);
|
||||||
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
||||||
@@ -70,6 +67,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
isEntityValuePanelOpen,
|
isEntityValuePanelOpen,
|
||||||
{ setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse },
|
{ setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse },
|
||||||
] = useBoolean(false);
|
] = useBoolean(false);
|
||||||
|
const [formError, setFormError] = useState<string>("");
|
||||||
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
|
||||||
/* Get default and previous saved entity headers */
|
/* Get default and previous saved entity headers */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,19 +97,36 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* Add new entity attribute */
|
/* Add new entity attribute */
|
||||||
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
|
const onSubmit = async (): Promise<void> => {
|
||||||
if (!isValidEntities(entities)) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
return undefined;
|
const { property, type } = entities[i];
|
||||||
|
if (property === "" || property === undefined) {
|
||||||
|
setFormError(`Property name cannot be empty. Please enter a property name`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExecuting(true);
|
||||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||||
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||||
|
try {
|
||||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||||
tableEntityListViewModel.redrawTableThrottled();
|
tableEntityListViewModel.redrawTableThrottled();
|
||||||
}
|
}
|
||||||
closeSidePanel();
|
} catch (error) {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
setFormError(errorMessage);
|
||||||
|
handleError(errorMessage, "AddTableRow");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsExecuting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
||||||
@@ -200,10 +216,35 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
setIsEntityValuePanelTrue();
|
setIsEntityValuePanelTrue();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPanelContent = (): JSX.Element => {
|
if (isEntityValuePanelOpen) {
|
||||||
return (
|
return (
|
||||||
<form className="panelFormWrapper">
|
<Stack style={{ padding: "20px 34px" }}>
|
||||||
<div className="panelFormWrapper">
|
<Stack horizontal {...columnProps}>
|
||||||
|
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
|
||||||
|
<Label>{entityAttributeProperty}</Label>
|
||||||
|
</Stack>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
rows={5}
|
||||||
|
value={entityAttributeValue}
|
||||||
|
onChange={(event, newInput?: string) => {
|
||||||
|
entityChange(newInput, selectedRow, "value");
|
||||||
|
setEntityAttributeValue(newInput);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: RightPaneFormProps = {
|
||||||
|
formError,
|
||||||
|
isExecuting,
|
||||||
|
submitButtonText: getButtonLabel(userContext.apiType),
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RightPaneForm {...props}>
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
{entities.map((entity, index) => {
|
{entities.map((entity, index) => {
|
||||||
return (
|
return (
|
||||||
@@ -249,61 +290,6 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="paneFooter">
|
</RightPaneForm>
|
||||||
<div className="leftpanel-okbut">
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
onClick={submit}
|
|
||||||
className="genericPaneSubmitBtn"
|
|
||||||
value={getButtonLabel(userContext.apiType)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => {
|
|
||||||
return (
|
|
||||||
<Stack horizontal {...columnProps}>
|
|
||||||
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
|
|
||||||
<Label>{entityAttributeProperty}</Label>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isEntityValuePanelOpen) {
|
|
||||||
return (
|
|
||||||
<PanelContainerComponent
|
|
||||||
headerText=""
|
|
||||||
onRenderNavigationContent={onRenderNavigationContent}
|
|
||||||
panelWidth="700px"
|
|
||||||
isOpen={true}
|
|
||||||
panelContent={
|
|
||||||
<TextField
|
|
||||||
multiline
|
|
||||||
rows={5}
|
|
||||||
className="entityValueTextField"
|
|
||||||
value={entityAttributeValue}
|
|
||||||
onChange={(event, newInput?: string) => {
|
|
||||||
entityChange(newInput, selectedRow, "value");
|
|
||||||
setEntityAttributeValue(newInput);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isConsoleExpanded={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PanelContainerComponent
|
|
||||||
headerText={getPanelTitle(userContext.apiType)}
|
|
||||||
panelWidth="700px"
|
|
||||||
isOpen={true}
|
|
||||||
panelContent={renderPanelContent()}
|
|
||||||
isConsoleExpanded={false}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
|
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||||
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
||||||
|
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { TableEntity } from "../../../Common/TableEntity";
|
import { TableEntity } from "../../../Common/TableEntity";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as TableConstants from "../../Tables/Constants";
|
import * as TableConstants from "../../Tables/Constants";
|
||||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||||
@@ -14,7 +14,7 @@ import * as Entities from "../../Tables/Entities";
|
|||||||
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
|
||||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||||
import { PanelContainerComponent } from "../PanelContainerComponent";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
import {
|
import {
|
||||||
attributeNameLabel,
|
attributeNameLabel,
|
||||||
attributeValueLabel,
|
attributeValueLabel,
|
||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
getEntityValuePlaceholder,
|
getEntityValuePlaceholder,
|
||||||
getFormattedTime,
|
getFormattedTime,
|
||||||
imageProps,
|
imageProps,
|
||||||
isValidEntities,
|
|
||||||
options,
|
options,
|
||||||
} from "./Validators/EntityTableHelper";
|
} from "./Validators/EntityTableHelper";
|
||||||
|
|
||||||
@@ -59,12 +58,13 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
tableEntityListViewModel,
|
tableEntityListViewModel,
|
||||||
cassandraApiClient,
|
cassandraApiClient,
|
||||||
}: EditTableEntityPanelProps): JSX.Element => {
|
}: EditTableEntityPanelProps): JSX.Element => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
|
||||||
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
||||||
const [selectedRow, setSelectedRow] = useState<number>(0);
|
const [selectedRow, setSelectedRow] = useState<number>(0);
|
||||||
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
||||||
const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({});
|
const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({});
|
||||||
const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>("");
|
const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>("");
|
||||||
|
const [formError, setFormError] = useState<string>("");
|
||||||
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
isEntityValuePanelOpen,
|
isEntityValuePanelOpen,
|
||||||
@@ -190,14 +190,26 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
return displayValue;
|
return displayValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
|
const onSubmit = async (): Promise<void> => {
|
||||||
if (!isValidEntities(entities)) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
return undefined;
|
const { property, type } = entities[i];
|
||||||
|
if (property === "" || property === undefined) {
|
||||||
|
setFormError(`Property name cannot be empty. Please enter a property name`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
|
if (!type) {
|
||||||
|
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExecuting(true);
|
||||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||||
const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient;
|
const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient;
|
||||||
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
|
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
|
||||||
|
|
||||||
|
try {
|
||||||
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
|
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
|
||||||
queryTablesTab.collection,
|
queryTablesTab.collection,
|
||||||
originalDocumentData,
|
originalDocumentData,
|
||||||
@@ -209,7 +221,13 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
}
|
}
|
||||||
tableEntityListViewModel.selected.removeAll();
|
tableEntityListViewModel.selected.removeAll();
|
||||||
tableEntityListViewModel.selected.push(newEntity);
|
tableEntityListViewModel.selected.push(newEntity);
|
||||||
closeSidePanel();
|
} catch (error) {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
handleError(errorMessage, "EditTableRow");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsExecuting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
||||||
@@ -299,10 +317,35 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
setIsEntityValuePanelTrue();
|
setIsEntityValuePanelTrue();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPanelContent = (): JSX.Element => {
|
if (isEntityValuePanelOpen) {
|
||||||
return (
|
return (
|
||||||
<form className="panelFormWrapper">
|
<Stack style={{ padding: "20px 34px" }}>
|
||||||
<div className="panelFormWrapper">
|
<Stack horizontal {...columnProps}>
|
||||||
|
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
|
||||||
|
<Label>{entityAttributeProperty}</Label>
|
||||||
|
</Stack>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
rows={5}
|
||||||
|
value={entityAttributeValue}
|
||||||
|
onChange={(event, newInput?: string) => {
|
||||||
|
setEntityAttributeValue(newInput);
|
||||||
|
entityChange(newInput, selectedRow, "value");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: RightPaneFormProps = {
|
||||||
|
formError,
|
||||||
|
isExecuting,
|
||||||
|
submitButtonText: "Update",
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RightPaneForm {...props}>
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
{entities.map((entity, index) => {
|
{entities.map((entity, index) => {
|
||||||
return (
|
return (
|
||||||
@@ -349,59 +392,6 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{renderPanelFooter()}
|
</RightPaneForm>
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPanelFooter = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<div className="paneFooter">
|
|
||||||
<div className="leftpanel-okbut">
|
|
||||||
<input type="submit" onClick={submit} className="genericPaneSubmitBtn" value="Update Entity" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => (
|
|
||||||
<Stack horizontal {...columnProps}>
|
|
||||||
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
|
|
||||||
<Label>{entityAttributeProperty}</Label>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
if (isEntityValuePanelOpen) {
|
|
||||||
return (
|
|
||||||
<PanelContainerComponent
|
|
||||||
headerText=""
|
|
||||||
onRenderNavigationContent={onRenderNavigationContent}
|
|
||||||
panelWidth="700px"
|
|
||||||
isOpen={true}
|
|
||||||
panelContent={
|
|
||||||
<TextField
|
|
||||||
multiline
|
|
||||||
rows={5}
|
|
||||||
className="entityValueTextField"
|
|
||||||
value={entityAttributeValue}
|
|
||||||
onChange={(event, newInput?: string) => {
|
|
||||||
setEntityAttributeValue(newInput);
|
|
||||||
entityChange(newInput, selectedRow, "value");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isConsoleExpanded={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PanelContainerComponent
|
|
||||||
headerText="Edit Table Entity"
|
|
||||||
panelWidth="700px"
|
|
||||||
isOpen={true}
|
|
||||||
panelContent={renderPanelContent()}
|
|
||||||
isConsoleExpanded={false}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2
|
|||||||
|
|
||||||
export const columnProps: Partial<IStackProps> = {
|
export const columnProps: Partial<IStackProps> = {
|
||||||
tokens: { childrenGap: 10 },
|
tokens: { childrenGap: 10 },
|
||||||
styles: { root: { width: 680 } },
|
styles: { root: { marginBottom: 8 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
@@ -134,8 +134,8 @@ export const getEntityValuePlaceholder = (entityType: string | number): string =
|
|||||||
|
|
||||||
export const isValidEntities = (entities: EntityRowType[]): boolean => {
|
export const isValidEntities = (entities: EntityRowType[]): boolean => {
|
||||||
for (let i = 0; i < entities.length; i++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
const { property } = entities[i];
|
const { property, type } = entities[i];
|
||||||
if (property === "" || property === undefined) {
|
if (property === "" || property === undefined || !type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,13 +170,6 @@ export const getDefaultEntities = (headers: string[], entityTypes: EntityType):
|
|||||||
return defaultEntities;
|
return defaultEntities;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPanelTitle = (apiType: string): string => {
|
|
||||||
if (apiType === "Cassandra") {
|
|
||||||
return "Add Table Row";
|
|
||||||
}
|
|
||||||
return "Add Table Row";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAddButtonLabel = (apiType: string): string => {
|
export const getAddButtonLabel = (apiType: string): string => {
|
||||||
if (apiType === "Cassandra") {
|
if (apiType === "Cassandra") {
|
||||||
return "Add Row";
|
return "Add Row";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import CollectionIcon from "../../../images/tree-collection.svg";
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../../Utils/APITypeUtils";
|
||||||
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
|
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
|
||||||
@@ -23,6 +24,8 @@ import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
|||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
import { AddDatabasePanel } from "../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||||
|
import { BrowseQueriesPane } from "../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
@@ -173,7 +176,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
<li>
|
<li>
|
||||||
<a role="link" href={SplashScreen.seeMoreItemUrl} target="_blank" tabIndex={0}>
|
<a role="link" href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}>
|
||||||
{SplashScreen.seeMoreItemTitle}
|
{SplashScreen.seeMoreItemTitle}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -241,20 +244,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
iconSrc: NewQueryIcon,
|
iconSrc: NewQueryIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined);
|
||||||
},
|
},
|
||||||
title: "New SQL Query",
|
title: "New SQL Query",
|
||||||
description: null,
|
description: undefined,
|
||||||
});
|
});
|
||||||
} else if (userContext.apiType === "Mongo") {
|
} else if (userContext.apiType === "Mongo") {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: NewQueryIcon,
|
iconSrc: NewQueryIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
|
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
|
||||||
},
|
},
|
||||||
title: "New Query",
|
title: "New Query",
|
||||||
description: null,
|
description: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +265,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
items.push({
|
items.push({
|
||||||
iconSrc: OpenQueryIcon,
|
iconSrc: OpenQueryIcon,
|
||||||
title: "Open Query",
|
title: "Open Query",
|
||||||
description: null,
|
description: undefined,
|
||||||
onClick: () => this.container.openBrowseQueriesPanel(),
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.container} />),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,10 +277,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
items.push({
|
items.push({
|
||||||
iconSrc: NewStoredProcedureIcon,
|
iconSrc: NewStoredProcedureIcon,
|
||||||
title: "New Stored Procedure",
|
title: "New Stored Procedure",
|
||||||
description: null,
|
description: undefined,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -286,7 +292,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
items.push({
|
items.push({
|
||||||
iconSrc: ScaleAndSettingsIcon,
|
iconSrc: ScaleAndSettingsIcon,
|
||||||
title: label,
|
title: label,
|
||||||
description: null,
|
description: undefined,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onSettingsClick();
|
selectedCollection && selectedCollection.onSettingsClick();
|
||||||
@@ -296,8 +302,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
items.push({
|
items.push({
|
||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
title: "New " + getDatabaseName(),
|
title: "New " + getDatabaseName(),
|
||||||
description: null,
|
description: undefined,
|
||||||
onClick: () => this.container.openAddDatabasePane(),
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,19 +357,19 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
private createTipsItems(): SplashScreenItem[] {
|
private createTipsItems(): SplashScreenItem[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: undefined,
|
||||||
title: "Data Modeling",
|
title: "Data Modeling",
|
||||||
description: "Learn more about modeling",
|
description: "Learn more about modeling",
|
||||||
onClick: () => window.open(SplashScreen.dataModelingUrl),
|
onClick: () => window.open(SplashScreen.dataModelingUrl),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: undefined,
|
||||||
title: "Cost & Throughput Calculation",
|
title: "Cost & Throughput Calculation",
|
||||||
description: "Learn more about cost calculation",
|
description: "Learn more about cost calculation",
|
||||||
onClick: () => window.open(SplashScreen.throughputEstimatorUrl),
|
onClick: () => window.open(SplashScreen.throughputEstimatorUrl),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: undefined,
|
||||||
title: "Configure automatic failover",
|
title: "Configure automatic failover",
|
||||||
description: "Learn more about Cosmos DB high-availability",
|
description: "Learn more about Cosmos DB high-availability",
|
||||||
onClick: () => window.open(SplashScreen.failoverUrl),
|
onClick: () => window.open(SplashScreen.failoverUrl),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export function getQuotedCqlIdentifier(identifier: string): string {
|
// Added return type optional undefined because passing undefined from test cases.
|
||||||
|
export function getQuotedCqlIdentifier(identifier: string | undefined): string | undefined {
|
||||||
let result = identifier;
|
let result = identifier;
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -22,12 +22,15 @@ import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
|||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
|
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import "./QueryTabComponent.less";
|
import "./QueryTabComponent.less";
|
||||||
|
|
||||||
@@ -389,13 +392,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSaveQueryClick = (): void => {
|
public onSaveQueryClick = (): void => {
|
||||||
this.props.collection && this.props.collection.container && this.props.collection.container.openSaveQueryPanel();
|
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSavedQueriesClick = (): void => {
|
public onSavedQueriesClick = (): void => {
|
||||||
this.props.collection &&
|
useSidePanel
|
||||||
this.props.collection.container &&
|
.getState()
|
||||||
this.props.collection.container.openBrowseQueriesPanel();
|
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
public async onFetchNextPageClick(): Promise<void> {
|
public async onFetchNextPageClick(): Promise<void> {
|
||||||
|
|||||||
@@ -137,13 +137,14 @@ export default class QueryTablesTab extends TabsBase {
|
|||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
"Add Table Entity",
|
"Add Table Row",
|
||||||
<AddTableEntityPanel
|
<AddTableEntityPanel
|
||||||
tableDataClient={this.tableDataClient}
|
tableDataClient={this.tableDataClient}
|
||||||
queryTablesTab={this}
|
queryTablesTab={this}
|
||||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
tableEntityListViewModel={this.tableEntityListViewModel()}
|
||||||
cassandraApiClient={new CassandraAPIDataClient()}
|
cassandraApiClient={new CassandraAPIDataClient()}
|
||||||
/>
|
/>,
|
||||||
|
"700px"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -157,7 +158,8 @@ export default class QueryTablesTab extends TabsBase {
|
|||||||
queryTablesTab={this}
|
queryTablesTab={this}
|
||||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
tableEntityListViewModel={this.tableEntityListViewModel()}
|
||||||
cassandraApiClient={new CassandraAPIDataClient()}
|
cassandraApiClient={new CassandraAPIDataClient()}
|
||||||
/>
|
/>,
|
||||||
|
"700px"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ import Collection from "./Collection";
|
|||||||
jest.mock("monaco-editor");
|
jest.mock("monaco-editor");
|
||||||
|
|
||||||
describe("Collection", () => {
|
describe("Collection", () => {
|
||||||
function generateCollection(
|
const generateCollection = (container: Explorer, databaseId: string, data: DataModels.Collection): Collection =>
|
||||||
container: Explorer,
|
new Collection(container, databaseId, data);
|
||||||
databaseId: string,
|
|
||||||
data: DataModels.Collection,
|
|
||||||
offer: DataModels.Offer
|
|
||||||
): Collection {
|
|
||||||
return new Collection(container, databaseId, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateMockCollectionsDataModelWithPartitionKey(
|
const generateMockCollectionsDataModelWithPartitionKey = (
|
||||||
partitionKey: DataModels.PartitionKey
|
partitionKey: DataModels.PartitionKey
|
||||||
): DataModels.Collection {
|
): DataModels.Collection => {
|
||||||
return {
|
return {
|
||||||
defaultTtl: 1,
|
defaultTtl: 1,
|
||||||
indexingPolicy: {} as DataModels.IndexingPolicy,
|
indexingPolicy: {} as DataModels.IndexingPolicy,
|
||||||
@@ -26,13 +20,12 @@ describe("Collection", () => {
|
|||||||
_ts: 1,
|
_ts: 1,
|
||||||
id: "",
|
id: "",
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection {
|
const generateMockCollectionWithDataModel = (data: DataModels.Collection): Collection => {
|
||||||
const mockContainer = {} as Explorer;
|
const mockContainer = {} as Explorer;
|
||||||
|
return generateCollection(mockContainer, "abc", data);
|
||||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
describe("Partition key path parsing", () => {
|
describe("Partition key path parsing", () => {
|
||||||
let collection: Collection;
|
let collection: Collection;
|
||||||
@@ -88,7 +81,7 @@ describe("Collection", () => {
|
|||||||
kind: "Hash",
|
kind: "Hash",
|
||||||
});
|
});
|
||||||
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
||||||
expect(collection.partitionKeyPropertyHeader).toBeNull;
|
expect(collection.partitionKeyPropertyHeader).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -571,8 +571,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSettingsClick = async (): Promise<void> => {
|
public onSettingsClick = async (): Promise<void> => {
|
||||||
await this.loadOffer();
|
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
|
await this.loadOffer();
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
description: "Settings node",
|
description: "Settings node",
|
||||||
@@ -744,8 +744,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
StoredProcedure.create(source, event);
|
StoredProcedure.create(source, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewUserDefinedFunctionClick(source: ViewModels.Collection, event: MouseEvent) {
|
public onNewUserDefinedFunctionClick(source: ViewModels.Collection) {
|
||||||
UserDefinedFunction.create(source, event);
|
UserDefinedFunction.create(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) {
|
public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
this.isOfferRead = false;
|
this.isOfferRead = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSettingsClick = () => {
|
public onSettingsClick = (): void => {
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
@@ -193,6 +193,8 @@ export default class Database implements ViewModels.Database {
|
|||||||
//merge collections
|
//merge collections
|
||||||
this.addCollectionsToList(collectionVMs);
|
this.addCollectionsToList(collectionVMs);
|
||||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||||
|
|
||||||
|
useDatabases.getState().updateDatabase(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openAddCollection(database: Database): Promise<void> {
|
public async openAddCollection(database: Database): Promise<void> {
|
||||||
|
|||||||
@@ -1,45 +1,18 @@
|
|||||||
import * as ko from "knockout";
|
import React from "react";
|
||||||
import * as React from "react";
|
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
|
export const ResourceTokenTree: React.FC = (): JSX.Element => {
|
||||||
public parameters: ko.Observable<number>;
|
const collection = useDatabases((state) => state.resourceTokenCollection);
|
||||||
public myNotebooksContentRoot: NotebookContentItem;
|
|
||||||
|
|
||||||
public constructor(private container: Explorer) {
|
const buildCollectionNode = (): TreeNode => {
|
||||||
this.parameters = ko.observable(Date.now());
|
|
||||||
|
|
||||||
useDatabases.subscribe(
|
|
||||||
() => this.triggerRender(),
|
|
||||||
(state) => state.resourceTokenCollection
|
|
||||||
);
|
|
||||||
useSelectedNode.subscribe(() => this.triggerRender());
|
|
||||||
useTabs.subscribe(
|
|
||||||
() => this.triggerRender(),
|
|
||||||
(state) => state.activeTab
|
|
||||||
);
|
|
||||||
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
|
||||||
const dataRootNode = this.buildCollectionNode();
|
|
||||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
public buildCollectionNode(): TreeNode {
|
|
||||||
const collection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return {
|
return {
|
||||||
label: undefined,
|
label: undefined,
|
||||||
@@ -86,9 +59,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children: [collectionNode],
|
children: [collectionNode],
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
public triggerRender() {
|
return <TreeComponent className="dataResourceTree" rootNode={buildCollectionNode()} />;
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
722
src/Explorer/Tree/ResourceTree.tsx
Normal file
722
src/Explorer/Tree/ResourceTree.tsx
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
|
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||||
|
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
||||||
|
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||||
|
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||||
|
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||||
|
import PublishIcon from "../../../images/notebook/publish_content.svg";
|
||||||
|
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||||
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
|
import { Areas } from "../../Common/Constants";
|
||||||
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
|
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
|
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
|
import { useDatabases } from "../useDatabases";
|
||||||
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
import StoredProcedure from "./StoredProcedure";
|
||||||
|
import Trigger from "./Trigger";
|
||||||
|
import UserDefinedFunction from "./UserDefinedFunction";
|
||||||
|
|
||||||
|
export const MyNotebooksTitle = "My Notebooks";
|
||||||
|
export const GitHubReposTitle = "GitHub repos";
|
||||||
|
|
||||||
|
interface ResourceTreeProps {
|
||||||
|
container: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
|
||||||
|
const databases = useDatabases((state) => state.databases);
|
||||||
|
const {
|
||||||
|
isNotebookEnabled,
|
||||||
|
myNotebooksContentRoot,
|
||||||
|
galleryContentRoot,
|
||||||
|
gitHubNotebooksContentRoot,
|
||||||
|
updateNotebookItem,
|
||||||
|
} = useNotebook();
|
||||||
|
const { activeTab, refreshActiveTab } = useTabs();
|
||||||
|
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
|
const pseudoDirPath = "PsuedoDir";
|
||||||
|
|
||||||
|
const buildGalleryCallout = (): JSX.Element => {
|
||||||
|
if (
|
||||||
|
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
||||||
|
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calloutProps: ICalloutProps = {
|
||||||
|
calloutMaxWidth: 350,
|
||||||
|
ariaLabel: "New gallery",
|
||||||
|
role: "alertdialog",
|
||||||
|
gapSpace: 0,
|
||||||
|
target: ".galleryHeader",
|
||||||
|
directionalHint: DirectionalHint.leftTopEdge,
|
||||||
|
onDismiss: () => {
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
|
},
|
||||||
|
setInitialFocus: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const openGalleryProps: ILinkProps = {
|
||||||
|
onClick: () => {
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
|
container.openGallery();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Callout {...calloutProps}>
|
||||||
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||||
|
<Text variant="xLarge" block>
|
||||||
|
New gallery
|
||||||
|
</Text>
|
||||||
|
<Text block>
|
||||||
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||||
|
contributors.
|
||||||
|
</Text>
|
||||||
|
<Link {...openGalleryProps}>Open gallery</Link>
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebooksTree = (): TreeNode => {
|
||||||
|
const notebooksTree: TreeNode = {
|
||||||
|
label: undefined,
|
||||||
|
isExpanded: true,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (galleryContentRoot) {
|
||||||
|
notebooksTree.children.push(buildGalleryNotebooksTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myNotebooksContentRoot) {
|
||||||
|
notebooksTree.children.push(buildMyNotebooksTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
|
// collapse all other notebook nodes
|
||||||
|
notebooksTree.children.forEach((node) => (node.isExpanded = false));
|
||||||
|
notebooksTree.children.push(buildGitHubNotebooksTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
return notebooksTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildGalleryNotebooksTree = (): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: "Gallery",
|
||||||
|
iconSrc: GalleryIcon,
|
||||||
|
className: "notebookHeader galleryHeader",
|
||||||
|
onClick: () => container.openGallery(),
|
||||||
|
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMyNotebooksTree = (): TreeNode => {
|
||||||
|
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||||
|
myNotebooksContentRoot,
|
||||||
|
(item: NotebookContentItem) => {
|
||||||
|
container.openNotebook(item).then((hasOpened) => {
|
||||||
|
if (hasOpened) {
|
||||||
|
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
myNotebooksTree.isExpanded = true;
|
||||||
|
myNotebooksTree.isAlphaSorted = true;
|
||||||
|
// Remove "Delete" menu item from context menu
|
||||||
|
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
|
||||||
|
return myNotebooksTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildGitHubNotebooksTree = (): TreeNode => {
|
||||||
|
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||||
|
gitHubNotebooksContentRoot,
|
||||||
|
(item: NotebookContentItem) => {
|
||||||
|
container.openNotebook(item).then((hasOpened) => {
|
||||||
|
if (hasOpened) {
|
||||||
|
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
gitHubNotebooksTree.contextMenu = [
|
||||||
|
{
|
||||||
|
label: "Manage GitHub settings",
|
||||||
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Manage GitHub settings",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={container}
|
||||||
|
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={container.notebookManager.junoClient}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Disconnect from GitHub",
|
||||||
|
onClick: () => {
|
||||||
|
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
container.notebookManager?.gitHubOAuthService.logout();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
gitHubNotebooksTree.isExpanded = true;
|
||||||
|
gitHubNotebooksTree.isAlphaSorted = true;
|
||||||
|
|
||||||
|
return gitHubNotebooksTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildChildNodes = (
|
||||||
|
container: Explorer,
|
||||||
|
item: NotebookContentItem,
|
||||||
|
onFileClick: (item: NotebookContentItem) => void
|
||||||
|
): TreeNode[] => {
|
||||||
|
if (!item || !item.children) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return item.children.map((item) => {
|
||||||
|
const result =
|
||||||
|
item.type === NotebookContentItemType.Directory
|
||||||
|
? buildNotebookDirectoryNode(item, onFileClick)
|
||||||
|
: buildNotebookFileNode(item, onFileClick);
|
||||||
|
result.timestamp = item.timestamp;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebookFileNode = (
|
||||||
|
item: NotebookContentItem,
|
||||||
|
onFileClick: (item: NotebookContentItem) => void
|
||||||
|
): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon,
|
||||||
|
className: "notebookHeader",
|
||||||
|
onClick: () => onFileClick(item),
|
||||||
|
isSelected: () => {
|
||||||
|
return (
|
||||||
|
activeTab &&
|
||||||
|
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||||
|
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
|
||||||
|
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
|
||||||
|
*/
|
||||||
|
(activeTab as any).notebookPath() === item.path
|
||||||
|
);
|
||||||
|
},
|
||||||
|
contextMenu: createFileContextMenu(container, item),
|
||||||
|
data: item,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => {
|
||||||
|
let items: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => container.renameNotebook(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
iconSrc: DeleteIcon,
|
||||||
|
onClick: () => {
|
||||||
|
container.showOkCancelModalDialog(
|
||||||
|
"Confirm delete",
|
||||||
|
`Are you sure you want to delete "${item.name}"`,
|
||||||
|
"Delete",
|
||||||
|
() => container.deleteNotebookFile(item),
|
||||||
|
"Cancel",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Copy to ...",
|
||||||
|
iconSrc: CopyIcon,
|
||||||
|
onClick: () => copyNotebook(container, item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Download",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => container.downloadFile(item),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (item.type === NotebookContentItemType.Notebook) {
|
||||||
|
items.push({
|
||||||
|
label: "Publish to gallery",
|
||||||
|
iconSrc: PublishIcon,
|
||||||
|
onClick: async () => {
|
||||||
|
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
|
||||||
|
source: Source.ResourceTreeMenu,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = await container.readFile(item);
|
||||||
|
if (content) {
|
||||||
|
await container.publishNotebook(item.name, content);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Copy to ..." isn't needed if github locations are not available
|
||||||
|
if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
|
items = items.filter((item) => item.label !== "Copy to ...");
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyNotebook = async (container: Explorer, item: NotebookContentItem) => {
|
||||||
|
const content = await container.readFile(item);
|
||||||
|
if (content) {
|
||||||
|
container.copyNotebook(item.name, content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => {
|
||||||
|
let items: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Refresh",
|
||||||
|
iconSrc: RefreshIcon,
|
||||||
|
onClick: () => loadSubitems(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
iconSrc: DeleteIcon,
|
||||||
|
onClick: () => {
|
||||||
|
container.showOkCancelModalDialog(
|
||||||
|
"Confirm delete",
|
||||||
|
`Are you sure you want to delete "${item.name}?"`,
|
||||||
|
"Delete",
|
||||||
|
() => container.deleteNotebookFile(item),
|
||||||
|
"Cancel",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => container.renameNotebook(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Directory",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.onCreateDirectory(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Notebook",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.onNewNotebookClicked(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Upload File",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.openUploadFilePanel(item),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
|
||||||
|
if (GitHubUtils.fromContentUri(item.path)) {
|
||||||
|
items = items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.label !== "Delete" &&
|
||||||
|
item.label !== "Rename" &&
|
||||||
|
item.label !== "New Directory" &&
|
||||||
|
item.label !== "Upload File"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebookDirectoryNode = (
|
||||||
|
item: NotebookContentItem,
|
||||||
|
onFileClick: (item: NotebookContentItem) => void
|
||||||
|
): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
iconSrc: undefined,
|
||||||
|
className: "notebookHeader",
|
||||||
|
isAlphaSorted: true,
|
||||||
|
isLeavesParentsSeparate: true,
|
||||||
|
onClick: () => {
|
||||||
|
if (!item.children) {
|
||||||
|
loadSubitems(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isSelected: () => {
|
||||||
|
return (
|
||||||
|
activeTab &&
|
||||||
|
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
||||||
|
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
|
||||||
|
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
|
||||||
|
*/
|
||||||
|
(activeTab as any).notebookPath() === item.path
|
||||||
|
);
|
||||||
|
},
|
||||||
|
contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined,
|
||||||
|
data: item,
|
||||||
|
children: buildChildNodes(container, item, onFileClick),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDataTree = (): TreeNode => {
|
||||||
|
const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => {
|
||||||
|
const databaseNode: TreeNode = {
|
||||||
|
label: database.id(),
|
||||||
|
iconSrc: CosmosDBIcon,
|
||||||
|
isExpanded: false,
|
||||||
|
className: "databaseHeader",
|
||||||
|
children: [],
|
||||||
|
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||||
|
onClick: async (isExpanded) => {
|
||||||
|
useSelectedNode.getState().setSelectedNode(database);
|
||||||
|
// Rewritten version of expandCollapseDatabase():
|
||||||
|
if (isExpanded) {
|
||||||
|
database.collapseDatabase();
|
||||||
|
} else {
|
||||||
|
if (databaseNode.children?.length === 0) {
|
||||||
|
databaseNode.isLoading = true;
|
||||||
|
}
|
||||||
|
await database.expandDatabase();
|
||||||
|
}
|
||||||
|
databaseNode.isLoading = false;
|
||||||
|
useCommandBar.getState().setContextButtons([]);
|
||||||
|
refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id());
|
||||||
|
},
|
||||||
|
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (database.isDatabaseShared()) {
|
||||||
|
databaseNode.children.push({
|
||||||
|
label: "Scale",
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettingsV2]),
|
||||||
|
onClick: database.onSettingsClick.bind(database),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find collections
|
||||||
|
database
|
||||||
|
.collections()
|
||||||
|
.forEach((collection: ViewModels.Collection) =>
|
||||||
|
databaseNode.children.push(buildCollectionNode(database, collection))
|
||||||
|
);
|
||||||
|
|
||||||
|
database.collections.subscribe((collections: ViewModels.Collection[]) => {
|
||||||
|
collections.forEach((collection: ViewModels.Collection) =>
|
||||||
|
databaseNode.children.push(buildCollectionNode(database, collection))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return databaseNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: undefined,
|
||||||
|
isExpanded: true,
|
||||||
|
children: databaseTreeNodes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
|
||||||
|
const children: TreeNode[] = [];
|
||||||
|
children.push({
|
||||||
|
label: collection.getLabel(),
|
||||||
|
onClick: () => {
|
||||||
|
collection.openTab();
|
||||||
|
// push to most recent
|
||||||
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
|
},
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(collection.databaseId, collection.id(), [
|
||||||
|
ViewModels.CollectionTabKind.Documents,
|
||||||
|
ViewModels.CollectionTabKind.Graph,
|
||||||
|
]),
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNotebookEnabled && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed()) {
|
||||||
|
children.push({
|
||||||
|
label: "Schema (Preview)",
|
||||||
|
onClick: collection.onSchemaAnalyzerClick.bind(collection),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
|
||||||
|
children.push({
|
||||||
|
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
|
||||||
|
onClick: collection.onSettingsClick.bind(collection),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(collection.databaseId, collection.id(), [
|
||||||
|
ViewModels.CollectionTabKind.CollectionSettingsV2,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaNode: TreeNode = buildSchemaNode(collection);
|
||||||
|
if (schemaNode) {
|
||||||
|
children.push(schemaNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showScriptNodes) {
|
||||||
|
children.push(buildStoredProcedureNode(collection));
|
||||||
|
children.push(buildUserDefinedFunctionsNode(collection));
|
||||||
|
children.push(buildTriggerNode(collection));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a rewrite of showConflicts
|
||||||
|
const showConflicts =
|
||||||
|
userContext?.databaseAccount?.properties.enableMultipleWriteLocations &&
|
||||||
|
collection.rawDataModel &&
|
||||||
|
!!collection.rawDataModel.conflictResolutionPolicy;
|
||||||
|
|
||||||
|
if (showConflicts) {
|
||||||
|
children.push({
|
||||||
|
label: "Conflicts",
|
||||||
|
onClick: collection.onConflictsClick.bind(collection),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: collection.id(),
|
||||||
|
iconSrc: CollectionIcon,
|
||||||
|
isExpanded: false,
|
||||||
|
children: children,
|
||||||
|
className: "collectionHeader",
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||||
|
onClick: () => {
|
||||||
|
// Rewritten version of expandCollapseCollection
|
||||||
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
|
useCommandBar.getState().setContextButtons([]);
|
||||||
|
refreshActiveTab(
|
||||||
|
(tab: TabsBase) =>
|
||||||
|
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onExpanded: () => {
|
||||||
|
if (showScriptNodes) {
|
||||||
|
collection.loadStoredProcedures();
|
||||||
|
collection.loadUserDefinedFunctions();
|
||||||
|
collection.loadTriggers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
|
||||||
|
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStoredProcedureNode = (collection: ViewModels.Collection): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: "Stored Procedures",
|
||||||
|
children: collection.storedProcedures().map((sp: StoredProcedure) => ({
|
||||||
|
label: sp.id(),
|
||||||
|
onClick: sp.open.bind(sp),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(collection.databaseId, collection.id(), [
|
||||||
|
ViewModels.CollectionTabKind.StoredProcedures,
|
||||||
|
]),
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp),
|
||||||
|
})),
|
||||||
|
onClick: () => {
|
||||||
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
|
||||||
|
refreshActiveTab(
|
||||||
|
(tab: TabsBase) =>
|
||||||
|
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUserDefinedFunctionsNode = (collection: ViewModels.Collection): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: "User Defined Functions",
|
||||||
|
children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({
|
||||||
|
label: udf.id(),
|
||||||
|
onClick: udf.open.bind(udf),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(collection.databaseId, collection.id(), [
|
||||||
|
ViewModels.CollectionTabKind.UserDefinedFunctions,
|
||||||
|
]),
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf),
|
||||||
|
})),
|
||||||
|
onClick: () => {
|
||||||
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
|
||||||
|
refreshActiveTab(
|
||||||
|
(tab: TabsBase) =>
|
||||||
|
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTriggerNode = (collection: ViewModels.Collection): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: "Triggers",
|
||||||
|
children: collection.triggers().map((trigger: Trigger) => ({
|
||||||
|
label: trigger.id(),
|
||||||
|
onClick: trigger.open.bind(trigger),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger),
|
||||||
|
})),
|
||||||
|
onClick: () => {
|
||||||
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
|
||||||
|
refreshActiveTab(
|
||||||
|
(tab: TabsBase) =>
|
||||||
|
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSchemaNode = (collection: ViewModels.Collection): TreeNode => {
|
||||||
|
if (collection.analyticalStorageTtl() === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collection.schema || !collection.schema.fields) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: "Schema",
|
||||||
|
children: getSchemaNodes(collection.schema.fields),
|
||||||
|
onClick: () => {
|
||||||
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
|
||||||
|
refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSchemaNodes = (fields: DataModels.IDataField[]): TreeNode[] => {
|
||||||
|
const schema: any = {};
|
||||||
|
|
||||||
|
//unflatten
|
||||||
|
fields.forEach((field: DataModels.IDataField) => {
|
||||||
|
const path: string[] = field.path.split(".");
|
||||||
|
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
|
||||||
|
let current: any = {};
|
||||||
|
path.forEach((name: string, pathIndex: number) => {
|
||||||
|
if (pathIndex === 0) {
|
||||||
|
if (schema[name] === undefined) {
|
||||||
|
if (pathIndex === path.length - 1) {
|
||||||
|
schema[name] = fieldProperties;
|
||||||
|
} else {
|
||||||
|
schema[name] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = schema[name];
|
||||||
|
} else {
|
||||||
|
if (current[name] === undefined) {
|
||||||
|
if (pathIndex === path.length - 1) {
|
||||||
|
current[name] = fieldProperties;
|
||||||
|
} else {
|
||||||
|
current[name] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current[name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const traverse = (obj: any): TreeNode[] => {
|
||||||
|
const children: TreeNode[] = [];
|
||||||
|
|
||||||
|
if (obj !== undefined && !Array.isArray(obj) && typeof obj === "object") {
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
children.push({ label: key, children: traverse(value) });
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(obj)) {
|
||||||
|
return [{ label: obj[0] }, { label: obj[1] }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
return traverse(schema);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSubitems = async (item: NotebookContentItem): Promise<void> => {
|
||||||
|
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
|
||||||
|
updateNotebookItem(updatedItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataRootNode = buildDataTree();
|
||||||
|
|
||||||
|
if (isNotebookEnabled) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccordionComponent>
|
||||||
|
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||||
|
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{buildGalleryCallout()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ import { Areas } from "../../Common/Constants";
|
|||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
@@ -33,6 +34,7 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
|||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
@@ -624,7 +626,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
gitHubNotebooksTree.contextMenu = [
|
gitHubNotebooksTree.contextMenu = [
|
||||||
{
|
{
|
||||||
label: "Manage GitHub settings",
|
label: "Manage GitHub settings",
|
||||||
onClick: () => this.container.openGitHubReposPanel("Manage GitHub settings"),
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Manage GitHub settings",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={this.container}
|
||||||
|
gitHubClientProp={this.container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={this.container.notebookManager.junoClient}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Disconnect from GitHub",
|
label: "Disconnect from GitHub",
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { TreeComponent, TreeComponentProps, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { useDatabases } from "../useDatabases";
|
|
||||||
import ResourceTokenCollection from "./ResourceTokenCollection";
|
|
||||||
import { ResourceTreeAdapterForResourceToken } from "./ResourceTreeAdapterForResourceToken";
|
|
||||||
|
|
||||||
describe("Resource tree for resource token", () => {
|
|
||||||
const mockContainer = {} as Explorer;
|
|
||||||
const resourceTree = new ResourceTreeAdapterForResourceToken(mockContainer);
|
|
||||||
const mockCollection = {
|
|
||||||
_rid: "fakeRid",
|
|
||||||
_self: "fakeSelf",
|
|
||||||
id: "fakeId",
|
|
||||||
} as DataModels.Collection;
|
|
||||||
const mockResourceTokenCollection: ViewModels.CollectionBase = new ResourceTokenCollection(
|
|
||||||
mockContainer,
|
|
||||||
"fakeDatabaseId",
|
|
||||||
mockCollection
|
|
||||||
);
|
|
||||||
useDatabases.setState({ resourceTokenCollection: mockResourceTokenCollection });
|
|
||||||
|
|
||||||
it("should render", () => {
|
|
||||||
const rootNode: TreeNode = resourceTree.buildCollectionNode();
|
|
||||||
const props: TreeComponentProps = {
|
|
||||||
rootNode,
|
|
||||||
className: "dataResourceTree",
|
|
||||||
};
|
|
||||||
const wrapper = shallow(<TreeComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -30,7 +30,7 @@ export default class UserDefinedFunction {
|
|||||||
this.body = ko.observable(data.body as string);
|
this.body = ko.observable(data.body as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create(source: ViewModels.Collection, event: MouseEvent) {
|
public static create(source: ViewModels.Collection) {
|
||||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1;
|
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1;
|
||||||
const userDefinedFunction = {
|
const userDefinedFunction = {
|
||||||
id: "",
|
id: "",
|
||||||
@@ -104,7 +104,9 @@ export default class UserDefinedFunction {
|
|||||||
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
|
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
|
||||||
this.collection.children.remove(this);
|
this.collection.children.remove(this);
|
||||||
},
|
},
|
||||||
(reason) => {}
|
() => {
|
||||||
|
/**/
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Resource tree for resource token should render 1`] = `
|
|
||||||
<div
|
|
||||||
className="treeComponent dataResourceTree"
|
|
||||||
role="tree"
|
|
||||||
>
|
|
||||||
<TreeNodeComponent
|
|
||||||
generation={0}
|
|
||||||
node={
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Items",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "collectionHeader",
|
|
||||||
"iconSrc": "",
|
|
||||||
"isExpanded": true,
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "fakeId",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"isExpanded": true,
|
|
||||||
"label": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paddingLeft={0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -19,6 +19,8 @@ interface DatabasesState {
|
|||||||
loadDatabaseOffers: () => Promise<void>;
|
loadDatabaseOffers: () => Promise<void>;
|
||||||
isFirstResourceCreated: () => boolean;
|
isFirstResourceCreated: () => boolean;
|
||||||
findSelectedDatabase: () => ViewModels.Database;
|
findSelectedDatabase: () => ViewModels.Database;
|
||||||
|
validateDatabaseId: (id: string) => boolean;
|
||||||
|
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||||
@@ -129,4 +131,12 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
|||||||
|
|
||||||
return selectedNode.collection?.database;
|
return selectedNode.collection?.database;
|
||||||
},
|
},
|
||||||
|
validateDatabaseId: (id: string): boolean => {
|
||||||
|
return !get().databases.some((database) => database.id() === id);
|
||||||
|
},
|
||||||
|
validateCollectionId: async (databaseId: string, collectionId: string): Promise<boolean> => {
|
||||||
|
const database = get().databases.find((db) => db.id() === databaseId);
|
||||||
|
await database.loadCollections();
|
||||||
|
return !database.collections().some((collection) => collection.id() === collectionId);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ window.addEventListener("load", async () => {
|
|||||||
if (openerWindow) {
|
if (openerWindow) {
|
||||||
const params = new URLSearchParams(document.location.search);
|
const params = new URLSearchParams(document.location.search);
|
||||||
await postRobot.send(
|
await postRobot.send(
|
||||||
window,
|
openerWindow,
|
||||||
GitHubConnectorMsgType,
|
GitHubConnectorMsgType,
|
||||||
{
|
{
|
||||||
state: params.get("state"),
|
state: params.get("state"),
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { IContent } from "@nteract/core";
|
import { IContent } from "@nteract/core";
|
||||||
import { fixture } from "@nteract/fixtures";
|
import { fixture } from "@nteract/fixtures";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
|
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||||
import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient";
|
import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient";
|
||||||
import { GitHubContentProvider } from "./GitHubContentProvider";
|
import { GitHubContentProvider } from "./GitHubContentProvider";
|
||||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
|
||||||
|
|
||||||
const gitHubClient = new GitHubClient(() => {});
|
const gitHubClient = new GitHubClient(() => {
|
||||||
|
/**/
|
||||||
|
});
|
||||||
const gitHubContentProvider = new GitHubContentProvider({
|
const gitHubContentProvider = new GitHubContentProvider({
|
||||||
gitHubClient,
|
gitHubClient,
|
||||||
promptForCommitMsg: () => Promise.resolve("commit msg"),
|
promptForCommitMsg: () => Promise.resolve("commit msg"),
|
||||||
@@ -46,7 +48,7 @@ const sampleNotebookModel: IContent<"notebook"> = {
|
|||||||
created: "",
|
created: "",
|
||||||
last_modified: "date",
|
last_modified: "date",
|
||||||
mimetype: "application/x-ipynb+json",
|
mimetype: "application/x-ipynb+json",
|
||||||
content: sampleFile.content ? JSON.parse(sampleFile.content) : null,
|
content: sampleFile.content ? JSON.parse(sampleFile.content) : undefined,
|
||||||
format: "json",
|
format: "json",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ describe("GitHubContentProvider remove", () => {
|
|||||||
it("errors on invalid path", async () => {
|
it("errors on invalid path", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||||
|
|
||||||
const response = await gitHubContentProvider.remove(null, "invalid path").toPromise();
|
const response = await gitHubContentProvider.remove(undefined, "invalid path").toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||||
@@ -63,7 +65,7 @@ describe("GitHubContentProvider remove", () => {
|
|||||||
it("errors on failed read", async () => {
|
it("errors on failed read", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -75,7 +77,7 @@ describe("GitHubContentProvider remove", () => {
|
|||||||
);
|
);
|
||||||
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -90,7 +92,7 @@ describe("GitHubContentProvider remove", () => {
|
|||||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(HttpStatusCodes.NoContent);
|
expect(response.status).toBe(HttpStatusCodes.NoContent);
|
||||||
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
||||||
@@ -102,7 +104,7 @@ describe("GitHubContentProvider get", () => {
|
|||||||
it("errors on invalid path", async () => {
|
it("errors on invalid path", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||||
|
|
||||||
const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise();
|
const response = await gitHubContentProvider.get(undefined, "invalid path", undefined).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||||
@@ -111,7 +113,7 @@ describe("GitHubContentProvider get", () => {
|
|||||||
it("errors on failed read", async () => {
|
it("errors on failed read", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, null).toPromise();
|
const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, undefined).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -122,7 +124,7 @@ describe("GitHubContentProvider get", () => {
|
|||||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise();
|
const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, {}).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -134,7 +136,7 @@ describe("GitHubContentProvider update", () => {
|
|||||||
it("errors on invalid path", async () => {
|
it("errors on invalid path", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||||
|
|
||||||
const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise();
|
const response = await gitHubContentProvider.update(undefined, "invalid path", undefined).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||||
@@ -143,7 +145,7 @@ describe("GitHubContentProvider update", () => {
|
|||||||
it("errors on failed read", async () => {
|
it("errors on failed read", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, null).toPromise();
|
const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, undefined).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -155,7 +157,7 @@ describe("GitHubContentProvider update", () => {
|
|||||||
);
|
);
|
||||||
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -170,7 +172,7 @@ describe("GitHubContentProvider update", () => {
|
|||||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -186,7 +188,7 @@ describe("GitHubContentProvider create", () => {
|
|||||||
it("errors on invalid path", async () => {
|
it("errors on invalid path", async () => {
|
||||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync");
|
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync");
|
||||||
|
|
||||||
const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise();
|
const response = await gitHubContentProvider.create(undefined, "invalid path", sampleNotebookModel).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled();
|
expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled();
|
||||||
@@ -195,7 +197,7 @@ describe("GitHubContentProvider create", () => {
|
|||||||
it("errors on failed create", async () => {
|
it("errors on failed create", async () => {
|
||||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||||
@@ -206,7 +208,7 @@ describe("GitHubContentProvider create", () => {
|
|||||||
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
|
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(HttpStatusCodes.Created);
|
expect(response.status).toBe(HttpStatusCodes.Created);
|
||||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||||
@@ -221,7 +223,7 @@ describe("GitHubContentProvider save", () => {
|
|||||||
it("errors on invalid path", async () => {
|
it("errors on invalid path", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||||
|
|
||||||
const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise();
|
const response = await gitHubContentProvider.save(undefined, "invalid path", undefined).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||||
@@ -230,7 +232,7 @@ describe("GitHubContentProvider save", () => {
|
|||||||
it("errors on failed read", async () => {
|
it("errors on failed read", async () => {
|
||||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, null).toPromise();
|
const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, undefined).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -242,7 +244,7 @@ describe("GitHubContentProvider save", () => {
|
|||||||
);
|
);
|
||||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||||
|
|
||||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(888);
|
expect(response.status).toBe(888);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -257,7 +259,7 @@ describe("GitHubContentProvider save", () => {
|
|||||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||||
@@ -271,7 +273,7 @@ describe("GitHubContentProvider save", () => {
|
|||||||
|
|
||||||
describe("GitHubContentProvider listCheckpoints", () => {
|
describe("GitHubContentProvider listCheckpoints", () => {
|
||||||
it("errors for everything", async () => {
|
it("errors for everything", async () => {
|
||||||
const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise();
|
const response = await gitHubContentProvider.listCheckpoints().toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
});
|
});
|
||||||
@@ -279,7 +281,7 @@ describe("GitHubContentProvider listCheckpoints", () => {
|
|||||||
|
|
||||||
describe("GitHubContentProvider createCheckpoint", () => {
|
describe("GitHubContentProvider createCheckpoint", () => {
|
||||||
it("errors for everything", async () => {
|
it("errors for everything", async () => {
|
||||||
const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise();
|
const response = await gitHubContentProvider.createCheckpoint().toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
});
|
});
|
||||||
@@ -287,7 +289,7 @@ describe("GitHubContentProvider createCheckpoint", () => {
|
|||||||
|
|
||||||
describe("GitHubContentProvider deleteCheckpoint", () => {
|
describe("GitHubContentProvider deleteCheckpoint", () => {
|
||||||
it("errors for everything", async () => {
|
it("errors for everything", async () => {
|
||||||
const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise();
|
const response = await gitHubContentProvider.deleteCheckpoint().toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
});
|
});
|
||||||
@@ -295,7 +297,7 @@ describe("GitHubContentProvider deleteCheckpoint", () => {
|
|||||||
|
|
||||||
describe("GitHubContentProvider restoreFromCheckpoint", () => {
|
describe("GitHubContentProvider restoreFromCheckpoint", () => {
|
||||||
it("errors for everything", async () => {
|
it("errors for everything", async () => {
|
||||||
const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise();
|
const response = await gitHubContentProvider.restoreFromCheckpoint().toPromise();
|
||||||
expect(response).toBeDefined();
|
expect(response).toBeDefined();
|
||||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable";
|
import { makeNotebookRecord, Notebook, stringifyNotebook, toJS } from "@nteract/commutable";
|
||||||
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
||||||
import { from, Observable, of } from "rxjs";
|
import { from, Observable, of } from "rxjs";
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
import { AjaxResponse } from "rxjs/ajax";
|
||||||
import * as Base64Utils from "../Utils/Base64Utils";
|
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
|
||||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
|
||||||
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
|
||||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
|
||||||
import * as UrlUtility from "../Common/UrlUtility";
|
|
||||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../Common/Logger";
|
||||||
|
import * as UrlUtility from "../Common/UrlUtility";
|
||||||
|
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||||
|
import * as Base64Utils from "../Utils/Base64Utils";
|
||||||
|
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||||
|
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
||||||
|
|
||||||
export interface GitHubContentProviderParams {
|
export interface GitHubContentProviderParams {
|
||||||
gitHubClient: GitHubClient;
|
gitHubClient: GitHubClient;
|
||||||
@@ -267,25 +267,25 @@ export class GitHubContentProvider implements IContentProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public listCheckpoints(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
public listCheckpoints(): Observable<AjaxResponse> {
|
||||||
const error = new GitHubContentProviderError("Not implemented");
|
const error = new GitHubContentProviderError("Not implemented");
|
||||||
Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno);
|
Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno);
|
||||||
return of(this.createErrorAjaxResponse(error));
|
return of(this.createErrorAjaxResponse(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
public createCheckpoint(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
public createCheckpoint(): Observable<AjaxResponse> {
|
||||||
const error = new GitHubContentProviderError("Not implemented");
|
const error = new GitHubContentProviderError("Not implemented");
|
||||||
Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno);
|
Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno);
|
||||||
return of(this.createErrorAjaxResponse(error));
|
return of(this.createErrorAjaxResponse(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
public deleteCheckpoint(): Observable<AjaxResponse> {
|
||||||
const error = new GitHubContentProviderError("Not implemented");
|
const error = new GitHubContentProviderError("Not implemented");
|
||||||
Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno);
|
Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno);
|
||||||
return of(this.createErrorAjaxResponse(error));
|
return of(this.createErrorAjaxResponse(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
public restoreFromCheckpoint(): Observable<AjaxResponse> {
|
||||||
const error = new GitHubContentProviderError("Not implemented");
|
const error = new GitHubContentProviderError("Not implemented");
|
||||||
Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno);
|
Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno);
|
||||||
return of(this.createErrorAjaxResponse(error));
|
return of(this.createErrorAjaxResponse(error));
|
||||||
|
|||||||
23
src/Index.ts
23
src/Index.ts
@@ -1,23 +0,0 @@
|
|||||||
import "../less/index.less";
|
|
||||||
import "./Libs/jquery";
|
|
||||||
|
|
||||||
import * as ko from "knockout";
|
|
||||||
|
|
||||||
class Index {
|
|
||||||
public navigationSelection: ko.Observable<string>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.navigationSelection = ko.observable("quickstart");
|
|
||||||
}
|
|
||||||
|
|
||||||
public quickstart_click() {
|
|
||||||
this.navigationSelection("quickstart");
|
|
||||||
}
|
|
||||||
|
|
||||||
public explorer_click() {
|
|
||||||
this.navigationSelection("explorer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = new Index();
|
|
||||||
ko.applyBindings(index);
|
|
||||||
66
src/Index.tsx
Normal file
66
src/Index.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import Arrow from "../images/Arrow.svg";
|
||||||
|
import CosmosDB_20170829 from "../images/CosmosDB_20170829.svg";
|
||||||
|
import Explorer from "../images/Explorer.svg";
|
||||||
|
import Feedback from "../images/Feedback.svg";
|
||||||
|
import Quickstart from "../images/Quickstart.svg";
|
||||||
|
import "../less/index.less";
|
||||||
|
|
||||||
|
const Index = (): JSX.Element => {
|
||||||
|
const [navigationSelection, setNavigationSelection] = useState("quickstart");
|
||||||
|
|
||||||
|
const quickstart_click = () => {
|
||||||
|
setNavigationSelection("quickstart");
|
||||||
|
};
|
||||||
|
|
||||||
|
const explorer_click = () => {
|
||||||
|
setNavigationSelection("explorer");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<header className="header HeaderBg">
|
||||||
|
<div className="items">
|
||||||
|
<img className="DocDBicon" src={CosmosDB_20170829} alt="Azure Cosmos DB" />
|
||||||
|
<a className="createdocdbacnt" href="https://aka.ms/documentdbcreate" rel="noreferrer" target="_blank">
|
||||||
|
Create an Azure Cosmos DB account <img className="rightarrowimg" src={Arrow} alt="" />
|
||||||
|
</a>
|
||||||
|
<span className="title">Azure Cosmos DB Emulator</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<nav className="fixedleftpane">
|
||||||
|
<div
|
||||||
|
id="Quickstart"
|
||||||
|
onClick={quickstart_click}
|
||||||
|
className={navigationSelection === "quickstart" ? "topSelected" : ""}
|
||||||
|
>
|
||||||
|
<img id="imgiconwidth1" src={Quickstart} alt="Open Quick Start" />
|
||||||
|
<span className="menuQuickStart">Quickstart</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="Explorer" onClick={explorer_click} className={navigationSelection === "explorer" ? "topSelected" : ""}>
|
||||||
|
<img id="imgiconwidth1" src={Explorer} alt="Open Data Explorer" />
|
||||||
|
<span className="menuExplorer">Explorer</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a className="feedbackstyle" href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Emulator%20Feedback">
|
||||||
|
<img id="imgiconwidth1" src={Feedback} alt="Report Issue" />
|
||||||
|
<span className="menuExplorer">Report Issue</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{navigationSelection === "quickstart" && (
|
||||||
|
<iframe name="quickstart" className="iframe" src="quickstart.html"></iframe>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{navigationSelection === "explorer" && (
|
||||||
|
<iframe name="explorer" className="iframe" src="explorer.html?platform=Emulator"></iframe>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.render(<Index />, document.getElementById("root"));
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
|
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
|
||||||
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
|
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
|
||||||
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
|
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
|
||||||
"Cost": "Cost",
|
"ApproximateCost": "Approximate Cost Per Hour",
|
||||||
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
|
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
|
||||||
"ConnectionString": "Connection String",
|
"ConnectionString": "Connection String",
|
||||||
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
|
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import "../less/TableStyles/fulldatatables.less";
|
|||||||
import "../less/TableStyles/queryBuilder.less";
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
import "../less/tree.less";
|
import "../less/tree.less";
|
||||||
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
||||||
import { ResourceTree } from "./Common/ResourceTree";
|
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
||||||
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
||||||
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||||
import { Dialog } from "./Explorer/Controls/Dialog";
|
import { Dialog } from "./Explorer/Controls/Dialog";
|
||||||
@@ -84,7 +84,11 @@ const App: React.FunctionComponent = () => {
|
|||||||
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
|
||||||
<div className="collectionsTreeWithSplitter">
|
<div className="collectionsTreeWithSplitter">
|
||||||
{/* Collections Tree Expanded - Start */}
|
{/* Collections Tree Expanded - Start */}
|
||||||
<ResourceTree toggleLeftPaneExpanded={toggleLeftPaneExpanded} isLeftPaneExpanded={isLeftPaneExpanded} />
|
<ResourceTreeContainer
|
||||||
|
container={explorer}
|
||||||
|
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
|
||||||
|
isLeftPaneExpanded={isLeftPaneExpanded}
|
||||||
|
/>
|
||||||
{/* Collections Tree Expanded - End */}
|
{/* Collections Tree Expanded - End */}
|
||||||
{/* Collections Tree Collapsed - Start */}
|
{/* Collections Tree Collapsed - Start */}
|
||||||
<CollapsedResourceTree
|
<CollapsedResourceTree
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const SwitchAccount: FunctionComponent<Props> = ({
|
|||||||
data: account,
|
data: account,
|
||||||
}))}
|
}))}
|
||||||
onChange={(_, option) => {
|
onChange={(_, option) => {
|
||||||
setSelectedAccountName(String(option.key));
|
setSelectedAccountName(String(option?.key));
|
||||||
dismissMenu();
|
dismissMenu();
|
||||||
}}
|
}}
|
||||||
defaultSelectedKey={selectedAccount?.name}
|
defaultSelectedKey={selectedAccount?.name}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const SwitchSubscription: FunctionComponent<Props> = ({
|
|||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
onChange={(_, option) => {
|
onChange={(_, option) => {
|
||||||
setSelectedSubscriptionId(String(option.key));
|
setSelectedSubscriptionId(String(option?.key));
|
||||||
}}
|
}}
|
||||||
defaultSelectedKey={selectedSubscription?.subscriptionId}
|
defaultSelectedKey={selectedSubscription?.subscriptionId}
|
||||||
placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"}
|
placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import * as DataModels from "../../../Contracts/DataModels";
|
|||||||
import { parseConnectionString } from "./ConnectionStringParser";
|
import { parseConnectionString } from "./ConnectionStringParser";
|
||||||
|
|
||||||
describe("ConnectionStringParser", () => {
|
describe("ConnectionStringParser", () => {
|
||||||
const mockAccountName: string = "Test";
|
const mockAccountName = "Test";
|
||||||
const mockMasterKey: string = "some-key";
|
const mockMasterKey = "some-key";
|
||||||
|
|
||||||
it("should parse a valid sql account connection string", () => {
|
it("should parse a valid sql account connection string", () => {
|
||||||
const metadata = parseConnectionString(
|
const metadata = parseConnectionString(
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function getDatabaseAccountKindFromExperience(apiExperience: typeof userC
|
|||||||
return AccountKind.GlobalDocumentDB;
|
return AccountKind.GlobalDocumentDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractMasterKeyfromConnectionString(connectionString: string): string {
|
export function extractMasterKeyfromConnectionString(connectionString: string): string | undefined {
|
||||||
// Only Gremlin uses the actual master key for connection to cosmos
|
// Only Gremlin uses the actual master key for connection to cosmos
|
||||||
const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$");
|
const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$");
|
||||||
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
|
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ export type Features = {
|
|||||||
readonly enableReactPane: boolean;
|
readonly enableReactPane: boolean;
|
||||||
readonly enableRightPanelV2: boolean;
|
readonly enableRightPanelV2: boolean;
|
||||||
readonly enableSchema: boolean;
|
readonly enableSchema: boolean;
|
||||||
enableSchemaAnalyzer: boolean;
|
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
|
partitionKeyDefault: boolean;
|
||||||
readonly enableSDKoperations: boolean;
|
readonly enableSDKoperations: boolean;
|
||||||
readonly enableSpark: boolean;
|
readonly enableSpark: boolean;
|
||||||
readonly enableTtl: boolean;
|
readonly enableTtl: boolean;
|
||||||
readonly executeSproc: boolean;
|
readonly executeSproc: boolean;
|
||||||
readonly enableAadDataPlane: boolean;
|
readonly enableAadDataPlane: boolean;
|
||||||
|
readonly enableKOResourceTree: boolean;
|
||||||
readonly hostedDataExplorer: boolean;
|
readonly hostedDataExplorer: boolean;
|
||||||
readonly junoEndpoint?: string;
|
readonly junoEndpoint?: string;
|
||||||
readonly livyEndpoint?: string;
|
readonly livyEndpoint?: string;
|
||||||
@@ -53,10 +54,10 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableReactPane: "true" === get("enablereactpane"),
|
enableReactPane: "true" === get("enablereactpane"),
|
||||||
enableRightPanelV2: "true" === get("enablerightpanelv2"),
|
enableRightPanelV2: "true" === get("enablerightpanelv2"),
|
||||||
enableSchema: "true" === get("enableschema"),
|
enableSchema: "true" === get("enableschema"),
|
||||||
enableSchemaAnalyzer: "true" === get("enableschemaanalyzer"),
|
|
||||||
enableSDKoperations: "true" === get("enablesdkoperations"),
|
enableSDKoperations: "true" === get("enablesdkoperations"),
|
||||||
enableSpark: "true" === get("enablespark"),
|
enableSpark: "true" === get("enablespark"),
|
||||||
enableTtl: "true" === get("enablettl"),
|
enableTtl: "true" === get("enablettl"),
|
||||||
|
enableKOResourceTree: "true" === get("enablekoresourcetree"),
|
||||||
executeSproc: "true" === get("dataexplorerexecutesproc"),
|
executeSproc: "true" === get("dataexplorerexecutesproc"),
|
||||||
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
|
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
|
||||||
junoEndpoint: get("junoendpoint"),
|
junoEndpoint: get("junoendpoint"),
|
||||||
@@ -70,5 +71,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
showMinRUSurvey: "true" === get("showminrusurvey"),
|
showMinRUSurvey: "true" === get("showminrusurvey"),
|
||||||
ttl90Days: "true" === get("ttl90days"),
|
ttl90Days: "true" === get("ttl90days"),
|
||||||
autoscaleDefault: "true" === get("autoscaledefault"),
|
autoscaleDefault: "true" === get("autoscaledefault"),
|
||||||
|
partitionKeyDefault: "true" === get("partitionkeytest"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { armRequestWithoutPolling } from "../../Utils/arm/request";
|
|||||||
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
||||||
import { RefreshResult } from "../SelfServeTypes";
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
import SqlX from "./SqlX";
|
import SqlX from "./SqlX";
|
||||||
import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes";
|
import {
|
||||||
|
FetchPricesResponse,
|
||||||
|
RegionsResponse,
|
||||||
|
SqlxServiceResource,
|
||||||
|
UpdateDedicatedGatewayRequestParameters,
|
||||||
|
} from "./SqlxTypes";
|
||||||
|
|
||||||
const apiVersion = "2021-04-01-preview";
|
const apiVersion = "2021-04-01-preview";
|
||||||
|
|
||||||
@@ -128,3 +133,67 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResu
|
|||||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
|
||||||
|
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getReadRegions = async (): Promise<Array<string>> => {
|
||||||
|
try {
|
||||||
|
const readRegions = new Array<string>();
|
||||||
|
|
||||||
|
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||||
|
host: configContext.ARM_ENDPOINT,
|
||||||
|
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: "2021-04-01-preview",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.result.location !== undefined) {
|
||||||
|
readRegions.push(response.result.location.replace(" ", "").toLowerCase());
|
||||||
|
} else {
|
||||||
|
for (const location of response.result.locations) {
|
||||||
|
readRegions.push(location.locationName.replace(" ", "").toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readRegions;
|
||||||
|
} catch (err) {
|
||||||
|
return new Array<string>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||||
|
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
|
||||||
|
try {
|
||||||
|
const priceMap = new Map<string, Map<string, number>>();
|
||||||
|
|
||||||
|
for (const region of regions) {
|
||||||
|
const regionPriceMap = new Map<string, number>();
|
||||||
|
|
||||||
|
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||||
|
host: configContext.ARM_ENDPOINT,
|
||||||
|
path: getFetchPricesPathForRegion(userContext.subscriptionId),
|
||||||
|
method: "POST",
|
||||||
|
apiVersion: "2020-01-01-preview",
|
||||||
|
queryParams: {
|
||||||
|
filter:
|
||||||
|
"armRegionName eq '" +
|
||||||
|
region +
|
||||||
|
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of response.result.Items) {
|
||||||
|
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||||
|
}
|
||||||
|
priceMap.set(region, regionPriceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceMap;
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
|||||||
import {
|
import {
|
||||||
deleteDedicatedGatewayResource,
|
deleteDedicatedGatewayResource,
|
||||||
getCurrentProvisioningState,
|
getCurrentProvisioningState,
|
||||||
|
getPriceMap,
|
||||||
|
getReadRegions,
|
||||||
refreshDedicatedGatewayProvisioning,
|
refreshDedicatedGatewayProvisioning,
|
||||||
updateDedicatedGatewayResource,
|
updateDedicatedGatewayResource,
|
||||||
} from "./SqlX.rp";
|
} from "./SqlX.rp";
|
||||||
|
|
||||||
const costPerHourValue: Description = {
|
const costPerHourDefaultValue: Description = {
|
||||||
textTKey: "CostText",
|
textTKey: "CostText",
|
||||||
type: DescriptionType.Text,
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
@@ -53,7 +55,10 @@ const CosmosD16s = "Cosmos.D16s";
|
|||||||
|
|
||||||
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
||||||
currentValues.set("sku", { value: newValue });
|
currentValues.set("sku", { value: newValue });
|
||||||
currentValues.set("costPerHour", { value: costPerHourValue });
|
currentValues.set("costPerHour", {
|
||||||
|
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
|
||||||
|
});
|
||||||
|
|
||||||
return currentValues;
|
return currentValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,6 +84,11 @@ const onNumberOfInstancesChange = (
|
|||||||
} else {
|
} else {
|
||||||
currentValues.set("warningBanner", undefined);
|
currentValues.set("warningBanner", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentValues.set("costPerHour", {
|
||||||
|
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
|
||||||
|
});
|
||||||
|
|
||||||
return currentValues;
|
return currentValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,6 +121,11 @@ const onEnableDedicatedGatewayChange = (
|
|||||||
} as Description,
|
} as Description,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentValues.set("costPerHour", {
|
||||||
|
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
|
||||||
|
hidden: false,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
currentValues.set("warningBanner", {
|
currentValues.set("warningBanner", {
|
||||||
value: {
|
value: {
|
||||||
@@ -122,6 +137,8 @@ const onEnableDedicatedGatewayChange = (
|
|||||||
} as Description,
|
} as Description,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
|
||||||
}
|
}
|
||||||
const sku = currentValues.get("sku");
|
const sku = currentValues.get("sku");
|
||||||
const instances = currentValues.get("instances");
|
const instances = currentValues.get("instances");
|
||||||
@@ -137,7 +154,6 @@ const onEnableDedicatedGatewayChange = (
|
|||||||
disabled: dedicatedGatewayOriginallyEnabled,
|
disabled: dedicatedGatewayOriginallyEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes });
|
|
||||||
currentValues.set("connectionString", {
|
currentValues.set("connectionString", {
|
||||||
value: connectionStringValue,
|
value: connectionStringValue,
|
||||||
hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
|
hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
|
||||||
@@ -177,6 +193,40 @@ const NumberOfInstancesDropdownInfo: Info = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ApproximateCostDropDownInfo: Info = {
|
||||||
|
messageTKey: "CostText",
|
||||||
|
link: {
|
||||||
|
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
|
||||||
|
textTKey: "DedicatedGatewayPricing",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let priceMap: Map<string, Map<string, number>>;
|
||||||
|
let regions: Array<string>;
|
||||||
|
|
||||||
|
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||||
|
try {
|
||||||
|
let costPerHour = 0;
|
||||||
|
for (const region of regions) {
|
||||||
|
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
|
||||||
|
if (incrementalCost === undefined) {
|
||||||
|
throw new Error("Value not found in map.");
|
||||||
|
}
|
||||||
|
costPerHour += incrementalCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
costPerHour *= instanceCount;
|
||||||
|
costPerHour = Math.round(costPerHour * 100) / 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
textTKey: `${costPerHour} USD`,
|
||||||
|
type: DescriptionType.Text,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return costPerHourDefaultValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@IsDisplayable()
|
@IsDisplayable()
|
||||||
@RefreshOptions({ retryIntervalInMs: 20000 })
|
@RefreshOptions({ retryIntervalInMs: 20000 })
|
||||||
export default class SqlX extends SelfServeBaseClass {
|
export default class SqlX extends SelfServeBaseClass {
|
||||||
@@ -274,12 +324,15 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
regions = await getReadRegions();
|
||||||
|
priceMap = await getPriceMap(regions);
|
||||||
|
|
||||||
const response = await getCurrentProvisioningState();
|
const response = await getCurrentProvisioningState();
|
||||||
if (response.status && response.status !== "Deleting") {
|
if (response.status && response.status !== "Deleting") {
|
||||||
defaults.set("enableDedicatedGateway", { value: true });
|
defaults.set("enableDedicatedGateway", { value: true });
|
||||||
defaults.set("sku", { value: response.sku, disabled: true });
|
defaults.set("sku", { value: response.sku, disabled: true });
|
||||||
defaults.set("instances", { value: response.instances, disabled: false });
|
defaults.set("instances", { value: response.instances, disabled: false });
|
||||||
defaults.set("costPerHour", { value: costPerHourValue });
|
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
|
||||||
defaults.set("connectionString", {
|
defaults.set("connectionString", {
|
||||||
value: connectionStringValue,
|
value: connectionStringValue,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
@@ -338,8 +391,9 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
})
|
})
|
||||||
instances: number;
|
instances: number;
|
||||||
|
|
||||||
|
@PropertyInfo(ApproximateCostDropDownInfo)
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "Cost",
|
labelTKey: "ApproximateCost",
|
||||||
isDynamicDescription: true,
|
isDynamicDescription: true,
|
||||||
})
|
})
|
||||||
costPerHour: string;
|
costPerHour: string;
|
||||||
|
|||||||
@@ -29,3 +29,23 @@ export type UpdateDedicatedGatewayRequestProperties = {
|
|||||||
instanceCount: number;
|
instanceCount: number;
|
||||||
serviceType: string;
|
serviceType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FetchPricesResponse = {
|
||||||
|
Items: Array<PriceItem>;
|
||||||
|
NextPageLink: string | undefined;
|
||||||
|
Count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceItem = {
|
||||||
|
retailPrice: number;
|
||||||
|
skuName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegionsResponse = {
|
||||||
|
locations: Array<RegionItem>;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegionItem = {
|
||||||
|
locationName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe("Default Experience Utility", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getApiKindFromDefaultExperience()", () => {
|
describe("getApiKindFromDefaultExperience()", () => {
|
||||||
function runScenario(defaultExperience: typeof userContext.apiType, expectedApiKind: number): void {
|
function runScenario(defaultExperience: typeof userContext.apiType | null, expectedApiKind: number): void {
|
||||||
const resolvedApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience);
|
const resolvedApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience);
|
||||||
expect(resolvedApiKind).toEqual(expectedApiKind);
|
expect(resolvedApiKind).toEqual(expectedApiKind);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "./StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "./StorageUtility";
|
||||||
|
|
||||||
export class ExplorerSettings {
|
export const createDefaultSettings = () => {
|
||||||
public static createDefaultSettings() {
|
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, Constants.Queries.itemsPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, Constants.Queries.itemsPerPage);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage);
|
||||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
|
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
|
||||||
LocalStorageUtility.setEntryNumber(
|
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism);
|
||||||
StorageKey.MaxDegreeOfParellism,
|
};
|
||||||
Constants.Queries.DefaultMaxDegreeOfParallelism
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static hasSettingsDefined(): boolean {
|
export const hasSettingsDefined = (): boolean => {
|
||||||
return (
|
return (
|
||||||
LocalStorageUtility.hasItem(StorageKey.ActualItemPerPage) &&
|
LocalStorageUtility.hasItem(StorageKey.ActualItemPerPage) &&
|
||||||
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled) &&
|
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled) &&
|
||||||
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
|
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
22
src/Shared/LocalStorageUtility.ts
Normal file
22
src/Shared/LocalStorageUtility.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { StorageKey } from "./StorageUtility";
|
||||||
|
import * as StringUtility from "./StringUtility";
|
||||||
|
|
||||||
|
export const hasItem = (key: StorageKey): boolean => !!localStorage.getItem(StorageKey[key]);
|
||||||
|
|
||||||
|
export const getEntryString = (key: StorageKey): string | null => localStorage.getItem(StorageKey[key]);
|
||||||
|
|
||||||
|
export const getEntryNumber = (key: StorageKey): number =>
|
||||||
|
StringUtility.toNumber(localStorage.getItem(StorageKey[key]));
|
||||||
|
|
||||||
|
export const getEntryBoolean = (key: StorageKey): boolean =>
|
||||||
|
StringUtility.toBoolean(localStorage.getItem(StorageKey[key]));
|
||||||
|
|
||||||
|
export const setEntryString = (key: StorageKey, value: string): void => localStorage.setItem(StorageKey[key], value);
|
||||||
|
|
||||||
|
export const removeEntry = (key: StorageKey): void => localStorage.removeItem(StorageKey[key]);
|
||||||
|
|
||||||
|
export const setEntryNumber = (key: StorageKey, value: number): void =>
|
||||||
|
localStorage.setItem(StorageKey[key], value.toString());
|
||||||
|
|
||||||
|
export const setEntryBoolean = (key: StorageKey, value: boolean): void =>
|
||||||
|
localStorage.setItem(StorageKey[key], value.toString());
|
||||||
@@ -2,26 +2,26 @@ import * as Constants from "./Constants";
|
|||||||
|
|
||||||
export function computeRUUsagePrice(serverId: string, requestUnits: number): string {
|
export function computeRUUsagePrice(serverId: string, requestUnits: number): string {
|
||||||
if (serverId === "mooncake") {
|
if (serverId === "mooncake") {
|
||||||
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU;
|
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU;
|
||||||
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
|
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
||||||
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string {
|
export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string {
|
||||||
if (serverId === "mooncake") {
|
if (serverId === "mooncake") {
|
||||||
let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerGB;
|
const storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerGB;
|
||||||
return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
|
return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerGB;
|
const storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerGB;
|
||||||
return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeDisplayUsageString(usageInKB: number): string {
|
export function computeDisplayUsageString(usageInKB: number): string {
|
||||||
let usageInMB = usageInKB / 1024,
|
const usageInMB = usageInKB / 1024,
|
||||||
usageInGB = usageInMB / 1024,
|
usageInGB = usageInMB / 1024,
|
||||||
displayUsageString =
|
displayUsageString =
|
||||||
usageInGB > 0.1
|
usageInGB > 0.1
|
||||||
@@ -33,7 +33,7 @@ export function computeDisplayUsageString(usageInKB: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usageInGB(usageInKB: number): number {
|
export function usageInGB(usageInKB: number): number {
|
||||||
let usageInMB = usageInKB / 1024,
|
const usageInMB = usageInKB / 1024,
|
||||||
usageInGB = usageInMB / 1024;
|
usageInGB = usageInMB / 1024;
|
||||||
return Math.ceil(usageInGB);
|
return Math.ceil(usageInGB);
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/Shared/SessionStorageUtility.ts
Normal file
20
src/Shared/SessionStorageUtility.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { StorageKey } from "./StorageUtility";
|
||||||
|
import * as StringUtility from "./StringUtility";
|
||||||
|
|
||||||
|
export const hasItem = (key: StorageKey): boolean => !!sessionStorage.getItem(StorageKey[key]);
|
||||||
|
|
||||||
|
export const getEntryString = (key: StorageKey): string | null => sessionStorage.getItem(StorageKey[key]);
|
||||||
|
|
||||||
|
export const getEntryNumber = (key: StorageKey): number =>
|
||||||
|
StringUtility.toNumber(sessionStorage.getItem(StorageKey[key]));
|
||||||
|
|
||||||
|
export const getEntry = (key: string): string | null => sessionStorage.getItem(key);
|
||||||
|
|
||||||
|
export const removeEntry = (key: StorageKey): void => sessionStorage.removeItem(StorageKey[key]);
|
||||||
|
|
||||||
|
export const setEntryString = (key: StorageKey, value: string): void => sessionStorage.setItem(StorageKey[key], value);
|
||||||
|
|
||||||
|
export const setEntry = (key: string, value: string): void => sessionStorage.setItem(key, value);
|
||||||
|
|
||||||
|
export const setEntryNumber = (key: StorageKey, value: number): void =>
|
||||||
|
sessionStorage.setItem(StorageKey[key], value.toString());
|
||||||
@@ -1,73 +1,7 @@
|
|||||||
import * as StringUtility from "./StringUtility";
|
import * as LocalStorageUtility from "./LocalStorageUtility";
|
||||||
|
import * as SessionStorageUtility from "./SessionStorageUtility";
|
||||||
export class LocalStorageUtility {
|
|
||||||
public static hasItem(key: StorageKey): boolean {
|
|
||||||
return !!localStorage.getItem(StorageKey[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getEntryString(key: StorageKey): string | null {
|
|
||||||
return localStorage.getItem(StorageKey[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getEntryNumber(key: StorageKey): number {
|
|
||||||
return StringUtility.toNumber(localStorage.getItem(StorageKey[key]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getEntryBoolean(key: StorageKey): boolean {
|
|
||||||
return StringUtility.toBoolean(localStorage.getItem(StorageKey[key]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setEntryString(key: StorageKey, value: string): void {
|
|
||||||
localStorage.setItem(StorageKey[key], value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static removeEntry(key: StorageKey): void {
|
|
||||||
return localStorage.removeItem(StorageKey[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setEntryNumber(key: StorageKey, value: number): void {
|
|
||||||
localStorage.setItem(StorageKey[key], value.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setEntryBoolean(key: StorageKey, value: boolean): void {
|
|
||||||
localStorage.setItem(StorageKey[key], value.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionStorageUtility {
|
|
||||||
public static hasItem(key: StorageKey): boolean {
|
|
||||||
return !!sessionStorage.getItem(StorageKey[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getEntryString(key: StorageKey): string | null {
|
|
||||||
return sessionStorage.getItem(StorageKey[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getEntryNumber(key: StorageKey): number {
|
|
||||||
return StringUtility.toNumber(sessionStorage.getItem(StorageKey[key]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getEntry(key: string): string | null {
|
|
||||||
return sessionStorage.getItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static removeEntry(key: StorageKey): void {
|
|
||||||
return sessionStorage.removeItem(StorageKey[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setEntryString(key: StorageKey, value: string): void {
|
|
||||||
sessionStorage.setItem(StorageKey[key], value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setEntry(key: string, value: string): void {
|
|
||||||
sessionStorage.setItem(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setEntryNumber(key: StorageKey, value: number): void {
|
|
||||||
sessionStorage.setItem(StorageKey[key], value.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export { LocalStorageUtility, SessionStorageUtility };
|
||||||
export enum StorageKey {
|
export enum StorageKey {
|
||||||
ActualItemPerPage,
|
ActualItemPerPage,
|
||||||
CustomItemPerPage,
|
CustomItemPerPage,
|
||||||
|
|||||||
13
src/Terminal/TerminalProps.ts
Normal file
13
src/Terminal/TerminalProps.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { AuthType } from "../AuthType";
|
||||||
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
import { ApiType } from "../UserContext";
|
||||||
|
|
||||||
|
export interface TerminalProps {
|
||||||
|
authToken: string;
|
||||||
|
notebookServerEndpoint: string;
|
||||||
|
terminalEndpoint: string;
|
||||||
|
databaseAccount: DataModels.DatabaseAccount;
|
||||||
|
authType: AuthType;
|
||||||
|
apiType: ApiType;
|
||||||
|
subscriptionId: string;
|
||||||
|
}
|
||||||
@@ -1,43 +1,36 @@
|
|||||||
import { ServerConnection } from "@jupyterlab/services";
|
import { ServerConnection } from "@jupyterlab/services";
|
||||||
import "@jupyterlab/terminal/style/index.css";
|
import "@jupyterlab/terminal/style/index.css";
|
||||||
import { HttpHeaders, TerminalQueryParams } from "../Common/Constants";
|
import postRobot from "post-robot";
|
||||||
|
import { HttpHeaders } from "../Common/Constants";
|
||||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
||||||
|
import { TerminalProps } from "./TerminalProps";
|
||||||
|
|
||||||
const getUrlVars = (): { [key: string]: string } => {
|
const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => {
|
||||||
const vars: { [key: string]: string } = {};
|
|
||||||
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (_m, key, value): string => {
|
|
||||||
vars[key] = decodeURIComponent(value);
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
return vars;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
|
|
||||||
let body: BodyInit | undefined;
|
let body: BodyInit | undefined;
|
||||||
let headers: HeadersInit | undefined;
|
let headers: HeadersInit | undefined;
|
||||||
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
|
if (props.terminalEndpoint) {
|
||||||
body = JSON.stringify({
|
body = JSON.stringify({
|
||||||
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint],
|
endpoint: props.terminalEndpoint,
|
||||||
});
|
});
|
||||||
headers = {
|
headers = {
|
||||||
[HttpHeaders.contentType]: "application/json",
|
[HttpHeaders.contentType]: "application/json",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = urlVars[TerminalQueryParams.Server];
|
const server = props.notebookServerEndpoint;
|
||||||
let options: Partial<ServerConnection.ISettings> = {
|
let options: Partial<ServerConnection.ISettings> = {
|
||||||
baseUrl: server,
|
baseUrl: server,
|
||||||
init: { body, headers },
|
init: { body, headers },
|
||||||
fetch: window.parent.fetch,
|
fetch: window.parent.fetch,
|
||||||
};
|
};
|
||||||
if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) {
|
if (props.authToken) {
|
||||||
options = {
|
options = {
|
||||||
baseUrl: server,
|
baseUrl: server,
|
||||||
token: urlVars[TerminalQueryParams.Token],
|
token: props.authToken,
|
||||||
appendToken: true,
|
appendToken: true,
|
||||||
init: { body, headers },
|
init: { body, headers },
|
||||||
fetch: window.parent.fetch,
|
fetch: window.parent.fetch,
|
||||||
@@ -47,30 +40,41 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
|
|||||||
return ServerConnection.makeSettings(options);
|
return ServerConnection.makeSettings(options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async (): Promise<void> => {
|
const initTerminal = async (props: TerminalProps) => {
|
||||||
const urlVars = getUrlVars();
|
// Initialize userContext (only properties which are needed by TelemetryProcessor)
|
||||||
|
|
||||||
// Initialize userContext. Currently only subscriptionId is required by TelemetryProcessor
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
subscriptionId: urlVars[TerminalQueryParams.SubscriptionId],
|
subscriptionId: props.subscriptionId,
|
||||||
|
apiType: props.apiType,
|
||||||
|
authType: props.authType,
|
||||||
|
databaseAccount: props.databaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverSettings = createServerSettings(urlVars);
|
const serverSettings = createServerSettings(props);
|
||||||
|
|
||||||
const data = { baseUrl: serverSettings.baseUrl };
|
const data = { baseUrl: serverSettings.baseUrl };
|
||||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
|
|
||||||
await JupyterLabAppFactory.createTerminalApp(serverSettings);
|
await JupyterLabAppFactory.createTerminalApp(serverSettings);
|
||||||
} else {
|
|
||||||
throw new Error("Only terminal is supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const main = async (): Promise<void> => {
|
||||||
|
postRobot.on(
|
||||||
|
"props",
|
||||||
|
{
|
||||||
|
window: window.parent,
|
||||||
|
domain: window.location.origin,
|
||||||
|
},
|
||||||
|
async (event) => {
|
||||||
|
// Typescript definition for event is wrong. So read props by casting to <any>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const props = (event as any).data as TerminalProps;
|
||||||
|
await initTerminal(props);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener("load", main);
|
window.addEventListener("load", main);
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import * as ViewModels from "../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../UserContext";
|
|
||||||
|
|
||||||
export class PortalTokenProvider implements ViewModels.TokenProvider {
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
public async getAuthHeader(): Promise<Headers> {
|
|
||||||
const bearerToken = userContext.authorizationToken;
|
|
||||||
let fetchHeaders = new Headers();
|
|
||||||
fetchHeaders.append("authorization", bearerToken);
|
|
||||||
return fetchHeaders;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { configContext, Platform } from "../ConfigContext";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
|
||||||
import { PortalTokenProvider } from "./PortalTokenProvider";
|
|
||||||
|
|
||||||
export class TokenProviderFactory {
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
public static create(): ViewModels.TokenProvider {
|
|
||||||
const platformType = configContext.platform;
|
|
||||||
switch (platformType) {
|
|
||||||
case Platform.Portal:
|
|
||||||
case Platform.Hosted:
|
|
||||||
return new PortalTokenProvider();
|
|
||||||
case Platform.Emulator:
|
|
||||||
default:
|
|
||||||
// should never get into this state
|
|
||||||
throw new Error(`Unknown platform ${platformType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export const isCapabilityEnabled = (capabilityName: string): boolean =>
|
export const isCapabilityEnabled = (capabilityName: string): boolean => {
|
||||||
userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName);
|
const { databaseAccount } = userContext;
|
||||||
|
if (databaseAccount && databaseAccount.properties && databaseAccount.properties.capabilities) {
|
||||||
|
return databaseAccount.properties.capabilities.some((capability) => capability.name === capabilityName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless);
|
export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe("PricingUtils Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if passed number is not number", () => {
|
it("should return false if passed number is not number", () => {
|
||||||
const value = PricingUtils.isLargerThanDefaultMinRU(null);
|
const value = PricingUtils.isLargerThanDefaultMinRU(undefined);
|
||||||
expect(value).toBe(false);
|
expect(value).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -28,7 +28,7 @@ describe("PricingUtils Tests", () => {
|
|||||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||||
serverId: "default",
|
serverId: "default",
|
||||||
requestUnits: 1,
|
requestUnits: 1,
|
||||||
numberOfRegions: null,
|
numberOfRegions: undefined,
|
||||||
multimasterEnabled: false,
|
multimasterEnabled: false,
|
||||||
isAutoscale: false,
|
isAutoscale: false,
|
||||||
});
|
});
|
||||||
@@ -38,7 +38,7 @@ describe("PricingUtils Tests", () => {
|
|||||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||||
serverId: "default",
|
serverId: "default",
|
||||||
requestUnits: 1,
|
requestUnits: 1,
|
||||||
numberOfRegions: null,
|
numberOfRegions: undefined,
|
||||||
multimasterEnabled: false,
|
multimasterEnabled: false,
|
||||||
isAutoscale: true,
|
isAutoscale: true,
|
||||||
});
|
});
|
||||||
@@ -264,11 +264,6 @@ describe("PricingUtils Tests", () => {
|
|||||||
|
|
||||||
describe("getRegionMultiplier()", () => {
|
describe("getRegionMultiplier()", () => {
|
||||||
describe("without multimaster", () => {
|
describe("without multimaster", () => {
|
||||||
it("should return 0 for null", () => {
|
|
||||||
const value = PricingUtils.getRegionMultiplier(null, false);
|
|
||||||
expect(value).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 0 for undefined", () => {
|
it("should return 0 for undefined", () => {
|
||||||
const value = PricingUtils.getRegionMultiplier(undefined, false);
|
const value = PricingUtils.getRegionMultiplier(undefined, false);
|
||||||
expect(value).toBe(0);
|
expect(value).toBe(0);
|
||||||
@@ -296,11 +291,6 @@ describe("PricingUtils Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("with multimaster", () => {
|
describe("with multimaster", () => {
|
||||||
it("should return 0 for null", () => {
|
|
||||||
const value = PricingUtils.getRegionMultiplier(null, true);
|
|
||||||
expect(value).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 0 for undefined", () => {
|
it("should return 0 for undefined", () => {
|
||||||
const value = PricingUtils.getRegionMultiplier(undefined, true);
|
const value = PricingUtils.getRegionMultiplier(undefined, true);
|
||||||
expect(value).toBe(0);
|
expect(value).toBe(0);
|
||||||
@@ -450,11 +440,6 @@ describe("PricingUtils Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("normalizeNumberOfRegions()", () => {
|
describe("normalizeNumberOfRegions()", () => {
|
||||||
it("should return 0 for null", () => {
|
|
||||||
const value = PricingUtils.normalizeNumber(null);
|
|
||||||
expect(value).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 0 for undefined", () => {
|
it("should return 0 for undefined", () => {
|
||||||
const value = PricingUtils.normalizeNumber(undefined);
|
const value = PricingUtils.normalizeNumber(undefined);
|
||||||
expect(value).toBe(0);
|
expect(value).toBe(0);
|
||||||
|
|||||||
@@ -5,23 +5,19 @@ import * as ViewModels from "../Contracts/ViewModels";
|
|||||||
import * as QueryUtils from "./QueryUtils";
|
import * as QueryUtils from "./QueryUtils";
|
||||||
|
|
||||||
describe("Query Utils", () => {
|
describe("Query Utils", () => {
|
||||||
function generatePartitionKeyForPath(path: string): DataModels.PartitionKey {
|
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
||||||
return {
|
return {
|
||||||
paths: [path],
|
paths: [path],
|
||||||
kind: "Hash",
|
kind: "Hash",
|
||||||
version: 2,
|
version: 2,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
describe("buildDocumentsQueryPartitionProjections()", () => {
|
describe("buildDocumentsQueryPartitionProjections()", () => {
|
||||||
it("should return empty string if partition key is undefined", () => {
|
it("should return empty string if partition key is undefined", () => {
|
||||||
expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", undefined)).toBe("");
|
expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", undefined)).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty string if partition key is null", () => {
|
|
||||||
expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", null)).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should replace slashes and embed projection in square braces", () => {
|
it("should replace slashes and embed projection in square braces", () => {
|
||||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a");
|
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a");
|
||||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||||
|
|||||||
@@ -3,27 +3,27 @@ import * as StringUtils from "./StringUtils";
|
|||||||
describe("StringUtils", () => {
|
describe("StringUtils", () => {
|
||||||
describe("stripSpacesFromString()", () => {
|
describe("stripSpacesFromString()", () => {
|
||||||
it("should strip all spaces from input string", () => {
|
it("should strip all spaces from input string", () => {
|
||||||
const transformedString: string = StringUtils.stripSpacesFromString("a b c");
|
const transformedString: string | undefined = StringUtils.stripSpacesFromString("a b c");
|
||||||
expect(transformedString).toBe("abc");
|
expect(transformedString).toBe("abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return original string if input string has no spaces", () => {
|
it("should return original string if input string has no spaces", () => {
|
||||||
const transformedString: string = StringUtils.stripSpacesFromString("abc");
|
const transformedString: string | undefined = StringUtils.stripSpacesFromString("abc");
|
||||||
expect(transformedString).toBe("abc");
|
expect(transformedString).toBe("abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return undefined if input is undefined", () => {
|
it("should return undefined if input is undefined", () => {
|
||||||
const transformedString: string = StringUtils.stripSpacesFromString(undefined);
|
const transformedString: string | undefined = StringUtils.stripSpacesFromString(undefined);
|
||||||
expect(transformedString).toBeUndefined();
|
expect(transformedString).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return undefined if input is undefiend", () => {
|
it("should return undefined if input is undefiend", () => {
|
||||||
const transformedString: string = StringUtils.stripSpacesFromString(undefined);
|
const transformedString: string | undefined = StringUtils.stripSpacesFromString(undefined);
|
||||||
expect(transformedString).toBe(undefined);
|
expect(transformedString).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty string if input is an empty string", () => {
|
it("should return empty string if input is an empty string", () => {
|
||||||
const transformedString: string = StringUtils.stripSpacesFromString("");
|
const transformedString: string | undefined = StringUtils.stripSpacesFromString("");
|
||||||
expect(transformedString).toBe("");
|
expect(transformedString).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export function stripSpacesFromString(inputString: string): string {
|
export function stripSpacesFromString(inputString?: string): string | undefined {
|
||||||
if (inputString === undefined || typeof inputString !== "string") {
|
if (inputString === undefined || typeof inputString !== "string") {
|
||||||
return inputString;
|
return inputString;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
|
|||||||
|
|
||||||
let accounts: Array<DatabaseAccount> = [];
|
let accounts: Array<DatabaseAccount> = [];
|
||||||
|
|
||||||
let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview`;
|
let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2021-06-15`;
|
||||||
|
|
||||||
while (nextLink) {
|
while (nextLink) {
|
||||||
const response: Response = await fetch(nextLink, { headers });
|
const response: Response = await fetch(nextLink, { headers });
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user