Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migration/move-document-tab-to-react

This commit is contained in:
sunilyadav840
2021-07-23 20:37:19 +05:30
116 changed files with 5788 additions and 7142 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,3 @@
{ {
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com", "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
"enableSchemaAnalyzer": true
} }

View File

@@ -2,6 +2,7 @@
.dataResourceTree { .dataResourceTree {
margin-left: @MediumSpace; margin-left: @MediumSpace;
overflow: auto;
.databaseHeader { .databaseHeader {
font-size: 14px; font-size: 14px;

View File

@@ -1,273 +1,270 @@
@import "./Common/Constants"; @import "./Common/Constants";
.resourceTree { .resourceTree {
height: 100%;
flex: 0 0 auto;
.main {
height: 100%; height: 100%;
width: 20%; }
flex: 0 0 auto;
.main {
height: 100%;
}
} }
.resourceTreeScroll { .resourceTreeScroll {
height: 100%; height: 100%;
display: flex; display: flex;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding-right: 10px; padding-right: 10px;
} }
.userSelectNone { .userSelectNone {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
} }
.treeHovermargin { .treeHovermargin {
margin-left: 16px; margin-left: 16px;
} }
.highlight { .highlight {
padding: @SmallSpace 2px; padding: @SmallSpace 2px;
outline: 0; outline: 0;
&:hover { &:hover {
.hover(); .hover();
} }
&:active { &:active {
.active(); .active();
} }
&:focus { &:focus {
.focus(); .focus();
} }
} }
.contextmenushowing { .contextmenushowing {
background-color: #EEE; background-color: #eee;
} }
.collectionstree { .collectionstree {
width: 100%; width: 100%;
margin-top: @DefaultSpace; margin-top: @DefaultSpace;
.databaseList {
list-style-type: none;
padding-left: 0px;
.databaseList { .collectionList {
list-style-type: none; padding-left: (2 * @MediumSpace);
padding-left: 0px;
.collectionList {
padding-left:(2 * @MediumSpace);
}
.collectionChildList {
padding-left: @LargeSpace;
}
.databaseDocuments {
padding-left: (5 * @MediumSpace);
}
} }
.collectionChildList {
padding-left: @LargeSpace;
}
.databaseDocuments {
padding-left: (5 * @MediumSpace);
}
}
} }
.pointerCursor { .pointerCursor {
cursor: pointer; cursor: pointer;
} }
.menuEllipsis { .menuEllipsis {
padding-right: 6px; padding-right: 6px;
font-weight: bold; font-weight: bold;
font-size: 18px; font-size: 18px;
position: relative; position: relative;
top: -5px; top: -5px;
left: 0px; left: 0px;
float: right; float: right;
display: none; display: none;
padding-left: 6px!important; padding-left: 6px !important;
line-height: @TreeLineHeight; line-height: @TreeLineHeight;
} }
.databaseMenu { .databaseMenu {
.flex-display(); .flex-display();
} }
.databaseMenu:hover .menuEllipsis, .databaseMenu:hover .menuEllipsis,
.databaseMenu:focus .menuEllipsis { .databaseMenu:focus .menuEllipsis {
display: block; display: block;
} }
.databaseCollChildTextOverflow { .databaseCollChildTextOverflow {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
flex: 1; flex: 1;
} }
.collectionMenu { .collectionMenu {
.flex-display(); .flex-display();
} }
.collectionMenu:hover .menuEllipsis, .collectionMenu:hover .menuEllipsis,
.collectionMenu:focus .menuEllipsis { .collectionMenu:focus .menuEllipsis {
display: block; display: block;
} }
.documentsMenu:hover .menuEllipsis, .documentsMenu:hover .menuEllipsis,
.documentsMenu:focus .menuEllipsis { .documentsMenu:focus .menuEllipsis {
display: block; display: block;
} }
.treeChildMenu { .treeChildMenu {
display: flex; display: flex;
} }
.storedProcedureMenu:hover .menuEllipsis, .storedProcedureMenu:hover .menuEllipsis,
.storedProcedureMenu:focus .menuEllipsis { .storedProcedureMenu:focus .menuEllipsis {
display: block; display: block;
} }
.childMenu { .childMenu {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
padding-left: (6 * @MediumSpace); padding-left: (6 * @MediumSpace);
width: 100%; width: 100%;
} }
.storedChildMenu:hover .menuEllipsis, .storedChildMenu:hover .menuEllipsis,
.storedChildMenu:focus .menuEllipsis { .storedChildMenu:focus .menuEllipsis {
display: block; display: block;
} }
.contextmenu6 { .contextmenu6 {
top: -29px; top: -29px;
} }
.userDefinedMenu:hover .contextmenu6 { .userDefinedMenu:hover .contextmenu6 {
display: block; display: block;
} }
.userDefinedchildMenu:hover .menuEllipsis, .userDefinedchildMenu:hover .menuEllipsis,
.userDefinedchildMenu:focus .menuEllipsis { .userDefinedchildMenu:focus .menuEllipsis {
display: block; display: block;
} }
.triggersMenu:hover .menuEllipsis, .triggersMenu:hover .menuEllipsis,
.triggersMenu:focus .menuEllipsis { .triggersMenu:focus .menuEllipsis {
display: block; display: block;
} }
.triggersChildMenu:hover .menuEllipsis, .triggersChildMenu:hover .menuEllipsis,
.triggersChildMenu:focus .menuEllipsis { .triggersChildMenu:focus .menuEllipsis {
display: block; display: block;
} }
.databaseId { .databaseId {
font-size: 14px; font-size: 14px;
} }
.storedUdfTriggerMenu { .storedUdfTriggerMenu {
padding-left: 0px; padding-left: 0px;
} }
.collectionstree img { .collectionstree img {
width: 16px; width: 16px;
height: 16px; height: 16px;
vertical-align: text-top; vertical-align: text-top;
} }
img.collectionsTreeCollapseExpand { img.collectionsTreeCollapseExpand {
width: 10px; width: 10px;
height: 10px; height: 10px;
vertical-align: middle; vertical-align: middle;
margin-bottom: 5px; margin-bottom: 5px;
} }
.collapsed::before { .collapsed::before {
content: "\23F5"; content: "\23F5";
margin-left: 0px; margin-left: 0px;
font-size: 15px; font-size: 15px;
} }
.expanded::before { .expanded::before {
content: '\23F7'; content: "\23F7";
margin-left: 0px; margin-left: 0px;
font-size: 15px; font-size: 15px;
} }
.collectionMenuChildren { .collectionMenuChildren {
padding-left: 42px; padding-left: 42px;
} }
.main-nav { .main-nav {
width: 100vh; width: 100vh;
height: 40px; height: 40px;
background: white; background: white;
transform-origin: left top; transform-origin: left top;
-webkit-transform-origin: left top; -webkit-transform-origin: left top;
-ms-transform-origin: left top; -ms-transform-origin: left top;
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 {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin: -32px 0 0 0; margin: -32px 0 0 0;
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%);
} }
.main-nav-img.main-nav-sub-img { .main-nav-img.main-nav-sub-img {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin: 0px 0px 0 0; margin: 0px 0px 0 0;
transform: rotate(180deg) translateX(0%); transform: rotate(180deg) translateX(0%);
-webkit-transform: rotate(180deg) translateX(0%); -webkit-transform: rotate(180deg) translateX(0%);
-ms-transform: rotate(180deg) translateX(0%); -ms-transform: rotate(180deg) translateX(0%);
position: absolute; position: absolute;
right: -8px; right: -8px;
top: 16px; top: 16px;
} }
ul.nav { ul.nav {
margin: 0 auto; margin: 0 auto;
margin-top: 0px; margin-top: 0px;
margin-left: 0px; margin-left: 0px;
} }
.mini ul.nav li { .mini ul.nav li {
float: right; float: right;
line-height: 25px; line-height: 25px;
height: auto; height: auto;
margin-top: 3px; margin-top: 3px;
} }
.spancolchildstyle { .spancolchildstyle {
padding: 4px; padding: 4px;
} }
.contextmenubutton { .contextmenubutton {
float: right; float: right;
display: none; display: none;
} }
.highlight:hover>.contextmenubutton { .highlight:hover > .contextmenubutton {
display: unset; display: unset;
} }
.highlight:hover>.contextmenubutton::after { .highlight:hover > .contextmenubutton::after {
content: "\2026"; content: "\2026";
font-size: 12px; font-size: 12px;
} }
.showEllipsis { .showEllipsis {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }

29
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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()", () => {

View File

@@ -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 */}

View File

@@ -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 () => {

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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",
}); });

View File

@@ -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 });
}; };

View File

@@ -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 {

View File

@@ -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());
}); });

View File

@@ -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}>

View File

@@ -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: { documentEndpoint: "https://testDocumentEndpoint.azure.com/",
cassandraEndpoint: null, },
documentEndpoint: "https://testDocumentEndpoint.azure.com/", type: "type",
gremlinEndpoint: null,
tableEndpoint: null,
},
type: "testType",
};
}; };
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", properties: {
kind: "testKind", mongoEndpoint: "https://testMongoEndpoint.azure.com/",
location: "testLocation", },
name: "testName",
properties: {
cassandraEndpoint: null,
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
gremlinEndpoint: null,
tableEndpoint: null,
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
},
type: "testType",
};
}; };
const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => { const testCassandraAccount: DataModels.DatabaseAccount = {
return { ...testAccount,
id: "testId", properties: {
kind: "testKind", cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
location: "testLocation", },
name: "testName",
properties: {
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: { notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
authToken: "testAuthToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
},
databaseAccount: createTestMongo32DatabaseAccount(),
});
}; };
const createMongo36Terminal = (): NotebookTerminalComponent => { const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
return new NotebookTerminalComponent({ authToken: "authToken",
notebookServerInfo: { notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
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",
},
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],
])
);
}); });
}); });

View File

@@ -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;
} }
public tryGetTerminalEndpoint(): string | null { sendPropsToTerminalFrame(): void {
let terminalEndpoint: string | null; if (!this.terminalWindow) {
return;
}
const notebookServerEndpoint: string = this.props.notebookServerInfo.notebookServerEndpoint; const props: TerminalProps = {
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,
};
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;
} }
} }

View File

@@ -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>
`;

View File

@@ -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],

View File

@@ -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()

View File

@@ -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();

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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;

View File

@@ -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

View File

@@ -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();

View File

@@ -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,

View File

@@ -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";

View File

@@ -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>

View File

@@ -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,
}; };
}; };

View File

@@ -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,18 +66,20 @@ 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);
if (!path || path !== item.path) { useNotebook.getState().deleteNotebookItem(item);
throw new Error("No path provided");
}
if (item.parent && item.parent.children) { // TODO: Delete once old resource tree is removed
// Remove deleted child if (!path || path !== item.path) {
const newChildren = item.parent.children.filter((child) => child.path !== path); throw new Error("No path provided");
item.parent.children = newChildren; }
}
}); if (item.parent && item.parent.children) {
// Remove deleted child
const newChildren = item.parent.children.filter((child) => child.path !== path);
item.parent.children = newChildren;
}
} }
/** /**

View File

@@ -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
); );

View File

@@ -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"

View File

@@ -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);
}); });

View File

@@ -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) || " ";

View File

@@ -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)),
}); });

View File

@@ -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 })),
}); });

View File

@@ -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 });
}
}
},
})); }));

View File

@@ -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">

View File

@@ -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,46 +376,28 @@ 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.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
branchesProps: {
...this.state.gitHubReposState.reposListProps.branchesProps,
[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]: this.branchesProps[item.key],
},
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
this.loadMoreBranches(item.repo); 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.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
branchesProps: {
...this.branchesProps,
},
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
this.isAddedRepo = false; this.isAddedRepo = false;
} }

View File

@@ -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],

View File

@@ -150,9 +150,6 @@
.backImageIcon { .backImageIcon {
margin-top: 8px; margin-top: 8px;
} }
.entityValueTextField {
margin: 24px;
}
.addEntityDatePicker { .addEntityDatePicker {
max-width: 145px; max-width: 145px;
} }

View File

@@ -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}
/> />
); );
}; };

View File

@@ -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 "

View File

@@ -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) {
event.preventDefault(); setFormError(`Property name cannot be empty. Please enter a property name`);
return;
}
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);
await tableEntityListViewModel.addEntityToCache(newEntity); try {
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { await tableEntityListViewModel.addEntityToCache(newEntity);
tableEntityListViewModel.redrawTableThrottled(); if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
handleError(errorMessage, "AddTableRow");
throw error;
} finally {
setIsExecuting(false);
} }
closeSidePanel();
}; };
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -200,110 +216,80 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsEntityValuePanelTrue(); setIsEntityValuePanelTrue();
}; };
const renderPanelContent = (): JSX.Element => {
return (
<form className="panelFormWrapper">
<div className="panelFormWrapper">
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
<div className="paneFooter">
<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) { if (isEntityValuePanelOpen) {
return ( return (
<PanelContainerComponent <Stack style={{ padding: "20px 34px" }}>
headerText="" <Stack horizontal {...columnProps}>
onRenderNavigationContent={onRenderNavigationContent} <Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
panelWidth="700px" <Label>{entityAttributeProperty}</Label>
isOpen={true} </Stack>
panelContent={ <TextField
<TextField multiline
multiline rows={5}
rows={5} value={entityAttributeValue}
className="entityValueTextField" onChange={(event, newInput?: string) => {
value={entityAttributeValue} entityChange(newInput, selectedRow, "value");
onChange={(event, newInput?: string) => { setEntityAttributeValue(newInput);
entityChange(newInput, selectedRow, "value"); }}
setEntityAttributeValue(newInput); />
}} </Stack>
/>
}
isConsoleExpanded={false}
/>
); );
} }
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: getButtonLabel(userContext.apiType),
onSubmit,
};
return ( return (
<PanelContainerComponent <RightPaneForm {...props}>
headerText={getPanelTitle(userContext.apiType)} <div className="panelMainContent">
panelWidth="700px" {entities.map((entity, index) => {
isOpen={true} return (
panelContent={renderPanelContent()} <TableEntity
isConsoleExpanded={false} key={"" + entity.id + index}
/> isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
</RightPaneForm>
); );
}; };

View File

@@ -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,26 +190,44 @@ 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;
}
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
} }
event.preventDefault();
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;
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
queryTablesTab.collection, try {
originalDocumentData, const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
entity queryTablesTab.collection,
); originalDocumentData,
await tableEntityListViewModel.updateCachedEntity(newEntity); entity
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { );
tableEntityListViewModel.redrawTableThrottled(); await tableEntityListViewModel.updateCachedEntity(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
} catch (error) {
const errorMessage = getErrorMessage(error);
handleError(errorMessage, "EditTableRow");
throw error;
} finally {
setIsExecuting(false);
} }
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
closeSidePanel();
}; };
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -299,109 +317,81 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
setIsEntityValuePanelTrue(); setIsEntityValuePanelTrue();
}; };
const renderPanelContent = (): JSX.Element => {
return (
<form className="panelFormWrapper">
<div className="panelFormWrapper">
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value?.toString()}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
{renderPanelFooter()}
</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) { if (isEntityValuePanelOpen) {
return ( return (
<PanelContainerComponent <Stack style={{ padding: "20px 34px" }}>
headerText="" <Stack horizontal {...columnProps}>
onRenderNavigationContent={onRenderNavigationContent} <Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
panelWidth="700px" <Label>{entityAttributeProperty}</Label>
isOpen={true} </Stack>
panelContent={ <TextField
<TextField multiline
multiline rows={5}
rows={5} value={entityAttributeValue}
className="entityValueTextField" onChange={(event, newInput?: string) => {
value={entityAttributeValue} setEntityAttributeValue(newInput);
onChange={(event, newInput?: string) => { entityChange(newInput, selectedRow, "value");
setEntityAttributeValue(newInput); }}
entityChange(newInput, selectedRow, "value"); />
}} </Stack>
/>
}
isConsoleExpanded={false}
/>
); );
} }
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: "Update",
onSubmit,
};
return ( return (
<PanelContainerComponent <RightPaneForm {...props}>
headerText="Edit Table Entity" <div className="panelMainContent">
panelWidth="700px" {entities.map((entity, index) => {
isOpen={true} return (
panelContent={renderPanelContent()} <TableEntity
isConsoleExpanded={false} key={"" + entity.id + index}
/> isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value?.toString()}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
</RightPaneForm>
); );
}; };

View File

@@ -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";

View File

@@ -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),

View File

@@ -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;

View File

@@ -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> {

View File

@@ -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"
); );
}; };

View File

@@ -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();
}); });
}); });
}); });

View File

@@ -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) {

View File

@@ -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> {

View File

@@ -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())); };
}
}

View 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} />;
};

View File

@@ -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",

View File

@@ -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();
});
});

View File

@@ -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) => {} () => {
/**/
}
); );
} }
} }

View File

@@ -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>
`;

View File

@@ -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);
},
})); }));

View File

@@ -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"),

View File

@@ -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);
}); });

View File

@@ -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));

View File

@@ -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
View 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"));

View File

@@ -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 ",

View File

@@ -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

View File

@@ -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}

View File

@@ -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"}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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"),
}; };
} }

View File

@@ -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:
"armRegionNameeq '" +
region +
"'andserviceFamilyeq '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;
}
};

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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);
} }

View File

@@ -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(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism);
LocalStorageUtility.setEntryNumber( };
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)
); );
} };
}

View 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());

View File

@@ -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);
} }

View 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());

View File

@@ -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,

View 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;
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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}`);
}
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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("");
}); });
}); });

View File

@@ -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;
} }

View File

@@ -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