Compare commits

...

42 Commits

Author SHA1 Message Date
hardiknai-techm
e20931fbfe merge master 2021-07-23 13:36:42 +05:30
hardiknai-techm
1f12940d48 migrate/QueryTablesTab UI fix 2021-07-23 13:34:31 +05:30
victor-meng
fecac5625a Migrate resource tree for resource token to react (#956) 2021-07-22 17:11:19 -07:00
victor-meng
dc21032d69 Clean up EditTableEntityPanel (#955) 2021-07-22 16:18:48 -07:00
siddjoshi-ms
39a67dbc98 Sqlx estimated cost calculation (#925)
Adds cost estimate for the SqlX services in the Dedicated Gateway blade (queries the new FetchPrices API to retrieve price data)
2021-07-22 10:48:19 -07:00
Sunil Kumar Yadav
ed9cf01b50 Fixed resourse tree collapse issue (#953) 2021-07-22 11:12:35 -05:00
vaidankarswapnil
e443d17b2e Migrate Index page to React (#952) 2021-07-22 10:19:17 -05:00
victor-meng
401660ae15 Fix connect to github (#951) 2021-07-21 17:47:55 -07:00
Steve Faulkner
c5e4ee9c2b Upgrade Playwright (#939) 2021-07-21 19:32:53 -05:00
victor-meng
913fec4e69 Improve e2e stability (#949) 2021-07-21 16:22:31 -07:00
hardiknai-techm
44315867ce Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate/QueryTablesTab 2021-07-21 06:27:33 +05:30
victor-meng
6d46e48490 Migrate resource tree to react (#941) 2021-07-20 11:40:04 -07:00
victor-meng
afacde4041 Fix AddTableEntityPanel (#945)
* Fix AddTableEntityPanel

* Add CSS

* Fix snapshot
2021-07-19 22:00:33 -07:00
hardiknai-techm
1f979a5fb7 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate/QueryTablesTab 2021-07-17 11:57:29 +05:30
vaidankarswapnil
8a3929775b Strict mode SwitchAccount and SwitchSubscription files (#940) 2021-07-15 10:54:07 -05:00
vaidankarswapnil
b115bb34ca Fix GitHub repos panel issues (#875)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
Co-authored-by: Tanuj Mittal <tamitta@microsoft.com>
2021-07-15 09:44:25 -05:00
Sunil Kumar Yadav
397231dca2 Fixed typescript strict issue of Statusbar, hostedUtils, StringUtils etc (#785) 2021-07-15 09:35:51 -05:00
Hardikkumar Nai
0bbf9de963 Resolve ESLint Errors (#934) 2021-07-15 08:53:07 -05:00
Sunil Kumar Yadav
103b3bf6c9 fixed typescript strict of CqlUtilities.test.ts CapabilityUtils.ts file (#787)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-07-15 00:51:36 -05:00
Sunil Kumar Yadav
7dd8bd567f fixed typescript strict useSidePanel.ts PanelContainerComponent.tsx etc (#802)
* fixed typescript strict useSidePanel.ts PanelContainerComponent.tsx SchemaAnalyzerSplashScreen.tsx files

* update snapshot
2021-07-14 21:42:46 -05:00
Tanuj Mittal
416358f540 Cleanup Schema Analyzer feature flag (#938) 2021-07-15 06:42:14 +05:30
Tanuj Mittal
62b483d740 Fix connect to GitHub (#937) 2021-07-14 17:10:19 -05:00
Tanuj Mittal
c665c4bb7a Fix notebooks not rendering (#936) 2021-07-14 16:55:03 -05:00
t-tarabhatia
887618e77e UX Changes for Default Partition Key A/B Experiment (#928) 2021-07-14 14:10:45 -05:00
victor-meng
8d6ccf8356 import ComponentRegisterer in Explorer (#935) 2021-07-13 19:59:39 -05:00
Zachary Foster
4614ab3427 Adds disableLocalAuth check for listKeys call (#931) 2021-07-13 08:20:13 -04:00
Hardikkumar Nai
71113e403e Resolve ESlint Errors (#932) 2021-07-12 22:38:16 -05:00
Tanuj Mittal
854bd2c149 Use window messaging to pass sensitive data to terminal iframe (#929)
* Use window messaging to pass sensitive data to terminal iframe

* Address feedback

* Format

* Update

* Add tests
2021-07-13 03:18:13 +05:30
Hardikkumar Nai
cfce78242c Remove Explorer.openBrowseQueriesPane (#889) 2021-07-12 09:40:43 -05:00
Hardikkumar Nai
ee3488d3a9 Resolve more ESLint (#920)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-07-12 08:55:19 -05:00
Hardikkumar Nai
e8d320e505 Resolve more ESLint (#922)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-07-12 00:12:38 -05:00
hardiknai-techm
474ec70923 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate/QueryTablesTab 2021-07-09 10:22:46 +05:30
victor-meng
f8ab0a82e0 Move tabs manager to zustand (#915) 2021-07-08 21:32:22 -07:00
Hardikkumar Nai
f4eef1b61b Resolve Eslint for Tables.Constants (#914)
* resolve_eslint_Tables.Constants

* correct spelling mistake
2021-07-08 21:10:29 -05:00
hardiknai-techm
1277940fe9 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate/QueryTablesTab 2021-07-08 10:02:44 +05:30
victor-meng
c486c1193e Fix scale component not showing in settings tab (#926) 2021-07-07 11:22:54 -07:00
victor-meng
db34024259 Move notebook flags to zustand (#912) 2021-07-06 15:21:23 -05:00
victor-meng
98d7bb37d5 Move resource token collection to useDatabases zustand store (#916) 2021-07-06 14:05:38 -05:00
Hardikkumar Nai
45d0b3f706 Resolve ESLint QueriesGridComponent (#923) 2021-07-06 09:12:41 -05:00
hardiknai-techm
2b8327d222 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate/QueryTablesTab 2021-07-02 17:06:58 +05:30
Hardikkumar Nai
a1d5648bbc Remove Explorer.openAddCollectionPane (#905)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-27 23:39:28 -05:00
hardiknai-techm
1b74f619ab Migrate QueryTablesTab to React 2021-06-04 09:00:31 +05:30
163 changed files with 7515 additions and 8365 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
@@ -105,17 +99,10 @@ src/Explorer/Notebook/NotebookContentClient.ts
src/Explorer/Notebook/NotebookContentItem.ts src/Explorer/Notebook/NotebookContentItem.ts
src/Explorer/Notebook/NotebookUtil.ts src/Explorer/Notebook/NotebookUtil.ts
src/Explorer/OpenActionsStubs.ts src/Explorer/OpenActionsStubs.ts
src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
src/Explorer/SplashScreen/SplashScreen.test.ts src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts src/Explorer/Tables/DataTable/CacheBase.ts
src/Explorer/Tables/DataTable/DataTableBindingManager.ts src/Explorer/Tables/DataTable/DataTableBindingManager.ts
src/Explorer/Tables/DataTable/DataTableBuilder.ts src/Explorer/Tables/DataTable/DataTableBuilder.ts
@@ -141,116 +128,47 @@ src/Explorer/Tabs/DocumentsTab.test.ts
src/Explorer/Tabs/DocumentsTab.ts src/Explorer/Tabs/DocumentsTab.ts
src/Explorer/Tabs/GraphTab.ts src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts src/Explorer/Tabs/MongoDocumentsTab.ts
# src/Explorer/Tabs/MongoQueryTab.ts
# src/Explorer/Tabs/MongoShellTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/ScriptTabBase.ts
# src/Explorer/Tabs/StoredProcedureTab.ts
src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabComponents.ts
src/Explorer/Tabs/TabsBase.ts 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/Database.ts
src/Explorer/Tree/DocumentId.ts src/Explorer/Tree/DocumentId.ts
src/Explorer/Tree/ObjectId.ts src/Explorer/Tree/ObjectId.ts
src/Explorer/Tree/ResourceTokenCollection.ts src/Explorer/Tree/ResourceTokenCollection.ts
src/Explorer/Tree/StoredProcedure.ts src/Explorer/Tree/StoredProcedure.ts
src/Explorer/Tree/TreeComponents.ts src/Explorer/Tree/TreeComponents.ts
src/Explorer/Tree/Trigger.ts src/Explorer/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/HostedExplorer.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/Main.ts
src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts
src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts
src/Platform/Emulator/DataAccessUtility.ts
src/Platform/Emulator/ExplorerFactory.ts
src/Platform/Emulator/Main.ts
src/Platform/Emulator/NotificationsClient.ts
src/Platform/Hosted/ArmResourceUtils.ts
src/Platform/Hosted/Authorization.ts src/Platform/Hosted/Authorization.ts
src/Platform/Hosted/DataAccessUtility.ts
src/Platform/Hosted/ExplorerFactory.ts
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
src/Platform/Hosted/Main.ts
src/Platform/Hosted/Maint.test.ts
src/Platform/Hosted/NotificationsClient.ts
src/Platform/Portal/DataAccessUtility.ts
src/Platform/Portal/ExplorerFactory.ts
src/Platform/Portal/Main.ts
src/Platform/Portal/NotificationsClient.ts
src/PlatformType.ts
src/ReactDevTools.ts src/ReactDevTools.ts
src/ResourceProvider/IResourceProviderClient.test.ts
src/ResourceProvider/IResourceProviderClient.ts
src/ResourceProvider/ResourceProviderClient.ts
src/ResourceProvider/ResourceProviderClientFactory.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/QueriesGridReactComponent/QueriesGridComponent.tsx
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx
src/Explorer/Controls/ResizeSensorReactComponent/ResizeSensorComponent.tsx
src/Explorer/Controls/Spark/ClusterSettingsComponent.tsx
src/Explorer/Controls/Spark/ClusterSettingsComponentAdapter.tsx
src/Explorer/Controls/Tabs/TabComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx src/Explorer/Controls/TreeComponent/TreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.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
@@ -258,46 +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
src/GalleryViewer/Cards/GalleryCardComponent.tsx
src/GalleryViewer/GalleryViewer.tsx
src/GalleryViewer/GalleryViewerComponent.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

@@ -16,6 +16,7 @@ import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent"; import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
@@ -49,7 +50,10 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
onClick: () => onClick: () =>
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel("Delete " + getDatabaseName(), <DeleteDatabaseConfirmationPanel explorer={container} />), .openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getDatabaseName()}`, label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem", styleClass: "deleteDatabaseMenuItem",
}); });
@@ -81,13 +85,13 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (container.isShellEnabled()) { if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
}, },
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell", label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
}); });
} }
@@ -105,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",
}); });
@@ -125,7 +129,10 @@ export const createCollectionContextMenuButton = (
onClick: () => onClick: () =>
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel("Delete " + getCollectionName(), <DeleteCollectionConfirmationPane explorer={container} />), .openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getCollectionName()}`, label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem", styleClass: "deleteCollectionMenuItem",
}); });

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

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

@@ -30,7 +30,7 @@ import * as DataModels from "../../../Contracts/DataModels";
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";
const title: string = "Open Saved Queries"; const title = "Open Saved Queries";
export interface QueriesGridComponentProps { export interface QueriesGridComponentProps {
queriesClient: QueriesClient; queriesClient: QueriesClient;
@@ -196,9 +196,9 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
{ {
key: "Action", key: "Action",
name: "Action", name: "Action",
fieldName: null, fieldName: undefined,
minWidth: 70, minWidth: 70,
onRender: (query: Query, index: number, column: IColumn) => { onRender: (query: Query) => {
const buttonProps: IButtonProps = { const buttonProps: IButtonProps = {
iconProps: { iconProps: {
iconName: "More", iconName: "More",
@@ -214,19 +214,15 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
{ {
key: "Open", key: "Open",
text: "Open query", text: "Open query",
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, menuItem: any) => { onClick: () => {
this.props.onQuerySelect(query); this.props.onQuerySelect(query);
}, },
}, },
{ {
key: "Delete", key: "Delete",
text: "Delete query", text: "Delete query",
onClick: async ( onClick: async () => {
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
menuItem: any
) => {
if (window.confirm("Are you sure you want to delete this query?")) { if (window.confirm("Are you sure you want to delete this query?")) {
const container = window.dataExplorer;
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title, paneTitle: title,

View File

@@ -16,7 +16,6 @@ import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/T
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
@@ -110,7 +109,6 @@ export interface SettingsComponentState {
initialNotification: DataModels.Notification; initialNotification: DataModels.Notification;
selectedTab: SettingsV2TabTypes; selectedTab: SettingsV2TabTypes;
offerLoaded: boolean;
} }
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> { export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
@@ -195,7 +193,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
initialNotification: undefined, initialNotification: undefined,
selectedTab: SettingsV2TabTypes.ScaleTab, selectedTab: SettingsV2TabTypes.ScaleTab,
offerLoaded: !!this.offer,
}; };
this.saveSettingsButton = { this.saveSettingsButton = {
@@ -217,7 +214,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.isCollectionSettingsTab) { if (this.isCollectionSettingsTab) {
this.refreshIndexTransformationProgress(); this.refreshIndexTransformationProgress();
this.loadMongoIndexes(); this.loadMongoIndexes();
this.loadCollectionOffer();
} }
this.setAutoPilotStates(); this.setAutoPilotStates();
@@ -372,34 +368,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}); });
}; };
private async loadCollectionOffer() {
try {
this.props.settingsTab.isExecuting(true);
await this.collection.loadOffer();
this.props.settingsTab.tabTitle(this.collection.offer() ? "Settings" : "Scale & Settings");
this.setState({ offerLoaded: true });
} catch (error) {
this.props.settingsTab.isExecutionError(true);
const errorMessage = getErrorMessage(error);
traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error),
},
this.props.settingsTab.onLoadStartKey
);
logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`);
} finally {
this.props.settingsTab.isExecuting(false);
}
}
private getMongoIndexesToSave = (): MongoIndex[] => { private getMongoIndexesToSave = (): MongoIndex[] => {
let finalIndexes: MongoIndex[] = []; let finalIndexes: MongoIndex[] = [];
this.state.currentMongoIndexes?.map((mongoIndex: MongoIndex, index: number) => { this.state.currentMongoIndexes?.map((mongoIndex: MongoIndex, index: number) => {
@@ -937,10 +905,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
); );
} }
if (!this.state.offerLoaded) {
return <></>;
}
const subSettingsComponentProps: SubSettingsComponentProps = { const subSettingsComponentProps: SubSettingsComponentProps = {
collection: this.collection, collection: this.collection,
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled, isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,

View File

@@ -30,17 +30,8 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
@@ -48,21 +39,11 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
@@ -116,17 +97,8 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
@@ -134,21 +106,11 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],

View File

@@ -58,7 +58,7 @@ export class TabComponent extends React.Component<TabComponentProps> {
as="span" as="span"
className={className} className={className}
role="presentation" role="presentation"
onActivated={(e) => this.setActiveTab(index)} onActivated={() => this.setActiveTab(index)}
aria-label={`Select tab: ${tab.title}`} aria-label={`Select tab: ${tab.title}`}
> >
{tab.title} {tab.title}

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,12 @@
*/ */
import * as React from "react"; import * as React from "react";
import { NeighborVertexBasicInfo, EditedEdges, GraphNewEdgeData, PossibleVertex } from "./GraphExplorer";
import * as GraphUtil from "./GraphUtil";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import DeleteIcon from "../../../../images/delete.svg";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import DeleteIcon from "../../../../images/delete.svg";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import { EditedEdges, GraphNewEdgeData, NeighborVertexBasicInfo, PossibleVertex } from "./GraphExplorer";
import * as GraphUtil from "./GraphUtil";
export interface EditorNeighborsComponentProps { export interface EditorNeighborsComponentProps {
isSource: boolean; isSource: boolean;
@@ -83,11 +83,11 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
} }
private removeCurrentNeighborEdge(index: number): void { private removeCurrentNeighborEdge(index: number): void {
let sources = this.props.editedNeighbors.currentNeighbors; const sources = this.props.editedNeighbors.currentNeighbors;
let id = sources[index].edgeId; const id = sources[index].edgeId;
sources.splice(index, 1); sources.splice(index, 1);
let droppedIds = this.props.editedNeighbors.droppedIds; const droppedIds = this.props.editedNeighbors.droppedIds;
droppedIds.push(id); droppedIds.push(id);
this.onUpdateEdges(); this.onUpdateEdges();
} }
@@ -215,7 +215,7 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
</td> </td>
<td className="actionCol"> <td className="actionCol">
<span className="rightPaneTrashIcon rightPaneBtns"> <span className="rightPaneTrashIcon rightPaneBtns">
<img src={DeleteIcon} alt="Delete" onClick={(e) => this.removeAddedEdgeToNeighbor(index)} /> <img src={DeleteIcon} alt="Delete" onClick={() => this.removeAddedEdgeToNeighbor(index)} />
</span> </span>
</td> </td>
</tr> </tr>

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

@@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import CloseIcon from "../../../../images/close-black.svg"; import CloseIcon from "../../../../images/close-black.svg";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
export interface QueryContainerComponentProps { export interface QueryContainerComponentProps {
initialQuery: string; initialQuery: string;
@@ -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

@@ -8,6 +8,7 @@ import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@@ -53,8 +54,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker", container.memoryUsageInfo)); uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
} }
return ( return (

View File

@@ -6,6 +6,8 @@ import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager"; import NotebookManager from "../../Notebook/NotebookManager";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
@@ -27,9 +29,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
}); });
it("Account is not serverless - button should be visible", () => { it("Account is not serverless - button should be visible", () => {
@@ -70,18 +69,19 @@ describe("CommandBarComponentButtonFactory tests", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
}); });
afterEach(() => { afterEach(() => {
updateUserContext({ updateUserContext({
portalEnv: "prod", portalEnv: "prod",
}); });
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
}); });
it("Notebooks is already enabled - button should be hidden", () => { it("Notebooks is already enabled - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
@@ -89,8 +89,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Account is running on one of the national clouds - button should be hidden", () => { it("Account is running on one of the national clouds - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
updateUserContext({ updateUserContext({
portalEnv: "mooncake", portalEnv: "mooncake",
}); });
@@ -101,8 +99,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is not enabled but is available - button should be shown and enabled", () => { it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
@@ -112,9 +109,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined(); expect(enableNotebookBtn).toBeDefined();
@@ -138,24 +132,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isShellEnabled = ko.observable(true);
}); });
afterAll(() => { afterAll(() => {
updateUserContext({ updateUserContext({
apiType: "SQL", apiType: "SQL",
}); });
useNotebook.getState().setIsShellEnabled(false);
}); });
beforeEach(() => { beforeEach(() => {
updateUserContext({ updateUserContext({
apiType: "Mongo", apiType: "Mongo",
}); });
mockExplorer.isNotebookEnabled = ko.observable(false); useNotebook.getState().setIsShellEnabled(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); });
mockExplorer.isShellEnabled = ko.observable(true); afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
}); });
it("Mongo Api not available - button should be hidden", () => { it("Mongo Api not available - button should be hidden", () => {
@@ -184,7 +179,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is not enabled and is available - button should be hidden", () => { it("Notebooks is not enabled and is available - button should be hidden", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@@ -192,7 +187,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@@ -202,8 +197,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@@ -213,9 +208,9 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
mockExplorer.isShellEnabled = ko.observable(false); useNotebook.getState().setIsShellEnabled(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
@@ -236,7 +231,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
}); });
beforeEach(() => { beforeEach(() => {
@@ -247,8 +241,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
mockExplorer.isNotebookEnabled = ko.observable(false); });
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
}); });
it("Cassandra Api not available - button should be hidden", () => { it("Cassandra Api not available - button should be hidden", () => {
@@ -259,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();
@@ -282,7 +278,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is not enabled and is available - button should be shown and enabled", () => { it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
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);
@@ -290,7 +286,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
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);
@@ -300,8 +296,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
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);
@@ -326,23 +322,17 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount, } as DatabaseAccount,
}); });
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.notebookManager = new NotebookManager(); mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined); mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
}); });
beforeEach(() => {
mockExplorer.isNotebookEnabled = ko.observable(false);
});
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
useNotebook.getState().setIsNotebookEnabled(false);
}); });
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => { it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel); const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
@@ -350,7 +340,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => { it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true); useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true); mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
@@ -376,10 +366,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Resource token", () => { describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase; const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection); useSelectedNode.getState().setSelectedNode(mockCollection);
useDatabases.setState({ resourceTokenCollection: mockCollection });
const selectedNodeState = useSelectedNode.getState(); const selectedNodeState = useSelectedNode.getState();
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.resourceTokenCollection = ko.observable(mockCollection);
updateUserContext({ updateUserContext({
authType: AuthType.ResourceToken, authType: AuthType.ResourceToken,

View File

@@ -22,15 +22,22 @@ 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";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
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 { SelectedNodeState } from "../../useSelectedNode"; import { SelectedNodeState } from "../../useSelectedNode";
let counter = 0; let counter = 0;
@@ -62,7 +69,7 @@ export function createStaticCommandBarButtons(
buttons.push(createDivider()); buttons.push(createDivider());
if (container.isNotebookEnabled()) { if (useNotebook.getState().isNotebookEnabled) {
const newNotebookButton = createNewNotebookButton(container); const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)]; newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton); buttons.push(newNotebookButton);
@@ -76,7 +83,7 @@ export function createStaticCommandBarButtons(
buttons.push(createNotebookWorkspaceResetButton(container)); buttons.push(createNotebookWorkspaceResetButton(container));
if ( if (
(userContext.apiType === "Mongo" && (userContext.apiType === "Mongo" &&
container.isShellEnabled() && useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) || selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra" userContext.apiType === "Cassandra"
) { ) {
@@ -138,13 +145,13 @@ export function createContextCommandBarButtons(
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell"; const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = { const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (container.isShellEnabled()) { if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
@@ -269,7 +276,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(), onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: container.isSynapseLinkUpdating(), disabled: useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label, ariaLabel: label,
}; };
} }
@@ -279,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,
@@ -413,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,
@@ -446,12 +453,18 @@ 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: !container.isNotebooksEnabledForAccount(), disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
ariaLabel: label, ariaLabel: label,
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip, tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
}; };
} }
@@ -475,15 +488,21 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
const title = "Set up workspace"; const title = "Set up workspace";
const description = const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account."; "Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled(); const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return { return {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
if (container.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,
@@ -501,15 +520,21 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
const title = "Set up workspace"; const title = "Set up workspace";
const description = const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account."; "Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled(); const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return { return {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
if (container.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,
@@ -536,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,
@@ -554,12 +590,12 @@ function createStaticCommandBarButtonsForResourceToken(
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState); const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton(container); const openQueryBtn = createOpenQueryButton(container);
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
const isResourceTokenCollectionNodeSelected: boolean = const isResourceTokenCollectionNodeSelected: boolean =
container.resourceTokenCollection() && resourceTokenCollection?.id() === selectedNodeState.selectedNode?.id();
container.resourceTokenCollection().id() === selectedNodeState.selectedNode?.id();
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected; newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
newSqlQueryBtn.onCommandClick = () => { newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection(); const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined); resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
}; };

View File

@@ -6,16 +6,14 @@ import {
IDropdownOption, IDropdownOption,
IDropdownStyles, IDropdownStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { Observable } from "knockout";
import * as React from "react"; import * as React from "react";
import _ from "underscore"; import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg"; import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { MemoryTrackerComponent } from "./MemoryTrackerComponent"; import { MemoryTracker } from "./MemoryTrackerComponent";
/** /**
* Convert our NavbarButtonConfig to UI Fabric buttons * Convert our NavbarButtonConfig to UI Fabric buttons
@@ -185,12 +183,9 @@ export const createDivider = (key: string): ICommandBarItemProps => {
}; };
}; };
export const createMemoryTracker = ( export const createMemoryTracker = (key: string): ICommandBarItemProps => {
key: string,
memoryUsageInfo: Observable<MemoryUsageInfo>
): ICommandBarItemProps => {
return { return {
key, key,
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />, onRender: () => <MemoryTracker />,
}; };
}; };

View File

@@ -1,48 +1,29 @@
import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react"; import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { Observable, Subscription } from "knockout";
import * as React from "react"; import * as React from "react";
import { MemoryUsageInfo } from "../../../Contracts/DataModels"; import { useNotebook } from "../../Notebook/useNotebook";
interface MemoryTrackerProps {
memoryUsageInfo: Observable<MemoryUsageInfo>;
}
export class MemoryTrackerComponent extends React.Component<MemoryTrackerProps> {
private memoryUsageInfoSubscription: Subscription;
public componentDidMount(): void {
this.memoryUsageInfoSubscription = this.props.memoryUsageInfo.subscribe(() => {
this.forceUpdate();
});
}
public componentWillUnmount(): void {
this.memoryUsageInfoSubscription && this.memoryUsageInfoSubscription.dispose();
}
public render(): JSX.Element {
const memoryUsageInfo: MemoryUsageInfo = this.props.memoryUsageInfo();
if (!memoryUsageInfo) {
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
}
const totalGB = memoryUsageInfo.totalKB / 1048576;
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
export const MemoryTracker: React.FC = (): JSX.Element => {
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
if (!memoryUsageInfo) {
return ( return (
<Stack className="memoryTrackerContainer" horizontal> <Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span> <span>Memory</span>
<ProgressIndicator <Spinner size={SpinnerSize.medium} />
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
</Stack> </Stack>
); );
} }
}
const totalGB = memoryUsageInfo.totalKB / 1048576;
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
</Stack>
);
};

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

@@ -34,6 +34,7 @@ import {
import { webSocket } from "rxjs/webSocket"; import { webSocket } from "rxjs/webSocket";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants"; import { Areas } from "../../../Common/Constants";
import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
@@ -776,9 +777,11 @@ const closeUnsupportedMimetypesEpic = (
if (explorer && !TextFile.handles(mimetype)) { if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
explorer.tabsManager.closeTabsByComparator( useTabs
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) .getState()
); .closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg); explorer.showOkModalDialog("File cannot be rendered", msg);
logConsoleError(msg); logConsoleError(msg);
@@ -804,9 +807,11 @@ const closeContentFailedToFetchEpic = (
if (explorer) { if (explorer) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
explorer.tabsManager.closeTabsByComparator( useTabs
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) .getState()
); .closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `Failed to load file: ${filepath}.`; const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg); explorer.showOkModalDialog("Failure to load", msg);
logConsoleError(msg); logConsoleError(msg);

View File

@@ -8,25 +8,26 @@ import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient { export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {}; private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean; private isResettingWorkspace: boolean;
constructor( constructor(private onConnectionLost: () => void) {
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>, const notebookServerInfo = useNotebook.getState().notebookServerInfo;
private onConnectionLost: () => void, if (notebookServerInfo?.notebookServerEndpoint) {
private onMemoryUsageInfoUpdate: (update: DataModels.MemoryUsageInfo) => void
) {
if (notebookServerInfo() && notebookServerInfo().notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs); this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else { } else {
const subscription = notebookServerInfo.subscribe((newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => { const unsub = useNotebook.subscribe(
if (newServerInfo && newServerInfo.notebookServerEndpoint) { (newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs); if (newServerInfo?.notebookServerEndpoint) {
} this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
subscription.dispose(); }
}); unsub();
},
(state) => state.notebookServerInfo
);
} }
} }
@@ -36,13 +37,14 @@ export class NotebookContainerClient {
private scheduleHeartbeat(delayMs: number): void { private scheduleHeartbeat(delayMs: number): void {
setTimeout(() => { setTimeout(() => {
this.getMemoryUsage() this.getMemoryUsage()
.then((memoryUsageInfo) => this.onMemoryUsageInfoUpdate(memoryUsageInfo)) .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs)); .finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
}, delayMs); }, delayMs);
} }
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> { private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) { const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected"; const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/getMemoryUsage"); Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
return Promise.reject(error); return Promise.reject(error);
@@ -98,7 +100,8 @@ export class NotebookContainerClient {
} }
private async _resetWorkspace(): Promise<void> { private async _resetWorkspace(): Promise<void> {
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) { const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected"; const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace"); Logger.logError(error, "NotebookContainerClient/resetWorkspace");
return Promise.reject(error); return Promise.reject(error);
@@ -117,15 +120,11 @@ export class NotebookContainerClient {
} }
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } { private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
let authToken: string, const notebookServerInfo = useNotebook.getState().notebookServerInfo;
notebookServerEndpoint = this.notebookServerInfo().notebookServerEndpoint, const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
token = this.notebookServerInfo().authToken;
if (token) {
authToken = `Token ${token}`;
}
return { return {
notebookServerEndpoint, notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
authToken, authToken,
}; };
} }

View File

@@ -1,24 +1,31 @@
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 DataModels from "../../Contracts/DataModels";
import * as StringUtils from "../../Utils/StringUtils"; import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil"; import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { NotebookUtil } from "./NotebookUtil"; import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContentClient { export class NotebookContentClient {
constructor( constructor(private contentProvider: IContentProvider) {}
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
public notebookBasePath: ko.Observable<string>,
private contentProvider: IContentProvider
) {}
/** /**
* 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));
@@ -59,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;
}
} }
/** /**
@@ -271,9 +280,10 @@ export class NotebookContentClient {
} }
private getServerConfig(): ServerConfig { private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return { return {
endpoint: this.notebookServerInfo().notebookServerEndpoint, endpoint: notebookServerInfo.notebookServerEndpoint,
token: this.notebookServerInfo().authToken, token: notebookServerInfo.authToken,
crossDomain: true, crossDomain: true,
}; };
} }

View File

@@ -4,13 +4,11 @@
import { ImmutableNotebook } from "@nteract/commutable"; import { ImmutableNotebook } from "@nteract/commutable";
import type { IContentProvider } from "@nteract/core"; import type { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react"; import React from "react";
import { contents } from "rx-jupyter"; import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants"; import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { GitHubClient } from "../../GitHub/GitHubClient"; import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
@@ -22,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";
@@ -37,7 +36,6 @@ export type { NotebookPaneContent };
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter; resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void; refreshCommandBarButtons: () => void;
refreshNotebookList: () => void; refreshNotebookList: () => void;
@@ -81,23 +79,28 @@ export default class NotebookManager {
contents.JupyterContentProvider contents.JupyterContentProvider
); );
this.notebookClient = new NotebookContainerClient( this.notebookClient = new NotebookContainerClient(() =>
this.params.container.notebookServerInfo, this.params.container.initNotebooks(userContext?.databaseAccount)
() => this.params.container.initNotebooks(userContext?.databaseAccount),
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
); );
this.notebookContentClient = new NotebookContentClient( this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
this.params.container.notebookServerInfo,
this.params.notebookBasePath,
this.notebookContentProvider
);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => { this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
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();
@@ -138,6 +141,7 @@ export default class NotebookManager {
notebookContentRef={notebookContentRef} notebookContentRef={notebookContentRef}
onTakeSnapshot={onTakeSnapshot} onTakeSnapshot={onTakeSnapshot}
/>, />,
"440px",
onClosePanel onClosePanel
); );
} }
@@ -170,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

@@ -0,0 +1,205 @@
import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
interface NotebookState {
isNotebookEnabled: boolean;
isNotebooksEnabledForAccount: boolean;
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
isSynapseLinkUpdating: boolean;
memoryUsageInfo: DataModels.MemoryUsageInfo;
isShellEnabled: boolean;
notebookBasePath: string;
isInitializingNotebooks: boolean;
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void;
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => 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, get) => ({
isNotebookEnabled: false,
isNotebooksEnabledForAccount: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
password: undefined,
endpoints: [],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
set({ isNotebooksEnabledForAccount: false });
return;
}
const firstWriteLocation =
databaseAccount?.properties?.writeLocations &&
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
body: JSON.stringify({
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
}),
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.contentType]: "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to fetch disallowed locations");
}
const disallowedLocations: string[] = await response.json();
if (!disallowedLocations) {
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: true });
return;
}
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
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

@@ -4,6 +4,7 @@ import { logError } from "../../../Common/Logger";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import { Collection } from "../../../Contracts/ViewModels"; import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor"; import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
@@ -36,7 +37,7 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query); selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query);
} }
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab); const queryTab = useTabs.getState().activeTab as NewQueryTab;
queryTab.tabTitle(savedQuery.queryName); queryTab.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`); queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);

View File

@@ -9,6 +9,7 @@ import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
@@ -101,7 +102,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
name: ResourceTreeAdapter.MyNotebooksTitle, name: ResourceTreeAdapter.MyNotebooksTitle,
path: container.getNotebookBasePath(), path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
break; break;

View File

@@ -10,7 +10,6 @@ import { Collection, Database } from "../../../Contracts/ViewModels";
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 { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
@@ -53,10 +52,7 @@ describe("Delete Collection Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => { describe("shouldRecordFeedback()", () => {
it("should return true if last collection and database does not have shared throughput else false", () => { it("should return true if last collection and database does not have shared throughput else false", () => {
const fakeExplorer = new Explorer(); const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
fakeExplorer.refreshAllDatabases = () => undefined;
const wrapper = shallow(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
const database = { id: ko.observable("testDB") } as Database; const database = { id: ko.observable("testDB") } as Database;
@@ -65,11 +61,11 @@ describe("Delete Collection Confirmation Pane", () => {
database.isDatabaseShared = ko.computed(() => false); database.isDatabaseShared = ko.computed(() => false);
useDatabases.getState().addDatabases([database]); useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(database); useSelectedNode.getState().setSelectedNode(database);
wrapper.setProps({ explorer: fakeExplorer }); wrapper.setProps({});
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
database.isDatabaseShared = ko.computed(() => true); database.isDatabaseShared = ko.computed(() => true);
wrapper.setProps({ explorer: fakeExplorer }); wrapper.setProps({});
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
}); });
}); });
@@ -77,8 +73,6 @@ describe("Delete Collection Confirmation Pane", () => {
describe("submit()", () => { describe("submit()", () => {
const selectedCollectionId = "testCol"; const selectedCollectionId = "testCol";
const databaseId = "testDatabase"; const databaseId = "testDatabase";
const fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
const database = { id: ko.observable(databaseId) } as Database; const database = { id: ko.observable(databaseId) } as Database;
const collection = { const collection = {
id: ko.observable(selectedCollectionId), id: ko.observable(selectedCollectionId),
@@ -115,7 +109,7 @@ describe("Delete Collection Confirmation Pane", () => {
}); });
it("should call delete collection", () => { it("should call delete collection", () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />); const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmCollectionId")).toBe(true); expect(wrapper.exists("#confirmCollectionId")).toBe(true);
@@ -132,7 +126,7 @@ describe("Delete Collection Confirmation Pane", () => {
}); });
it("should record feedback", async () => { it("should record feedback", async () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />); const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
expect(wrapper.exists("#confirmCollectionId")).toBe(true); expect(wrapper.exists("#confirmCollectionId")).toBe(true);
wrapper wrapper
.find("#confirmCollectionId") .find("#confirmCollectionId")

View File

@@ -6,23 +6,23 @@ import DeleteFeedback from "../../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Collection } from "../../../Contracts/ViewModels"; import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
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 } from "../../../Utils/APITypeUtils"; import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface DeleteCollectionConfirmationPaneProps { export interface DeleteCollectionConfirmationPaneProps {
explorer: Explorer; refreshDatabases: () => Promise<void>;
} }
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
explorer, refreshDatabases,
}: DeleteCollectionConfirmationPaneProps) => { }: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>(""); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
@@ -31,8 +31,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const [isExecuting, setIsExecuting] = useState(false); const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean => const shouldRecordFeedback = (): boolean =>
useDatabases.getState().isLastCollection() && useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
!useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName; const paneTitle = "Delete " + collectionName;
@@ -63,10 +62,12 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
setIsExecuting(false); setIsExecuting(false);
useSelectedNode.getState().setSelectedNode(collection.database); useSelectedNode.getState().setSelectedNode(collection.database);
explorer.tabsManager?.closeTabsByComparator( useTabs
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId .getState()
); .closeTabsByComparator(
explorer.refreshAllDatabases(); (tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);
refreshDatabases();
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey); TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);

View File

@@ -2,11 +2,7 @@
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = ` exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
<DeleteCollectionConfirmationPane <DeleteCollectionConfirmationPane
explorer={ refreshDatabases={[Function]}
Object {
"refreshAllDatabases": [Function],
}
}
> >
<RightPaneForm <RightPaneForm
formError="" formError=""

View File

@@ -10,15 +10,12 @@ import { Collection, Database } from "../../Contracts/ViewModels";
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 { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
describe("Delete Database Confirmation Pane", () => { describe("Delete Database Confirmation Pane", () => {
const selectedDatabaseId = "testDatabase"; const selectedDatabaseId = "testDatabase";
let fakeExplorer: Explorer;
let database: Database; let database: Database;
beforeAll(() => { beforeAll(() => {
@@ -37,10 +34,6 @@ describe("Delete Database Confirmation Pane", () => {
}); });
beforeEach(() => { beforeEach(() => {
fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.tabsManager = new TabsManager();
database = {} as Database; database = {} as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]); database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>(selectedDatabaseId); database.id = ko.observable<string>(selectedDatabaseId);
@@ -56,17 +49,17 @@ describe("Delete Database Confirmation Pane", () => {
}); });
it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => { it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => {
const wrapper = shallow(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />); const wrapper = shallow(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().addDatabases([database]); useDatabases.getState().addDatabases([database]);
wrapper.setProps({ explorer: fakeExplorer }); wrapper.setProps({});
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
useDatabases.getState().clearDatabases(); useDatabases.getState().clearDatabases();
}); });
it("Should call delete database", () => { it("Should call delete database", () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />); const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true); expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
@@ -81,7 +74,7 @@ describe("Delete Database Confirmation Pane", () => {
}); });
it("should record feedback", async () => { it("should record feedback", async () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />); const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
expect(wrapper.exists("#confirmDatabaseId")).toBe(true); expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper wrapper
.find("#confirmDatabaseId") .find("#confirmDatabaseId")

View File

@@ -7,23 +7,23 @@ import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection, Database } from "../../Contracts/ViewModels"; import { Collection, Database } from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
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 { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
interface DeleteDatabaseConfirmationPanelProps { interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer; refreshDatabases: () => Promise<void>;
} }
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
explorer, refreshDatabases,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => { }: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase); const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase);
@@ -32,7 +32,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>(""); const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>(""); const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const selectedDatabase: Database = useSelectedNode.getState().findSelectedDatabase(); const selectedDatabase: Database = useDatabases.getState().findSelectedDatabase();
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
@@ -52,15 +52,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
try { try {
await deleteDatabase(selectedDatabase.id()); await deleteDatabase(selectedDatabase.id());
closeSidePanel(); closeSidePanel();
explorer.refreshAllDatabases(); refreshDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id()); useTabs.getState().closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
useSelectedNode.getState().setSelectedNode(undefined); useSelectedNode.getState().setSelectedNode(undefined);
selectedDatabase selectedDatabase
.collections() .collections()
.forEach((collection: Collection) => .forEach((collection: Collection) =>
explorer.tabsManager.closeTabsByComparator( useTabs
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId .getState()
) .closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
)
); );
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.DeleteDatabase, Action.DeleteDatabase,

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

@@ -19,17 +19,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
@@ -37,21 +28,11 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [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

@@ -5,6 +5,7 @@ import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
@@ -34,7 +35,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
logConsoleError("Failed to save query: account not setup to save queries"); logConsoleError("Failed to save query: account not setup to save queries");
} }
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab); const queryTab = useTabs.getState().activeTab as NewQueryTab;
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent(); const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
if (!queryName || queryName.length === 0) { if (!queryName || queryName.length === 0) {

View File

@@ -63,7 +63,7 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
userContext.databaseAccount.name, userContext.databaseAccount.name,
"default" "default"
); );
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks explorer.refreshExplorer();
closeSidePanel(); closeSidePanel();

View File

@@ -1,15 +1,14 @@
import { TextField } from "@fluentui/react"; import { TextField } from "@fluentui/react";
import React, { FormEvent, FunctionComponent, useState } from "react"; import React, { FormEvent, FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab"; import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface StringInputPanelProps { export interface StringInputPanelProps {
explorer: Explorer;
closePanel: () => void; closePanel: () => void;
errorMessage: string; errorMessage: string;
inProgressMessage: string; inProgressMessage: string;
@@ -23,7 +22,6 @@ export interface StringInputPanelProps {
} }
export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
explorer: container,
closePanel, closePanel,
errorMessage, errorMessage,
inProgressMessage, inProgressMessage,
@@ -55,10 +53,12 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
logConsoleInfo(`${successMessage}: ${stringInput}`); logConsoleInfo(`${successMessage}: ${stringInput}`);
const originalPath = notebookFile.path; const originalPath = notebookFile.path;
const notebookTabs = container.tabsManager.getTabs( const notebookTabs = useTabs
ViewModels.CollectionTabKind.NotebookV2, .getState()
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath) .getTabs(
); ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
notebookTabs.forEach((tab) => { notebookTabs.forEach((tab) => {
tab.tabTitle(newNotebookFile.name); tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path); tab.tabPath(newNotebookFile.path);

View File

@@ -9,17 +9,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
@@ -27,21 +18,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [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";
@@ -14,8 +14,8 @@ import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../../Tables/TableDataClient";
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 { NewQueryTablesTab } from "../../Tabs/QueryTablesTab/NewQueryTablesTab";
import { PanelContainerComponent } from "../PanelContainerComponent"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@@ -30,15 +30,13 @@ import {
getCassandraDefaultEntities, getCassandraDefaultEntities,
getDefaultEntities, getDefaultEntities,
getEntityValuePlaceholder, getEntityValuePlaceholder,
getPanelTitle,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
interface AddTableEntityPanelProps { interface AddTableEntityPanelProps {
tableDataClient: TableDataClient; tableDataClient: TableDataClient;
queryTablesTab: QueryTablesTab; queryTablesTab: NewQueryTablesTab;
tableEntityListViewModel: TableEntityListViewModel; tableEntityListViewModel: TableEntityListViewModel;
cassandraApiClient: CassandraAPIDataClient; cassandraApiClient: CassandraAPIDataClient;
} }
@@ -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";
@@ -13,8 +13,8 @@ import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListView
import * as Entities from "../../Tables/Entities"; 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 { NewQueryTablesTab } from "../../Tabs/QueryTablesTab/NewQueryTablesTab";
import { PanelContainerComponent } from "../PanelContainerComponent"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@@ -29,13 +29,12 @@ import {
getEntityValuePlaceholder, getEntityValuePlaceholder,
getFormattedTime, getFormattedTime,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
interface EditTableEntityPanelProps { interface EditTableEntityPanelProps {
tableDataClient: TableDataClient; tableDataClient: TableDataClient;
queryTablesTab: QueryTablesTab; queryTablesTab: NewQueryTablesTab;
tableEntityListViewModel: TableEntityListViewModel; tableEntityListViewModel: TableEntityListViewModel;
cassandraApiClient: CassandraAPIDataClient; cassandraApiClient: CassandraAPIDataClient;
} }
@@ -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

@@ -2,15 +2,7 @@
exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<DeleteDatabaseConfirmationPanel <DeleteDatabaseConfirmationPanel
explorer={ refreshDatabases={[Function]}
Object {
"refreshAllDatabases": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
> >
<RightPaneForm <RightPaneForm
formError="" formError=""

View File

@@ -1,14 +1,10 @@
import * as ko from "knockout";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil"; import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { SplashScreen } from "./SplashScreen"; import { SplashScreen } from "./SplashScreen";
jest.mock("../Explorer"); jest.mock("../Explorer");
const createExplorer = () => { const createExplorer = () => {
const mock = new Explorer(); const mock = new Explorer();
mock.isNotebookEnabled = ko.observable(false);
mock.tabsManager = new TabsManager();
return mock as jest.Mocked<Explorer>; return mock as jest.Mocked<Explorer>;
}; };

View File

@@ -16,12 +16,16 @@ 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";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil"; 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 { 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";
@@ -61,8 +65,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() { public componentDidMount() {
this.subscriptions.push( this.subscriptions.push(
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }, {
this.container.isNotebookEnabled.subscribe(() => this.setState({})) dispose: useNotebook.subscribe(
() => this.setState({}),
(state) => state.isNotebookEnabled
),
},
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }
); );
} }
@@ -167,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>
@@ -210,7 +219,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}); });
} }
if (this.container.isNotebookEnabled()) { if (useNotebook.getState().isNotebookEnabled) {
heroes.push({ heroes.push({
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
title: "New Notebook", title: "New Notebook",
@@ -235,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,
}); });
} }
@@ -256,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} />),
}); });
} }
@@ -265,22 +277,22 @@ 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);
}, },
}); });
} }
/* Scale & Settings */ /* Scale & Settings */
const isShared = useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared(); const isShared = useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const label = isShared ? "Settings" : "Scale & Settings"; const label = isShared ? "Settings" : "Scale & Settings";
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();
@@ -290,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} />),
}); });
} }
@@ -342,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,4 @@
export var TableType = { export const TableType = {
String: "String", String: "String",
Boolean: "Boolean", Boolean: "Boolean",
Binary: "Binary", Binary: "Binary",
@@ -9,7 +9,7 @@ export var TableType = {
Int64: "Int64", Int64: "Int64",
}; };
export var CassandraType = { export const CassandraType = {
Ascii: "Ascii", Ascii: "Ascii",
Bigint: "Bigint", Bigint: "Bigint",
Blob: "Blob", Blob: "Blob",
@@ -27,12 +27,12 @@ export var CassandraType = {
Tinyint: "Tinyint", Tinyint: "Tinyint",
}; };
export var ClauseRule = { export const ClauseRule = {
And: "And", And: "And",
Or: "Or", Or: "Or",
}; };
export var Operator = { export const Operator = {
EqualTo: "==", EqualTo: "==",
GreaterThan: ">", GreaterThan: ">",
GreaterThanOrEqualTo: ">=", GreaterThanOrEqualTo: ">=",
@@ -42,7 +42,7 @@ export var Operator = {
Equal: "=", Equal: "=",
}; };
export var ODataOperator = { export const ODataOperator = {
EqualTo: "eq", EqualTo: "eq",
GreaterThan: "gt", GreaterThan: "gt",
GreaterThanOrEqualTo: "ge", GreaterThanOrEqualTo: "ge",
@@ -51,7 +51,7 @@ export var ODataOperator = {
NotEqualTo: "ne", NotEqualTo: "ne",
}; };
export var timeOptions = { export const timeOptions = {
lastHour: "Last hour", lastHour: "Last hour",
last24Hours: "Last 24 hours", last24Hours: "Last 24 hours",
last7Days: "Last 7 days", last7Days: "Last 7 days",
@@ -62,7 +62,7 @@ export var timeOptions = {
custom: "Custom...", custom: "Custom...",
}; };
export var htmlSelectors = { export const htmlSelectors = {
dataTableSelector: "#storageTable", dataTableSelector: "#storageTable",
dataTableAllRowsSelector: "#storageTable tbody tr", dataTableAllRowsSelector: "#storageTable tbody tr",
dataTableHeadRowSelector: ".dataTable thead tr", dataTableHeadRowSelector: ".dataTable thead tr",
@@ -84,9 +84,9 @@ export var htmlSelectors = {
selectAllDropdownSelector: "#select-all-dropdown", selectAllDropdownSelector: "#select-all-dropdown",
}; };
export var defaultHeader = " "; export const defaultHeader = " ";
export var EntityKeyNames = { export const EntityKeyNames = {
PartitionKey: "PartitionKey", PartitionKey: "PartitionKey",
RowKey: "RowKey", RowKey: "RowKey",
Timestamp: "Timestamp", Timestamp: "Timestamp",
@@ -94,7 +94,7 @@ export var EntityKeyNames = {
Etag: "etag", Etag: "etag",
}; };
export var htmlAttributeNames = { export const htmlAttributeNames = {
dataTableNameAttr: "name_attr", dataTableNameAttr: "name_attr",
dataTableContentTypeAttr: "contentType_attr", dataTableContentTypeAttr: "contentType_attr",
dataTableSnapshotAttr: "snapshot_attr", dataTableSnapshotAttr: "snapshot_attr",
@@ -103,14 +103,14 @@ export var htmlAttributeNames = {
dataTableHeaderIndex: "data-column-index", dataTableHeaderIndex: "data-column-index",
}; };
export var cssColors = { export const cssColors = {
commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */, commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */,
}; };
export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"]; export const clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
export var transparentColor = "transparent"; export const transparentColor = "transparent";
export var keyCodes = { export const keyCodes = {
RightClick: 3, RightClick: 3,
Enter: 13, Enter: 13,
Esc: 27, Esc: 27,
@@ -163,7 +163,7 @@ export var keyCodes = {
Dash: 189, Dash: 189,
}; };
export var InputType = { export const InputType = {
Text: "text", Text: "text",
// Chrome doesn't support datetime, instead, datetime-local is supported. // Chrome doesn't support datetime, instead, datetime-local is supported.
DateTime: "datetime-local", DateTime: "datetime-local",

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

@@ -1,14 +1,14 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import CacheBase from "./CacheBase";
import * as CommonConstants from "../../../Common/Constants"; import * as CommonConstants from "../../../Common/Constants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { NewQueryTablesTab } from "../../Tabs/QueryTablesTab/NewQueryTablesTab";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import CacheBase from "./CacheBase";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
// This is the format of the data we will have to pass to Datatable render callback, // This is the format of the data we will have to pass to Datatable render callback,
// and property names are defined by Datatable as well. // and property names are defined by Datatable as well.
@@ -49,7 +49,7 @@ abstract class DataTableViewModel {
private dataTableOperationManager: IDataTableOperation; private dataTableOperationManager: IDataTableOperation;
public queryTablesTab: QueryTablesTab; public queryTablesTab: NewQueryTablesTab;
constructor() { constructor() {
this.items([]); this.items([]);

View File

@@ -7,6 +7,7 @@ import { Action } 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 QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { NewQueryTablesTab } from "../../Tabs/QueryTablesTab/NewQueryTablesTab";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
@@ -112,7 +113,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
public queryErrorMessage: ko.Observable<string>; public queryErrorMessage: ko.Observable<string>;
public id: string; public id: string;
constructor(tableCommands: TableCommands, queryTablesTab: QueryTablesTab) { constructor(tableCommands: TableCommands, queryTablesTab: NewQueryTablesTab) {
super(); super();
this.cache = new TableEntityCache(); this.cache = new TableEntityCache();
this.queryErrorMessage = ko.observable<string>(); this.queryErrorMessage = ko.observable<string>();

View File

@@ -5,7 +5,7 @@ import { KeyCodes } from "../../../Common/Constants";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import { NewQueryTablesTab } from "../../Tabs/QueryTablesTab/NewQueryTablesTab";
import { getQuotedCqlIdentifier } from "../CqlUtilities"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities"; import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel"; import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
@@ -39,14 +39,14 @@ export default class QueryViewModel {
public columnOptions: ko.ObservableArray<string>; public columnOptions: ko.ObservableArray<string>;
public queryTablesTab: QueryTablesTab; public queryTablesTab: NewQueryTablesTab;
public id: string; public id: string;
private _tableEntityListViewModel: TableEntityListViewModel; private _tableEntityListViewModel: TableEntityListViewModel;
constructor(queryTablesTab: QueryTablesTab) { constructor(queryTablesTab: NewQueryTablesTab) {
this.queryTablesTab = queryTablesTab; this.queryTablesTab = queryTablesTab;
this.id = `queryViewModel${this.queryTablesTab.tabId}`; this.id = `queryViewModel${this.queryTablesTab.tabId}`;
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel(); this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel;
this.queryTextIsReadOnly = ko.computed<boolean>(() => { this.queryTextIsReadOnly = ko.computed<boolean>(() => {
return userContext.apiType !== "Cassandra"; return userContext.apiType !== "Cassandra";

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import type { TabOptions } from "../../../Contracts/ViewModels"; import type { TabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent"; import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent";
@@ -33,7 +34,7 @@ export class NewMongoShellTab extends TabsBase {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
this.iMongoShellTabAccessor.onTabClickEvent(); this.iMongoShellTabAccessor.onTabClickEvent();
} }
} }

View File

@@ -6,6 +6,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { NotebookClientV2 } from "../Notebook/NotebookClientV2"; import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { useNotebook } from "../Notebook/useNotebook";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
export interface NotebookTabBaseOptions extends ViewModels.TabOptions { export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
@@ -28,7 +29,7 @@ export default class NotebookTabBase extends TabsBase {
if (!NotebookTabBase.clientManager) { if (!NotebookTabBase.clientManager) {
NotebookTabBase.clientManager = new NotebookClientV2({ NotebookTabBase.clientManager = new NotebookClientV2({
connectionInfo: this.container.notebookServerInfo(), connectionInfo: useNotebook.getState().notebookServerInfo,
databaseAccountName: userContext?.databaseAccount?.name, databaseAccountName: userContext?.databaseAccount?.name,
defaultExperience: userContext.apiType, defaultExperience: userContext.apiType,
contentProvider: this.container.notebookManager?.notebookContentProvider, contentProvider: this.container.notebookManager?.notebookContentProvider,

View File

@@ -23,6 +23,7 @@ import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { useNotebook } from "../Notebook/useNotebook";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
export interface NotebookTabOptions extends NotebookTabBaseOptions { export interface NotebookTabOptions extends NotebookTabBaseOptions {
@@ -39,10 +40,13 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.container = options.container; this.container = options.container;
this.notebookPath = ko.observable(options.notebookContentItem.path); this.notebookPath = ko.observable(options.notebookContentItem.path);
this.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received.")); useNotebook.subscribe(
() => logConsoleInfo("New notebook server info received."),
(state) => state.notebookServerInfo
);
this.notebookComponentAdapter = new NotebookComponentAdapter({ this.notebookComponentAdapter = new NotebookComponentAdapter({
contentItem: options.notebookContentItem, contentItem: options.notebookContentItem,
notebooksBasePath: this.container.getNotebookBasePath(), notebooksBasePath: useNotebook.getState().notebookBasePath,
notebookClient: NotebookTabBase.clientManager, notebookClient: NotebookTabBase.clientManager,
onUpdateKernelInfo: this.onKernelUpdate, onUpdateKernelInfo: this.onKernelUpdate,
}); });
@@ -359,8 +363,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
}; };
private async configureServiceEndpoints(kernelName: string) { private async configureServiceEndpoints(kernelName: string) {
const notebookConnectionInfo = this.container && this.container.notebookServerInfo(); const notebookConnectionInfo = useNotebook.getState().notebookServerInfo;
const sparkClusterConnectionInfo = this.container && this.container.sparkClusterConnectionInfo(); const sparkClusterConnectionInfo = useNotebook.getState().sparkClusterConnectionInfo;
await NotebookConfigurationUtils.configureServiceEndpoints( await NotebookConfigurationUtils.configureServiceEndpoints(
this.notebookPath(), this.notebookPath(),
notebookConnectionInfo, notebookConnectionInfo,

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels"; import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent"; import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
@@ -40,12 +41,12 @@ export class NewQueryTab extends TabsBase {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
this.iTabAccessor.onTabClickEvent(); this.iTabAccessor.onTabClickEvent();
} }
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
this.manager?.closeTab(this); useTabs.getState().closeTab(this);
if (this.iTabAccessor) { if (this.iTabAccessor) {
this.iTabAccessor.onCloseClickEvent(true); this.iTabAccessor.onCloseClickEvent(true);
} }

View File

@@ -22,14 +22,16 @@ 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 { TabsManager } from "../TabsManager";
import "./QueryTabComponent.less"; import "./QueryTabComponent.less";
enum ToggleState { enum ToggleState {
@@ -65,7 +67,6 @@ export interface IQueryTabComponentProps {
partitionKey: DataModels.PartitionKey; partitionKey: DataModels.PartitionKey;
container: Explorer; container: Explorer;
activeTab?: TabsBase; activeTab?: TabsBase;
tabManager?: TabsManager;
onTabAccessor: (instance: ITabAccessor) => void; onTabAccessor: (instance: ITabAccessor) => void;
isPreferredApiMongoDB?: boolean; isPreferredApiMongoDB?: boolean;
monacoEditorSetting?: string; monacoEditorSetting?: string;
@@ -391,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

@@ -0,0 +1,52 @@
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { TabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import TableCommands from "../../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import TabsBase from "../TabsBase";
import {
IQueryTablesTabAccessor,
IQueryTablesTabComponentProps,
QueryTablesTabComponent,
} from "./QueryTablesTabComponent";
export interface IQueryTablesTabProps {
container: Explorer;
}
export class NewQueryTablesTab extends TabsBase {
public queryText: string;
public currentQuery: string;
public partitionKey: DataModels.PartitionKey;
public iQueryTablesTabComponentProps: IQueryTablesTabComponentProps;
public tableEntityListViewModel: TableEntityListViewModel;
public tableCommands: TableCommands;
public iQueryTablesTabAccessor: IQueryTablesTabAccessor;
constructor(options: TabOptions, private props: IQueryTablesTabProps) {
super(options);
this.tableCommands = new TableCommands(props.container);
this.tableEntityListViewModel = new TableEntityListViewModel(this.tableCommands, this);
this.iQueryTablesTabComponentProps = {
collection: this.collection,
tabId: this.tabId,
tabsBaseInstance: this,
queryTablesTab: this,
container: this.props.container,
onQueryTablesTabAccessor: (instance: IQueryTablesTabAccessor) => {
this.iQueryTablesTabAccessor = instance;
},
};
}
public render(): JSX.Element {
return <QueryTablesTabComponent {...this.iQueryTablesTabComponentProps} />;
}
public onTabClick(): void {
useTabs.getState().activateTab(this);
this.iQueryTablesTabAccessor.onTabClickEvent();
}
}

View File

@@ -0,0 +1,619 @@
import React from "react";
import AddEntityIcon from "../../../../images/AddEntity.svg";
import DeleteEntitiesIcon from "../../../../images/DeleteEntities.svg";
import EditEntityIcon from "../../../../images/Edit-entity.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import QueryBuilderIcon from "../../../../images/Query-Builder.svg";
import QueryTextIcon from "../../../../images/Query-Text.svg";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { AddTableEntityPanel } from "../../Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "../../Panes/Tables/EditTableEntityPanel";
import TableCommands from "../../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
import TabsBase from "../TabsBase";
import { NewQueryTablesTab } from "./NewQueryTablesTab";
export interface IQueryTablesTabAccessor {
onTabClickEvent: () => void;
}
export interface Button {
visible: boolean;
enabled: boolean;
isSelected?: boolean;
}
export interface IQueryTablesTabComponentStates {
queryViewModel: QueryViewModel;
queryText: string;
selectedQueryText: string;
executeQueryButton: Button;
queryBuilderButton: Button;
queryTextButton: Button;
addEntityButton: Button;
editEntityButton: Button;
deleteEntityButton: Button;
}
export interface IQueryTablesTabComponentProps {
collection: ViewModels.CollectionBase;
tabsBaseInstance: TabsBase;
tabId: string;
queryTablesTab: NewQueryTablesTab;
container: Explorer;
onQueryTablesTabAccessor: (instance: IQueryTablesTabAccessor) => void;
}
export class QueryTablesTabComponent extends React.Component<
IQueryTablesTabComponentProps,
IQueryTablesTabComponentStates
> {
public collection: ViewModels.Collection;
public tableEntityListViewModel: TableEntityListViewModel;
public tableCommands: TableCommands;
public tableDataClient: TableDataClient;
public container: Explorer;
public queryViewModel: QueryViewModel;
constructor(props: IQueryTablesTabComponentProps) {
super(props);
this.container = props.collection && props.collection.container;
this.tableCommands = new TableCommands(this.container);
this.tableDataClient = this.container.tableDataClient;
this.tableEntityListViewModel = new TableEntityListViewModel(this.tableCommands, props.queryTablesTab);
this.tableEntityListViewModel.queryTablesTab = props.queryTablesTab;
this.queryViewModel = new QueryViewModel(props.queryTablesTab);
this.state = {
queryViewModel: this.queryViewModel,
queryText: "PartitionKey eq 'partitionKey1'",
selectedQueryText: "",
executeQueryButton: {
enabled: true,
visible: true,
},
queryBuilderButton: {
enabled: true,
visible: true,
isSelected: this.queryViewModel ? this.queryViewModel.isHelperActive() : false,
},
queryTextButton: {
enabled: true,
visible: true,
isSelected: this.queryViewModel ? this.queryViewModel.isEditorActive() : false,
},
addEntityButton: {
enabled: true,
visible: true,
},
editEntityButton: {
enabled: this.tableCommands.isEnabled(
TableCommands.editEntityCommand,
this.tableEntityListViewModel.selected()
),
visible: true,
},
deleteEntityButton: {
enabled: this.tableCommands.isEnabled(
TableCommands.deleteEntitiesCommand,
this.tableEntityListViewModel.selected()
),
visible: true,
},
};
if (this.tableEntityListViewModel.items().length > 0 && userContext.apiType === "Tables") {
const sampleQuerySubscription = this.state.queryViewModel.queryBuilderViewModel().setExample();
}
}
public onExecuteQueryClick(): void {
this.state.queryViewModel.runQuery();
}
public onQueryBuilderClick(): void {
this.state.queryViewModel.selectHelper();
}
public onQueryTextClick(): void {
this.state.queryViewModel.selectEditor();
}
public onAddEntityClick(): void {
useSidePanel
.getState()
.openSidePanel(
"Add Table Row",
<AddTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this.props.queryTablesTab}
tableEntityListViewModel={this.tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>,
"700px"
);
}
public onEditEntityClick(): void {
useSidePanel
.getState()
.openSidePanel(
"Edit Table Entity",
<EditTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this.props.queryTablesTab}
tableEntityListViewModel={this.tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public onDeleteEntityClick(): void {
this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel);
}
public onActivate(): void {
const columns =
!!this.tableEntityListViewModel &&
!!this.tableEntityListViewModel.table &&
this.tableEntityListViewModel.table.columns;
if (!!columns) {
columns.adjust();
$(window).resize();
}
}
public getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (this.state.queryBuilderButton.visible) {
const label = userContext.apiType === "Cassandra" ? "CQL Query Builder" : "Query Builder";
buttons.push({
iconSrc: QueryBuilderIcon,
iconAlt: label,
onCommandClick: this.onQueryBuilderClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.state.queryBuilderButton.enabled,
isSelected: this.state.queryBuilderButton.isSelected,
});
}
if (this.state.queryTextButton.visible) {
const label = userContext.apiType === "Cassandra" ? "CQL Query Text" : "Query Text";
buttons.push({
iconSrc: QueryTextIcon,
iconAlt: label,
onCommandClick: this.onQueryTextClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.state.queryTextButton.enabled,
isSelected: this.state.queryTextButton.isSelected,
});
}
if (this.state.executeQueryButton.visible) {
const label = "Run Query";
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: this.onExecuteQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.state.executeQueryButton.enabled,
});
}
if (this.state.addEntityButton.visible) {
const label = userContext.apiType === "Cassandra" ? "Add Row" : "Add Entity";
buttons.push({
iconSrc: AddEntityIcon,
iconAlt: label,
onCommandClick: this.onAddEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.state.addEntityButton.enabled,
});
}
if (this.state.editEntityButton.visible) {
const label = userContext.apiType === "Cassandra" ? "Edit Row" : "Edit Entity";
buttons.push({
iconSrc: EditEntityIcon,
iconAlt: label,
onCommandClick: this.onEditEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.state.editEntityButton.enabled,
});
}
if (this.state.deleteEntityButton.visible) {
const label = userContext.apiType === "Cassandra" ? "Delete Rows" : "Delete Entities";
buttons.push({
iconSrc: DeleteEntitiesIcon,
iconAlt: label,
onCommandClick: this.onDeleteEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.state.deleteEntityButton.enabled,
});
}
return buttons;
}
public render(): JSX.Element {
const { tabId } = this.props;
const { queryViewModel, queryText } = this.state;
const {
isEditorActive,
isHelperActive,
hasQueryError,
queryErrorMessage,
queryBuilderViewModel,
toggleAdvancedOptions,
ontoggleAdvancedOptionsKeyDown,
} = queryViewModel;
const {
canGroupClauses,
andLabel,
actionLabel,
groupClauses,
groupSelectedClauses,
fieldLabel,
dataTypeLabel,
operatorLabel,
valueLabel,
clauseArray,
columnOptions,
clauseRules,
operators,
insertNewFilterLine,
onAddClauseKeyDown,
addClauseIndex,
removeThisFilterLine,
onDeleteClauseKeyDown,
deleteClause,
getClauseGroupViewModels,
updateColumnOptions,
edmTypes,
addNewClause,
addNewClauseLine,
} = queryBuilderViewModel();
console.log("queryBuilderViewModel()", queryBuilderViewModel());
console.log("clauseArray()", clauseArray());
console.log("clauseRules()", clauseRules());
console.log("columnOptions()", columnOptions());
console.log("operators()", operators());
return (
<div className="tab-pane tableContainer" id={tabId} role="tabpanel">
<div className="query-builder" id={queryViewModel.id}>
<div className="error-bar">
{hasQueryError && (
<div className="error-message" aria-label="Error Message">
<span>
<img className="entity-error-Img" src="/error_red.svg" />
</span>
<span className="error-text" role="alert">
{queryErrorMessage}
</span>
</div>
)}
</div>
{isEditorActive() && (
<div className="query-editor-panel">
<div>
<textarea
className="query-editor-text"
data-bind="
css: { 'query-editor-text-invalid': hasQueryError },
readOnly: true"
name="query-editor"
rows={5}
cols={100}
>
{queryText}
</textarea>
</div>
</div>
)}
{isHelperActive && (
<div style={{ paddingLeft: "13px" }}>
<div className="clause-table" data-bind="with: queryBuilderViewModel ">
<div className="scroll-box scrollable" id="scroll">
<table className="clause-table">
<thead>
<tr className="clause-table-row">
<th className="clause-table-cell header-background action-header">
<span>{actionLabel}</span>
</th>
<th className="clause-table-cell header-background group-control-header">
{canGroupClauses && (
<button type="button" onClick={groupClauses} title={groupSelectedClauses}>
<img className="and-or-svg" src="/And-Or.svg" alt="Group selected clauses" />
</button>
)}
</th>
<th className="clause-table-cell header-background"></th>
<th className="clause-table-cell header-background and-or-header">
<span>{andLabel}</span>
</th>
<th className="clause-table-cell header-background field-header">
<span>{fieldLabel}</span>
</th>
<th className="clause-table-cell header-background type-header">
<span>{dataTypeLabel}</span>
</th>
<th className="clause-table-cell header-background operator-header">
<span>{operatorLabel}</span>
</th>
<th className="clause-table-cell header-background value-header">
<span>{valueLabel}</span>
</th>
</tr>
</thead>
<tbody>
{clauseArray().map((data, index) => (
<tr className="clause-table-row">
<td className="clause-table-cell action-column">
<span
className="entity-Add-Cancel"
role="button"
tabIndex={0}
onClick={addClauseIndex.bind(data, index)}
onKeyDown={onAddClauseKeyDown.bind(data, index)}
title={insertNewFilterLine}
>
<img className="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" />
</span>
<span
className="entity-Add-Cancel"
role="button"
tabIndex={0}
data-bind="hasFocus: isDeleteButtonFocused"
onClick={deleteClause.bind(data, index)}
onKeyDown={onDeleteClauseKeyDown.bind(data, index)}
title={removeThisFilterLine}
>
<img className="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" />
</span>
</td>
{/* <td className="clause-table-cell group-control-column">
<input type="checkbox" aria-label="And/Or" checked={checkedForGrouping} />
</td> */}
{/* <td>
<table className="group-indicator-table">
<tbody>
<tr>
{getClauseGroupViewModels(data).map((gi, index) => (
<td
className="group-indicator-column"
style={{
backgroundColor: gi.backgroundColor,
borderTop: gi.showTopBorder.peek() ? "solid thin #CCCCCC" : "none",
borderLeft: gi.showLeftBorder.peek() ? "solid thin #CCCCCC" : "none",
borderBottom: gi.showBottomBorder.peek()
? "solid thin #CCCCCC"
: gi.borderBackgroundColor,
}}
>
{gi.canUngroup && (
<button type="button" onClick={} title={ungroupClausesLabel}>
<img src="/QueryBuilder/UngroupClause_16x.png" alt="Ungroup clauses" />
</button>
)}
</td>
))}
</tr>
</tbody>
</table>
</td>
<td className="clause-table-cell and-or-column">
{canAnd && (
<select
className="clause-table-field and-or-column"
data-bind=" value: and_or, "
autoFocus={isAndOrFocused}
aria-label=" and_or "
>
{clauseRules().map((data, index) => (
<option value={data}>{data}</option>
))}
</select>
)}
</td>
<td className="clause-table-cell field-column" onClick={updateColumnOptions}>
<select
aria-label={field}
className="clause-table-field field-column"
data-bind=" value: field"
>
{columnOptions().map((data, index) => (
<option value={data}>{data}</option>
))}
</select>
</td>
<td className="clause-table-cell type-column">
{isTypeEditable && (
<select
className="clause-table-field type-column"
aria-label={type}
value={type}
data-bind="css: {'query-builder-isDisabled': !isTypeEditable()}"
>
{edmTypes.map((data) => (
<option>{parent.edmTypes} </option>
))}
</select>
)}
</td>
<td className="clause-table-cell operator-column">
{isOperaterEditable && (
<select
className="clause-table-field operator-column"
aria-label={operator}
data-bind="
value: operator,
css: {'query-builder-isDisabled': !isOperaterEditable()}"
>
{operators().map((data, index) => (
<option key={index}>{data}</option>
))}
</select>
)}
</td>
<td className="clause-table-cell value-column">
{isValue && (
<input
type="text"
className="clause-table-field value-column"
type="search"
data-bind="textInput: value"
aria-label={valueLabel}
/>
)}
{isTimestamp && (
<select
className="clause-table-field time-column"
data-bind="options: $parent.timeOptions, value: timeValue"
></select>
)}
{isCustomLastTimestamp && (
<input
className="clause-table-field time-column"
value={customTimeValue}
onClick={customTimestampDialog}
/>
)}
{isCustomRangeTimestamp && (
<input
className="clause-table-field time-column"
type="datetime-local"
step="1"
value={customTimeValue}
/>
)}
</td>
*/}
</tr>
))}
</tbody>
</table>
</div>
<div
className="addClause"
role="button"
onClick={addNewClause}
data-bind="event: { keydown: onAddNewClauseKeyDown }"
title={addNewClauseLine}
tabIndex={0}
>
<div className="addClause-heading">
<span className="clause-table addClause-title">
<img
className="addclauseProperty-Img"
style={{ marginBottom: "5px" }}
src="/Add-property.svg"
alt="Add new clause"
/>
<span style={{ marginLeft: "5px" }}>{addNewClauseLine}</span>
</span>
</div>
</div>
</div>
</div>
)}
<div className="advanced-options-panel">
{/* <div className="advanced-heading">
<span
className="advanced-title"
role="button"
onClick={toggleAdvancedOptions}
onKeyDown={ontoggleAdvancedOptionsKeyDown}
aria-expanded={isExpanded() ? "true" : "false"}
tabIndex={0}
>
<div
className="themed-images"
type="text/html"
id="ExpandChevronRight"
data-bind="hasFocus: focusExpandIcon"
>
<img
className="imgiconwidth expand-triangle expand-triangle-right"
src="/Triangle-right.svg"
alt="toggle"
/>
</div>
<div className="themed-images" type="text/html" id="ExpandChevronDown">
<img className="imgiconwidth expand-triangle" src="/Triangle-down.svg" alt="toggle" />
</div>
<span>Advanced Options</span>
</span>
</div>
{isExpanded && (
<div className="advanced-options">
<div className="top">
<span>Show top results:</span>
<input
className="top-input"
type="number"
autoFocus={focusTopResult}
value={topValue}
title={topValueLimitMessage}
role="textbox"
aria-label="Show top results"
/>
{isExceedingLimit && (
<div role="alert" aria-atomic="true" className="inline-div">
<img className="advanced-options-icon" src="/QueryBuilder/StatusWarning_16x.png" />
<span>{topValueLimitMessage}</span>
</div>
)}
</div>
<div className="select">
<span> Select fields for query: </span>
{isSelected && (
<div>
<img className="advanced-options-icon" src="/QueryBuilder/QueryInformation_16x.png" />
<span className="select-options-text">{selectMessage}</span>
</div>
)}
<a
className="select-options-link"
onKeyDown={onselectQueryOptionsKeyDown}
onClick={selectQueryOptions}
tabIndex={0}
role="link"
>
<span>Choose Columns... </span>
</a>
</div>
</div>
)} */}
</div>
</div>
<div className="tablesQueryTab tableContainer" id={this.tableEntityListViewModel.id}>
<table
id="storageTable"
className="storage azure-table show-gridlines"
tabIndex={0}
data-bind="tableSource: items, tableSelection: selected"
></table>
</div>
</div>
);
}
}

View File

@@ -2,6 +2,7 @@ import React from "react";
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure"; import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
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 { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure"; import StoredProcedure from "../../Tree/StoredProcedure";
import ScriptTabBase from "../ScriptTabBase"; import ScriptTabBase from "../ScriptTabBase";
@@ -51,12 +52,12 @@ export class NewStoredProcedureTab extends ScriptTabBase {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
this.iStoreProcAccessor.onTabClickEvent(); this.iStoreProcAccessor.onTabClickEvent();
} }
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
this.manager?.closeTab(this); useTabs.getState().closeTab(this);
} }
public onExecuteSprocsResult(result: ExecuteSprocResult): void { public onExecuteSprocsResult(result: ExecuteSprocResult): void {

View File

@@ -10,6 +10,7 @@ import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProc
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure"; import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { useTabs } from "../../../hooks/useTabs";
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";
@@ -144,7 +145,7 @@ export default class StoredProcedureTabComponent extends React.Component<
} }
public onTabClick(): void { public onTabClick(): void {
if (this.props.container.tabsManager.openedTabs().length > 0) { if (useTabs.getState().openedTabs.length > 0) {
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
} }
@@ -396,10 +397,8 @@ export default class StoredProcedureTabComponent extends React.Component<
editorModel && editorModel.setValue(createdResource.body as string); editorModel && editorModel.setValue(createdResource.body as string);
this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string); this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createStoredProcedureNode(createdResource); this.node = this.collection.createStoredProcedureNode(createdResource);
this.props.container.tabsManager.openedTabs()[ this.props.scriptTabBaseInstance.node = this.node;
this.props.container.tabsManager.openedTabs().length - 1 useTabs.getState().updateTab(this.props.scriptTabBaseInstance);
].node = this.node;
this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
this.setState({ this.setState({

View File

@@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable"; import { useObservable } from "../../hooks/useObservable";
import { useTabs } from "../../hooks/useTabs";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
type Tab = TabsBase | (TabsBase & { render: () => JSX.Element }); type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => ( export const Tabs = (): JSX.Element => {
<div className="tabsManagerContainer"> const { openedTabs, activeTab } = useTabs();
<div id="content" className="flexContainer hideOverflows"> return (
<div className="nav-tabs-margin"> <div className="tabsManagerContainer">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist"> <div id="content" className="flexContainer hideOverflows">
{tabs.map((tab) => ( <div className="nav-tabs-margin">
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} /> <ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{openedTabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</ul>
</div>
<div className="tabPanesContainer">
{openedTabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))} ))}
</ul> </div>
</div>
<div className="tabPanesContainer">
{tabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</div> </div>
</div> </div>
</div> );
); };
function TabNav({ tab, active }: { tab: Tab; active: boolean }) { function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);

View File

@@ -4,6 +4,7 @@ import * as ThemeUtility from "../../Common/ThemeUtility";
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 { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
@@ -11,7 +12,6 @@ import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel { export default class TabsBase extends WaitsForTemplateViewModel {
private static id = 0; private static id = 0;
@@ -28,7 +28,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public isExecutionError = ko.observable(false); public isExecutionError = ko.observable(false);
public isExecuting = ko.observable(false); public isExecuting = ko.observable(false);
public pendingNotification?: ko.Observable<DataModels.Notification>; public pendingNotification?: ko.Observable<DataModels.Notification>;
public manager?: TabsManager;
protected _theme: string; protected _theme: string;
public onLoadStartKey: number; public onLoadStartKey: number;
@@ -60,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
} }
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
this.manager?.closeTab(this); useTabs.getState().closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
tabName: this.constructor.name, tabName: this.constructor.name,
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
@@ -70,7 +69,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
} }
public onTabClick(): void { public onTabClick(): void {
this.manager?.activateTab(this); useTabs.getState().activateTab(this);
} }
protected updateSelectedNode(): void { protected updateSelectedNode(): void {
@@ -105,7 +104,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
/** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */ /** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
public isActive() { public isActive() {
return this === this.manager?.activeTab(); return this === useTabs.getState().activeTab;
} }
public onActivate(): void { public onActivate(): void {

View File

@@ -1,54 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "./TabsBase";
export class TabsManager {
public openedTabs = ko.observableArray<TabsBase>([]);
public activeTab = ko.observable<TabsBase>();
public activateNewTab(tab: TabsBase): void {
this.openedTabs.push(tab);
this.activateTab(tab);
}
public activateTab(tab: TabsBase): void {
if (this.openedTabs().includes(tab)) {
tab.manager = this;
this.activeTab(tab);
tab.onActivate();
}
}
public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] {
return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab)));
}
public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate();
}
public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void {
this.openedTabs()
.filter(comparator)
.forEach((tab) => tab.onCloseTabButtonClick());
}
public closeTab(tab: TabsBase): void {
const tabIndex = this.openedTabs().indexOf(tab);
if (tabIndex !== -1) {
this.openedTabs.remove(tab);
tab.manager = undefined;
if (this.openedTabs().length === 0) {
this.activeTab(undefined);
}
if (tab === this.activeTab()) {
const tabToTheRight = this.openedTabs()[tabIndex];
const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1];
this.activateTab(tabToTheRight ?? lastOpenTab);
}
}
}
}

View File

@@ -8,6 +8,7 @@ import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent"; import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
export interface TerminalTabOptions extends ViewModels.TabOptions { export interface TerminalTabOptions extends ViewModels.TabOptions {
@@ -54,8 +55,8 @@ export default class TerminalTab extends TabsBase {
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => { this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if ( if (
this.isTemplateReady() && this.isTemplateReady() &&
this.container.isNotebookEnabled() && useNotebook.getState().isNotebookEnabled &&
this.container.notebookServerInfo().notebookServerEndpoint useNotebook.getState().notebookServerInfo?.notebookServerEndpoint
) { ) {
return true; return true;
} }
@@ -95,7 +96,7 @@ export default class TerminalTab extends TabsBase {
throw new Error(`Terminal kind: ${options.kind} not supported`); throw new Error(`Terminal kind: ${options.kind} not supported`);
} }
const info: DataModels.NotebookWorkspaceConnectionInfo = options.container.notebookServerInfo(); const info: DataModels.NotebookWorkspaceConnectionInfo = useNotebook.getState().notebookServerInfo;
return { return {
authToken: info.authToken, authToken: info.authToken,
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`, notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,

View File

@@ -1,23 +1,19 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer"; import { container } from "../Controls/Settings/TestUtils";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
import { container } from "./../Controls/Settings/TestUtils";
import DocumentsTab from "./DocumentsTab"; import DocumentsTab from "./DocumentsTab";
import { NewQueryTab } from "./QueryTab/QueryTab"; import { NewQueryTab } from "./QueryTab/QueryTab";
import { TabsManager } from "./TabsManager";
describe("Tabs manager tests", () => { describe("useTabs tests", () => {
let tabsManager: TabsManager;
let explorer: Explorer;
let database: ViewModels.Database; let database: ViewModels.Database;
let collection: ViewModels.Collection; let collection: ViewModels.Collection;
let queryTab: NewQueryTab; let queryTab: NewQueryTab;
let documentsTab: DocumentsTab; let documentsTab: DocumentsTab;
beforeEach(() => { beforeEach(() => {
explorer = new Explorer();
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
id: "test", id: "test",
@@ -30,7 +26,6 @@ describe("Tabs manager tests", () => {
}); });
database = { database = {
container: explorer,
id: ko.observable<string>("test"), id: ko.observable<string>("test"),
isDatabaseShared: () => false, isDatabaseShared: () => false,
} as ViewModels.Database; } as ViewModels.Database;
@@ -38,7 +33,6 @@ describe("Tabs manager tests", () => {
database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>(); database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
collection = { collection = {
container: explorer,
databaseId: "test", databaseId: "test",
id: ko.observable<string>("test"), id: ko.observable<string>("test"),
} as ViewModels.Collection; } as ViewModels.Collection;
@@ -76,63 +70,70 @@ describe("Tabs manager tests", () => {
documentsTab.tabId = "2"; documentsTab.tabId = "2";
}); });
beforeEach(() => (tabsManager = new TabsManager())); beforeEach(() => useTabs.setState({ openedTabs: [], activeTab: undefined }));
it("open new tabs", () => { it("open new tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab } = useTabs.getState();
expect(tabsManager.openedTabs().length).toBe(1); activateNewTab(queryTab);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab); let tabsState = useTabs.getState();
expect(tabsManager.activeTab()).toEqual(queryTab); expect(tabsState.openedTabs.length).toBe(1);
expect(tabsState.openedTabs[0]).toEqual(queryTab);
expect(tabsState.activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true); expect(queryTab.isActive()).toBe(true);
tabsManager.activateNewTab(documentsTab); activateNewTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(2); tabsState = useTabs.getState();
expect(tabsManager.openedTabs()[1]).toEqual(documentsTab); expect(tabsState.openedTabs.length).toBe(2);
expect(tabsManager.activeTab()).toEqual(documentsTab); expect(tabsState.openedTabs[1]).toEqual(documentsTab);
expect(tabsState.activeTab).toEqual(documentsTab);
expect(queryTab.isActive()).toBe(false); expect(queryTab.isActive()).toBe(false);
expect(documentsTab.isActive()).toBe(true); expect(documentsTab.isActive()).toBe(true);
}); });
it("open existing tabs", () => { it("open existing tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab, activateTab } = useTabs.getState();
tabsManager.activateNewTab(documentsTab); activateNewTab(queryTab);
tabsManager.activateTab(queryTab); activateNewTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(2); activateTab(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
const { openedTabs, activeTab } = useTabs.getState();
expect(openedTabs.length).toBe(2);
expect(activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true); expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false); expect(documentsTab.isActive()).toBe(false);
}); });
it("get tabs", () => { it("get tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab, getTabs } = useTabs.getState();
tabsManager.activateNewTab(documentsTab); activateNewTab(queryTab);
activateNewTab(documentsTab);
const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query); const queryTabs = getTabs(ViewModels.CollectionTabKind.Query);
expect(queryTabs.length).toBe(1); expect(queryTabs.length).toBe(1);
expect(queryTabs[0]).toEqual(queryTab); expect(queryTabs[0]).toEqual(queryTab);
const documentsTabs = tabsManager.getTabs( const documentsTabs = getTabs(ViewModels.CollectionTabKind.Documents, (tab) => tab.tabId === documentsTab.tabId);
ViewModels.CollectionTabKind.Documents,
(tab) => tab.tabId === documentsTab.tabId
);
expect(documentsTabs.length).toBe(1); expect(documentsTabs.length).toBe(1);
expect(documentsTabs[0]).toEqual(documentsTab); expect(documentsTabs[0]).toEqual(documentsTab);
}); });
it("close tabs", () => { it("close tabs", () => {
tabsManager.activateNewTab(queryTab); const { activateNewTab, closeTab, closeTabsByComparator } = useTabs.getState();
tabsManager.activateNewTab(documentsTab); activateNewTab(queryTab);
activateNewTab(documentsTab);
closeTab(documentsTab);
tabsManager.closeTab(documentsTab); let tabsState = useTabs.getState();
expect(tabsManager.openedTabs().length).toBe(1); expect(tabsState.openedTabs.length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab); expect(tabsState.openedTabs[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab); expect(tabsState.activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true); expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false); expect(documentsTab.isActive()).toBe(false);
tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId); closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
expect(tabsManager.openedTabs().length).toBe(0); tabsState = useTabs.getState();
expect(tabsManager.activeTab()).toEqual(undefined); expect(tabsState.openedTabs.length).toBe(0);
expect(tabsState.activeTab).toEqual(undefined);
expect(queryTab.isActive()).toBe(false); expect(queryTab.isActive()).toBe(false);
}); });
}); });

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

@@ -15,6 +15,7 @@ import { fetchPortalNotifications } from "../../Common/PortalNotifications";
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 { UploadDetailsRecord } from "../../Contracts/ViewModels"; import { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
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";
@@ -31,7 +32,7 @@ import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab"; import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab";
import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab"; import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab"; import { NewQueryTablesTab } from "../Tabs/QueryTablesTab/NewQueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -239,9 +240,11 @@ export default class Collection implements ViewModels.Collection {
this.expandCollection(); this.expandCollection();
} }
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
this.container.tabsManager.refreshActiveTab( useTabs
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getState()
); .refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
} }
public collapseCollection() { public collapseCollection() {
@@ -288,14 +291,16 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( const documentsTabs: DocumentsTab[] = useTabs
ViewModels.CollectionTabKind.Documents, .getState()
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getTabs(
) as DocumentsTab[]; ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as DocumentsTab[];
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
if (documentsTab) { if (documentsTab) {
this.container.tabsManager.activateTab(documentsTab); useTabs.getState().activateTab(documentsTab);
} else { } else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@@ -317,7 +322,7 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); });
this.container.tabsManager.activateNewTab(documentsTab); useTabs.getState().activateNewTab(documentsTab);
} }
} }
@@ -333,14 +338,16 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const conflictsTabs: ConflictsTab[] = this.container.tabsManager.getTabs( const conflictsTabs: ConflictsTab[] = useTabs
ViewModels.CollectionTabKind.Conflicts, .getState()
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getTabs(
) as ConflictsTab[]; ViewModels.CollectionTabKind.Conflicts,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as ConflictsTab[];
let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0]; let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0];
if (conflictsTab) { if (conflictsTab) {
this.container.tabsManager.activateTab(conflictsTab); useTabs.getState().activateTab(conflictsTab);
} else { } else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@@ -362,7 +369,7 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); });
this.container.tabsManager.activateNewTab(conflictsTab); useTabs.getState().activateNewTab(conflictsTab);
} }
} }
@@ -384,14 +391,16 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
const queryTablesTabs: QueryTablesTab[] = this.container.tabsManager.getTabs( const queryTablesTabs: NewQueryTablesTab[] = useTabs
ViewModels.CollectionTabKind.QueryTables, .getState()
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getTabs(
) as QueryTablesTab[]; ViewModels.CollectionTabKind.QueryTables,
let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0]; (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as NewQueryTablesTab[];
let queryTablesTab: NewQueryTablesTab = queryTablesTabs && queryTablesTabs[0];
if (queryTablesTab) { if (queryTablesTab) {
this.container.tabsManager.activateTab(queryTablesTab); useTabs.getState().activateTab(queryTablesTab);
} else { } else {
this.documentIds([]); this.documentIds([]);
let title = `Entities`; let title = `Entities`;
@@ -406,16 +415,16 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title, tabTitle: title,
}); });
queryTablesTab = new QueryTablesTab({ queryTablesTab = new NewQueryTablesTab({
tabKind: ViewModels.CollectionTabKind.QueryTables, tabKind: ViewModels.CollectionTabKind.QueryTables,
title: title, title: title,
tabPath: "", tabPath: "",
collection: this, collection: this,
node: this, node: this,
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); }, { container: this.container });
this.container.tabsManager.activateNewTab(queryTablesTab); useTabs.getState().activateNewTab(queryTablesTab);
} }
} }
@@ -431,14 +440,16 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const graphTabs: GraphTab[] = this.container.tabsManager.getTabs( const graphTabs: GraphTab[] = useTabs
ViewModels.CollectionTabKind.Graph, .getState()
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getTabs(
) as GraphTab[]; ViewModels.CollectionTabKind.Graph,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as GraphTab[];
let graphTab: GraphTab = graphTabs && graphTabs[0]; let graphTab: GraphTab = graphTabs && graphTabs[0];
if (graphTab) { if (graphTab) {
this.container.tabsManager.activateTab(graphTab); useTabs.getState().activateTab(graphTab);
} else { } else {
this.documentIds([]); this.documentIds([]);
const title = "Graph"; const title = "Graph";
@@ -466,7 +477,7 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); });
this.container.tabsManager.activateNewTab(graphTab); useTabs.getState().activateNewTab(graphTab);
} }
} }
@@ -482,14 +493,16 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs( const mongoDocumentsTabs: MongoDocumentsTab[] = useTabs
ViewModels.CollectionTabKind.Documents, .getState()
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getTabs(
) as MongoDocumentsTab[]; ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as MongoDocumentsTab[];
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0]; let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
if (mongoDocumentsTab) { if (mongoDocumentsTab) {
this.container.tabsManager.activateTab(mongoDocumentsTab); useTabs.getState().activateTab(mongoDocumentsTab);
} else { } else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@@ -510,7 +523,7 @@ export default class Collection implements ViewModels.Collection {
node: this, node: this,
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); });
this.container.tabsManager.activateNewTab(mongoDocumentsTab); useTabs.getState().activateNewTab(mongoDocumentsTab);
} }
}; };
@@ -525,13 +538,13 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
for (const tab of this.container.tabsManager.openedTabs()) { for (const tab of useTabs.getState().openedTabs) {
if ( if (
tab instanceof SchemaAnalyzerTab && tab instanceof SchemaAnalyzerTab &&
tab.collection?.databaseId === this.databaseId && tab.collection?.databaseId === this.databaseId &&
tab.collection?.id() === this.id() tab.collection?.id() === this.id()
) { ) {
return this.container.tabsManager.activateTab(tab); return useTabs.getState().activateTab(tab);
} }
} }
@@ -542,7 +555,7 @@ export default class Collection implements ViewModels.Collection {
tabTitle: "Schema", tabTitle: "Schema",
}); });
this.documentIds([]); this.documentIds([]);
this.container.tabsManager.activateNewTab( useTabs.getState().activateNewTab(
new SchemaAnalyzerTab({ new SchemaAnalyzerTab({
account: userContext.databaseAccount, account: userContext.databaseAccount,
masterKey: userContext.masterKey || "", masterKey: userContext.masterKey || "",
@@ -559,6 +572,7 @@ export default class Collection implements ViewModels.Collection {
public onSettingsClick = async (): Promise<void> => { public onSettingsClick = async (): Promise<void> => {
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",
@@ -570,12 +584,9 @@ export default class Collection implements ViewModels.Collection {
}); });
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const matchingTabs = this.container.tabsManager.getTabs( const matchingTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.CollectionSettingsV2, (tab) => {
ViewModels.CollectionTabKind.CollectionSettingsV2, return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
(tab) => { });
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
}
);
const traceStartData = { const traceStartData = {
databaseName: this.databaseId, databaseName: this.databaseId,
@@ -607,15 +618,15 @@ export default class Collection implements ViewModels.Collection {
settingsTabOptions.onLoadStartKey = startKey; settingsTabOptions.onLoadStartKey = startKey;
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2; settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2;
settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions); settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions);
this.container.tabsManager.activateNewTab(settingsTabV2); useTabs.getState().activateNewTab(settingsTabV2);
} else { } else {
this.container.tabsManager.activateTab(settingsTabV2); useTabs.getState().activateTab(settingsTabV2);
} }
}; };
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source; const collection: ViewModels.Collection = source.collection || source;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id; const title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId, databaseName: this.databaseId,
@@ -625,7 +636,7 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title, tabTitle: title,
}); });
this.container.tabsManager.activateNewTab( useTabs.getState().activateNewTab(
new NewQueryTab( new NewQueryTab(
{ {
tabKind: ViewModels.CollectionTabKind.Query, tabKind: ViewModels.CollectionTabKind.Query,
@@ -644,7 +655,7 @@ export default class Collection implements ViewModels.Collection {
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) { public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source; const collection: ViewModels.Collection = source.collection || source;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id; const title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
@@ -671,11 +682,11 @@ export default class Collection implements ViewModels.Collection {
} }
); );
this.container.tabsManager.activateNewTab(newMongoQueryTab); useTabs.getState().activateNewTab(newMongoQueryTab);
} }
public onNewGraphClick() { public onNewGraphClick() {
const id: number = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Graph).length + 1; const id: number = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Graph).length + 1;
const title: string = "Graph Query " + id; const title: string = "Graph Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
@@ -701,13 +712,11 @@ export default class Collection implements ViewModels.Collection {
onLoadStartKey: startKey, onLoadStartKey: startKey,
}); });
this.container.tabsManager.activateNewTab(graphTab); useTabs.getState().activateNewTab(graphTab);
} }
public onNewMongoShellClick() { public onNewMongoShellClick() {
const mongoShellTabs = this.container.tabsManager.getTabs( const mongoShellTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.MongoShell) as NewMongoShellTab[];
ViewModels.CollectionTabKind.MongoShell
) as NewMongoShellTab[];
let index = 1; let index = 1;
if (mongoShellTabs.length > 0) { if (mongoShellTabs.length > 0) {
@@ -728,15 +737,15 @@ export default class Collection implements ViewModels.Collection {
} }
); );
this.container.tabsManager.activateNewTab(mongoShellTab); useTabs.getState().activateNewTab(mongoShellTab);
} }
public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) { public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
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) {
@@ -786,9 +795,11 @@ export default class Collection implements ViewModels.Collection {
} else { } else {
this.expandStoredProcedures(); this.expandStoredProcedures();
} }
this.container.tabsManager.refreshActiveTab( useTabs
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getState()
); .refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
} }
public expandStoredProcedures() { public expandStoredProcedures() {
@@ -845,9 +856,11 @@ export default class Collection implements ViewModels.Collection {
} else { } else {
this.expandUserDefinedFunctions(); this.expandUserDefinedFunctions();
} }
this.container.tabsManager.refreshActiveTab( useTabs
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getState()
); .refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
} }
public expandUserDefinedFunctions() { public expandUserDefinedFunctions() {
@@ -904,9 +917,11 @@ export default class Collection implements ViewModels.Collection {
} else { } else {
this.expandTriggers(); this.expandTriggers();
} }
this.container.tabsManager.refreshActiveTab( useTabs
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() .getState()
); .refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
} }
public expandTriggers() { public expandTriggers() {

View File

@@ -1,7 +1,7 @@
import * as ko from "knockout";
import { HttpStatusCodes } from "../../Common/Constants"; import { HttpStatusCodes } from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { JunoClient } from "../../Juno/JunoClient"; import { JunoClient } from "../../Juno/JunoClient";
import { Features } from "../../Platform/Hosted/extractFeatures";
import { updateUserContext, userContext } from "../../UserContext"; import { updateUserContext, userContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import Database from "./Database"; import Database from "./Database";
@@ -31,11 +31,10 @@ updateUserContext({
describe("Add Schema", () => { describe("Add Schema", () => {
it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => { it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => {
const collection: DataModels.Collection = {} as DataModels.Collection; const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection;
collection.analyticalStorageTtl = undefined; collection.analyticalStorageTtl = undefined;
const database = new Database(createMockContainer(), { id: "fakeId" }); const database = new Database(createMockContainer(), collection);
database.container = createMockContainer(); database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => false);
database.junoClient = new JunoClient(); database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn(); database.junoClient.requestSchema = jest.fn();
@@ -47,12 +46,16 @@ describe("Add Schema", () => {
}); });
it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => { it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => {
const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection; const collection: DataModels.Collection = {} as DataModels.Collection;
collection.analyticalStorageTtl = 0; collection.analyticalStorageTtl = 0;
const database = new Database(createMockContainer(), {}); const database = new Database(createMockContainer(), collection);
database.container = createMockContainer(); database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => true); updateUserContext({
features: {
enableSchema: true,
} as Features,
});
database.junoClient = new JunoClient(); database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn(); database.junoClient.requestSchema = jest.fn();

Some files were not shown because too many files have changed in this diff Show More