Compare commits

..

95 Commits

Author SHA1 Message Date
sunilyadav840
6b5e2618dd fixed tsstrict of InputTypeHeadComponent.tsx 2021-08-26 16:02:29 +05:30
victor-meng
8eeda41021 Move synapse link out of advanced section in add collection panel (#989) 2021-08-19 12:18:21 -07:00
Hardikkumar Nai
960cd9fc55 Resolve ESlint Controls (#990) 2021-08-19 12:16:35 -05:00
Hardikkumar Nai
9ec0ac9f54 Resolve ESLint Contracts (#986) 2021-08-19 12:15:52 -05:00
Steve Faulkner
b66aeb814a Polyfill Buffer (#988) 2021-08-18 21:12:40 -05:00
Tanuj Mittal
410f582378 Fix notebooksTemporarilyDown feature flag (#983) 2021-08-17 07:27:41 +05:30
Hardikkumar Nai
678ca51c77 Update to Webpack 5 (#964)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-08-16 15:44:40 -05:00
Steve Faulkner
2dfd90ca0f Disable Notebooks Test (#980) 2021-08-16 11:35:21 -05:00
Srinath Narayanan
4110be10bd Removing author from publish notebook payload (#966)
* removing author from publish payload

* fixed failing tests
2021-08-15 13:15:30 +05:30
Srinath Narayanan
6e55e397b3 Changes for Disabling notebook related features (#979)
* notebook removal changes

* fixed failing tests
2021-08-14 12:09:22 +05:30
Tanuj Mittal
24d0a16123 Disable notebooks temporarily (#978)
* Disable notebooks temporarily

* Updates
2021-08-14 07:03:58 +05:30
Tanuj Mittal
f8ac36962b Add Security Warning Bar for untrusted notebooks (#970)
* Add Security Warning Bar for untrusted notebooks

* Update

* Another update

* Add a snapshot test

* UX updates

* More updates

* Add tests

* Update test snapshot

* Update string

* Update security message
2021-08-10 23:54:26 +05:30
Sunil Kumar Yadav
51f3f9a718 Move inputTypeahead to react (#946)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-08-04 09:46:18 -05:00
victor-meng
ee4404c439 Fix enable synapse link error (#918)
* Fix enable synapse error

* Default all ARM requests to JSON

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-08-02 21:11:42 -05:00
Steve Faulkner
b65456f754 Fix bug in Dialog State store (#969) 2021-08-02 20:54:30 -05:00
victor-meng
56699ccb1b Fix new resource tree (#962) 2021-07-30 16:23:36 -07:00
victor-meng
042f980b89 Replace window.confirm and window.alert with modal dialog (#965) 2021-07-30 10:27:27 -07:00
t-tarabhatia
7e0c4b7290 Add changes to Partition Key A/B Test (#954) 2021-07-29 08:48:03 -05:00
victor-meng
a66fc06dad Turn off react resource tree (#963) 2021-07-28 21:29:45 -07:00
victor-meng
a8bc821dec Add initializeGitHubRepos function in useNotebooks store (#957) 2021-07-23 18:44:24 -07:00
victor-meng
1394aae944 Fix validateCollectionId for new tables account (#958) 2021-07-23 18:44:16 -07:00
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
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
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
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
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
Hardikkumar Nai
a1d5648bbc Remove Explorer.openAddCollectionPane (#905)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-27 23:39:28 -05:00
Steve Faulkner
447db01647 TypeScript 4.3 (#910) 2021-06-24 19:14:26 -05:00
victor-meng
4d2a6999d4 Move selectedNode to zustand (#903) 2021-06-24 13:56:33 -05:00
Hardikkumar Nai
a7239c7579 Remove Explorer.openDeleteDatabaseConfirmationPane (#908)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-24 00:21:27 -05:00
Jordi Bunster
c1d4008895 Bypass ko<->React adapter in SettingsTab (#732)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-24 00:07:10 -05:00
victor-meng
59655eed5f Remove route handlers (#909)
* Remove tab handlers

* Fix tests
2021-06-23 23:54:37 -05:00
Laurent Nguyen
6b35ab03f2 Switch notebook editor from Monaco back to Code Mirror (#901) 2021-06-23 14:05:52 -05:00
victor-meng
738a02a0f3 Replace subscriptions in resource tree with useDatabases hook (#904) 2021-06-23 13:45:29 -05:00
vaidankarswapnil
b392bed1b0 Migrate Stored Procedure tab to react (#894)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-23 11:49:57 -05:00
vaidankarswapnil
f255387ccd Migrate Mongo Shell tab to React (#900) 2021-06-22 23:39:58 -05:00
Steve Faulkner
f9bd12eaa6 Fix for QueryTab Load More (#907) 2021-06-22 15:21:58 -05:00
Steve Faulkner
39215dc4de Remove unused GitHubReposComponentAdapter (#902) 2021-06-18 13:55:54 -05:00
victor-meng
96e6bba38b Move databases to zustand (#898) 2021-06-18 13:25:08 -05:00
Sunil Kumar Yadav
c9fa44f6f4 Fix UDF placeholder text (#899) 2021-06-17 10:54:40 -05:00
Sunil Kumar Yadav
05f59307c2 Migrate userDefinedFunctionTab to React (#860) 2021-06-16 19:09:22 -05:00
vaidankarswapnil
1d449e5b52 Implemented spinner in EditorReact component (#897) 2021-06-16 19:02:33 -05:00
Steve Faulkner
6f68c75257 Allow dynamic MSAL Authority (#896) 2021-06-16 09:13:11 -05:00
Sunil Kumar Yadav
914c372f5b Migrate Trigger tab to React (#855)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-15 21:53:50 -05:00
Steve Faulkner
af71a96d54 Add tree and treeitem roles to Resource Tree (#895)
* Add tree and treeitem roles to Resource Tree

* Updates
2021-06-15 14:52:21 -05:00
Steve Faulkner
239c7edf7b Fix Incorrect Image Links (#892) 2021-06-14 20:52:02 -05:00
Hardikkumar Nai
0c6324a4c1 Remove Explorer.openTableSelectQueryPanel (#881)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-14 16:57:47 -05:00
Hardikkumar Nai
615bfeaf48 Remove Explorer openEditTableEntityPanel and openAddTableEntityPanel (#887)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-14 16:34:37 -05:00
Steve Faulkner
3bc58a80e4 Remove Explorer.isHostedDataExplorerEnabled (#890) 2021-06-14 14:46:14 -05:00
vaidankarswapnil
5da9724deb Migrate Mongo Query Tab to React(#854)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-14 14:23:19 -05:00
vaidankarswapnil
999fad3bad Migrate Query Tab to React (#852)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-06-14 14:15:13 -05:00
victor-meng
baa3252ba8 Properly style CassandraAddCollectionPane (#862) 2021-06-11 14:25:05 -07:00
Hardikkumar Nai
959d34d88d Remove Explorer.openCassandraAddCollectionPane (#888) 2021-06-10 23:41:24 -05:00
Steve Faulkner
ce3c2fcfb6 Remove settings component container dependency (#886) 2021-06-10 19:29:41 -05:00
Steve Faulkner
0a1a2bf421 Remove QueryUtils.queryAll and fix test (#885) 2021-06-10 19:29:19 -05:00
victor-meng
b0bbeb188a Fix query tab/pane related issues (#879) 2021-06-10 17:17:11 -07:00
Steve Faulkner
fc9f287d0a Remove Explorer.configure (#884) 2021-06-10 19:02:07 -05:00
Steve Faulkner
006230262c Remvoe Explorer.isServerlessEnabled (#883) 2021-06-10 18:16:43 -05:00
Steve Faulkner
6de77a4fba Update strict mode files (#882) 2021-06-10 12:44:57 -05:00
Hardikkumar Nai
c980af9a5c Remove method Explorer.openSettingPane (#880) 2021-06-10 08:09:38 -05:00
Steve Faulkner
c632342a43 Remove window.jQuery (#878) 2021-06-09 16:03:42 -05:00
Steve Faulkner
bcc9f8dd32 Migrate remaining notification console methods to zustand (#873) 2021-06-09 15:11:12 -05:00
Steve Faulkner
fc9f4c5583 Migrate notebooks workspaces to generated clients (#876) 2021-06-09 15:08:10 -05:00
Steve Faulkner
8f6cac3d35 Remove Unused Splitter (#874) 2021-06-09 08:35:25 -05:00
Steve Faulkner
2c296ede35 Remove unused KO->React Adapters (#863) 2021-06-07 23:14:05 -05:00
victor-meng
16b09df5fa Hide provision throughput checkbox for serverless account (#861) 2021-06-07 23:05:41 -05:00
Srinath Narayanan
ee60f61cfe Mongo tabs UX fixes (#851)
* Fixed mongo tabs UX

* changed logic for new tab index

* moved index to tabs base

* removed getIndex method
2021-06-08 00:17:55 +05:30
304 changed files with 14971 additions and 18747 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

View File

@@ -21,16 +21,8 @@ src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts
src/Common/QueriesClient.ts
src/Common/Splitter.ts
src/Config.ts
src/Contracts/ActionContracts.ts
src/Contracts/DataModels.ts
src/Contracts/Diagnostics.ts
src/Contracts/ExplorerContracts.ts
src/Contracts/Versions.ts
src/Contracts/ViewModels.ts
src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts
src/Controls/Heatmap/HeatmapDatatypes.ts
src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts
src/Definitions/globals.d.ts
@@ -44,35 +36,14 @@ src/Definitions/png.d.ts
src/Definitions/svg.d.ts
src/Explorer/ComponentRegisterer.test.ts
src/Explorer/ComponentRegisterer.ts
src/Explorer/ContextMenuButtonFactory.ts
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
src/Explorer/Controls/DynamicList/DynamicList.test.ts
src/Explorer/Controls/DynamicList/DynamicListComponent.ts
src/Explorer/Controls/Editor/EditorComponent.ts
src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
src/Explorer/Controls/Toolbar/IToolbarAction.ts
src/Explorer/Controls/Toolbar/IToolbarDisplayable.ts
src/Explorer/Controls/Toolbar/IToolbarDropDown.ts
src/Explorer/Controls/Toolbar/IToolbarItem.ts
src/Explorer/Controls/Toolbar/IToolbarSeperator.ts
src/Explorer/Controls/Toolbar/IToolbarToggle.ts
src/Explorer/Controls/Toolbar/KeyCodes.ts
src/Explorer/Controls/Toolbar/Toolbar.ts
src/Explorer/Controls/Toolbar/ToolbarAction.ts
src/Explorer/Controls/Toolbar/ToolbarDropDown.ts
src/Explorer/Controls/Toolbar/ToolbarToggle.ts
src/Explorer/Controls/Toolbar/Utilities.ts
src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
src/Explorer/DataSamples/ContainerSampleGenerator.ts
src/Explorer/DataSamples/DataSamplesUtil.test.ts
src/Explorer/DataSamples/DataSamplesUtil.ts
src/Explorer/Explorer.tsx
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
@@ -84,11 +55,6 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.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/MostRecentActivity/MostRecentActivity.ts
src/Explorer/Notebook/NotebookClientV2.ts
@@ -105,20 +71,11 @@ src/Explorer/Notebook/NotebookContainerClient.ts
src/Explorer/Notebook/NotebookContentClient.ts
src/Explorer/Notebook/NotebookContentItem.ts
src/Explorer/Notebook/NotebookUtil.ts
src/Explorer/OpenActions.test.ts
src/Explorer/OpenActions.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/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
src/Explorer/Tables/DataTable/DataTableBuilder.ts
@@ -135,7 +92,6 @@ src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryViewModel.ts
src/Explorer/Tables/TableDataClient.ts
src/Explorer/Tables/TableEntityProcessor.ts
src/Explorer/Tables/Utilities.ts
@@ -145,170 +101,67 @@ src/Explorer/Tabs/DocumentsTab.test.ts
src/Explorer/Tabs/DocumentsTab.ts
src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/MongoQueryTab.ts
src/Explorer/Tabs/MongoShellTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts
src/Explorer/Tabs/QueryTablesTab.ts
src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/StoredProcedureTab.ts
src/Explorer/Tabs/TabComponents.ts
src/Explorer/Tabs/TabsBase.ts
src/Explorer/Tabs/TriggerTab.ts
src/Explorer/Tabs/UserDefinedFunctionTab.ts
src/Explorer/Tree/AccessibleVerticalList.ts
src/Explorer/Tree/Collection.test.ts
src/Explorer/Tree/Collection.ts
src/Explorer/Tree/ConflictId.ts
src/Explorer/Tree/Database.ts
src/Explorer/Tree/DocumentId.ts
src/Explorer/Tree/ObjectId.ts
src/Explorer/Tree/ResourceTokenCollection.ts
src/Explorer/Tree/StoredProcedure.ts
src/Explorer/Tree/TreeComponents.ts
src/Explorer/Tree/Trigger.ts
src/Explorer/Tree/UserDefinedFunction.ts
src/Explorer/WaitsForTemplateViewModel.ts
src/GitHub/GitHubClient.test.ts
src/GitHub/GitHubClient.ts
src/GitHub/GitHubConnector.ts
src/GitHub/GitHubContentProvider.test.ts
src/GitHub/GitHubContentProvider.ts
src/GitHub/GitHubOAuthService.ts
src/HostedExplorer.ts
src/Index.ts
src/Juno/JunoClient.test.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/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/ResourceProvider/IResourceProviderClient.test.ts
src/ResourceProvider/IResourceProviderClient.ts
src/ResourceProvider/ResourceProviderClient.ts
src/ResourceProvider/ResourceProviderClientFactory.ts
src/RouteHandlers/RouteHandler.ts
src/RouteHandlers/TabRouteHandler.test.ts
src/RouteHandlers/TabRouteHandler.ts
src/Shared/Constants.ts
src/Shared/DefaultExperienceUtility.test.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/SparkClusterManager/ArcadiaResourceManager.ts
src/SparkClusterManager/SparkClusterManager.ts
src/Terminal/JupyterLabAppFactory.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/global.d.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/Notebook/NotebookTerminalComponent.test.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/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/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.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.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/NotebookComponentBootstrapper.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/NotebookRenderer/AzureTheme.tsx
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.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/hijack-scroll/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/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/ResourceTreeAdapterForResourceToken.tsx
src/GalleryViewer/Cards/GalleryCardComponent.tsx
src/GalleryViewer/GalleryViewer.tsx
src/GalleryViewer/GalleryViewerComponent.tsx
__mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
src/Explorer/Tree/ResourceTree.tsx

View File

@@ -143,7 +143,7 @@ jobs:
- ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
- ./test/notebooks/upload.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
- ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts
steps:

View File

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

View File

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 842 B

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,6 +9,7 @@
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@GrayScale: "grayscale()";
@xSmallFontSize: 4px;
@smallFontSize: 8px;

View File

@@ -724,45 +724,24 @@ execute-sproc-params-pane {
.results-container,
.errors-container {
padding: @MediumSpace 0px 0px @MediumSpace;
height: 100%;
.flex-display();
.flex-direction();
overflow: hidden;
.toggles {
height: @ToggleHeight;
width: @ToggleWidth;
margin-left: @MediumSpace;
&:focus {
.focus();
}
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
.enterInputParameters {
padding: @LargeSpace @MediumSpace;
}
div[role="tabpanel"] {
height: 100%;
padding-bottom: 50px;
}
}
.errors-container {
padding-left: (2 * @MediumSpace);
padding: @MediumSpace 0px 0px @MediumSpace;
.errors-header {
font-weight: 700;
font-size: @DefaultFontSize;
@@ -3088,4 +3067,11 @@ settings-pane {
.hiddenMain {
display: none;
height: 0px;
}
}
.spinner {
width: 100%;
position: absolute;
z-index: 1;
background: white;
height: 100%;
}

View File

@@ -200,4 +200,12 @@
.migration:disabled {
background-color: #ccc;
}
.trigger-field {
width: 40%;
margin-top: 10px
}
.trigger-form {
padding: 10px 30px 10px 30px;
}

View File

@@ -2,6 +2,7 @@
.dataResourceTree {
margin-left: @MediumSpace;
overflow: auto;
.databaseHeader {
font-size: 14px;
@@ -18,6 +19,10 @@
.notebookHeader {
font-size: 12px;
}
.clickDisabled {
pointer-events: none;
}
}

View File

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

4802
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"@nteract/data-explorer": "8.0.3",
"@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.2",
"@nteract/editor": "10.1.12",
"@nteract/fixtures": "2.3.0",
"@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0",
@@ -42,14 +42,15 @@
"@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3",
"@testing-library/jest-dom": "5.11.9",
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",
"clean-webpack-plugin": "0.1.19",
"clean-webpack-plugin": "3.0.0",
"clipboard-copy": "4.0.1",
"copy-webpack-plugin": "6.0.2",
"copy-webpack-plugin": "9.0.1",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "6.1.1",
@@ -80,15 +81,16 @@
"plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",
"q": "1.5.1",
"react": "16.13.1",
"react": "16.14.0",
"react-animate-height": "2.0.8",
"react-dnd": "9.4.0",
"react-dnd-html5-backend": "9.4.0",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0",
"react-dom": "16.13.1",
"react-hotkeys": "2.0.0",
"react-i18next": "11.8.5",
"react-notification-system": "0.2.17",
"react-redux": "7.1.3",
"react-splitter-layout": "4.0.0",
"redux": "4.0.4",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
@@ -96,7 +98,7 @@
"sanitize-html": "2.3.3",
"styled-components": "4.3.2",
"swr": "0.4.0",
"terser-webpack-plugin": "3.1.0",
"terser-webpack-plugin": "5.1.4",
"underscore": "1.9.1",
"utility-types": "3.10.0",
"zustand": "3.5.0"
@@ -123,12 +125,14 @@
"@types/react-dom": "17.0.3",
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",
"@types/react-splitter-layout": "3.0.1",
"@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1",
"@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0",
"@webpack-cli/serve": "1.5.2",
"babel-jest": "24.9.0",
"babel-loader": "8.1.0",
"buffer": "5.1.0",
@@ -150,44 +154,45 @@
"html-inline-css-webpack-plugin": "1.11.0",
"html-loader": "0.5.5",
"html-loader-jest": "0.2.1",
"html-webpack-plugin": "4.5.2",
"jest": "25.5.4",
"jest-canvas-mock": "2.1.0",
"html-webpack-plugin": "5.3.2",
"jest": "26.6.3",
"jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7",
"less": "3.8.1",
"less-loader": "4.1.0",
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "0.4.3",
"mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1",
"playwright": "1.10.0",
"playwright": "1.13.0",
"prettier": "2.2.1",
"process": "0.11.10",
"raw-loader": "0.5.1",
"react-dev-utils": "11.0.4",
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
"ts-loader": "6.2.2",
"ts-loader": "9.2.4",
"tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0",
"typedoc": "0.20.36",
"typescript": "4.2.4",
"typescript": "4.3.4",
"url-loader": "1.1.1",
"wait-on": "4.0.2",
"webpack": "4.46.0",
"webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "3.3.10",
"webpack-dev-server": "3.11.0"
"webpack": "5.47.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "4.7.2",
"webpack-dev-server": "3.11.2"
},
"scripts": {
"start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js",
"start": "webpack serve --mode development",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci",
"build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
"build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
"pack:prod": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode production",
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
"pack:prod": "webpack --mode production",
"pack:fast": "webpack --mode development --progress",
"copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",

View File

@@ -94,7 +94,8 @@ export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest";
public static readonly SchemaAnalyzer = "schemaanalyzer";
public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
}
export class AfecFeatures {
@@ -158,16 +159,6 @@ export class DocumentsGridMetrics {
public static DocumentEditorMaxWidthRatio: number = 0.4;
}
export class ExplorerMetrics {
public static SplitterMinWidth: number = 240;
public static SplitterMaxWidth: number = 400;
public static CollapsedResourceTreeWidth: number = 36;
}
export class SplitterMetrics {
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
}
export class Areas {
public static ResourceTree: string = "Resource Tree";
public static ContextualPane: string = "Contextual Pane";
@@ -359,6 +350,11 @@ export class Notebook {
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
}
export class SparkLibrary {

View File

@@ -1,6 +1,6 @@
import * as HeadersUtility from "./HeadersUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import * as HeadersUtility from "./HeadersUtility";
describe("Headers Utility", () => {
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {

View File

@@ -5,7 +5,6 @@ import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { updateUserContext } from "../UserContext";
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
const databaseId = "testDB";

View File

@@ -111,7 +111,7 @@ export function queryDocuments(
headers: response.headers,
};
}
errorHandling(response, "querying documents", params);
await errorHandling(response, "querying documents", params);
return undefined;
});
}
@@ -153,11 +153,11 @@ export function readDocument(
),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "reading document", params);
return await errorHandling(response, "reading document", params);
});
}
@@ -192,11 +192,11 @@ export function createDocument(
...authHeaders(),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "creating document", params);
return await errorHandling(response, "creating document", params);
});
}
@@ -238,11 +238,11 @@ export function updateDocument(
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "updating document", params);
return await errorHandling(response, "updating document", params);
});
}
@@ -278,11 +278,11 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
return undefined;
}
return errorHandling(response, "deleting document", params);
return await errorHandling(response, "deleting document", params);
});
}
@@ -325,11 +325,11 @@ export function createMongoCollectionWithProxy(
},
}
)
.then((response) => {
.then(async (response) => {
if (response.ok) {
return response.json();
}
return errorHandling(response, "creating collection", mongoParams);
return await errorHandling(response, "creating collection", mongoParams);
});
}

View File

@@ -1,19 +1,17 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId";
import { useDatabases } from "../Explorer/useDatabases";
import { userContext } from "../UserContext";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { createCollection } from "./dataAccess/createCollection";
import { createDocument } from "./dataAccess/createDocument";
import { deleteDocument } from "./dataAccess/deleteDocument";
import { queryDocuments } from "./dataAccess/queryDocuments";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
import { handleError } from "./ErrorHandlingUtils";
export class QueriesClient {
@@ -100,45 +98,35 @@ export class QueriesClient {
const options: any = { enableCrossPartitionQuery: true };
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries");
const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
const results = await queryDocuments(
SavedQueries.DatabaseName,
SavedQueries.CollectionName,
this.fetchQueriesQuery(),
options
);
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
return QueryUtils.queryAllPages(fetchQueries)
.then(
(results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
if (!document) {
return undefined;
}
const { id, resourceId, query, queryName } = document;
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id,
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
return Promise.resolve(queries);
},
(error: any) => {
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
return Promise.reject(error);
}
)
.finally(() => clearMessage());
).fetchAll();
let queries: DataModels.Query[] = _.map(results.resources, (document: DataModels.Query) => {
if (!document) {
return undefined;
}
const { id, resourceId, query, queryName } = document;
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id,
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
clearMessage();
return queries;
}
public async deleteQuery(query: DataModels.Query): Promise<void> {
@@ -189,7 +177,7 @@ export class QueriesClient {
private findQueriesCollection(): ViewModels.Collection {
const queriesDatabase: ViewModels.Database = _.find(
this.container.databases(),
useDatabases.getState().databases,
(database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName
);
if (!queriesDatabase) {

View File

@@ -2,17 +2,22 @@ import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg";
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";
export interface ResourceTreeProps {
export interface ResourceTreeContainerProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
container: Explorer;
}
export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: ResourceTreeProps): JSX.Element => {
container,
}: ResourceTreeContainerProps): JSX.Element => {
return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */}
@@ -48,9 +53,11 @@ export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
</div>
</div>
{userContext.authType === AuthType.ResourceToken ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
) : (
<ResourceTokenTree />
) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : (
<ResourceTree container={container} />
)}
</div>
{/* Collections Window - End */}

View File

@@ -1,7 +1,3 @@
import * as ko from "knockout";
import { SplitterMetrics } from "./Constants";
export enum SplitterDirection {
Horizontal = "horizontal",
Vertical = "vertical",
@@ -28,14 +24,12 @@ export class Splitter {
public lastX!: number;
public lastWidth!: number;
private isCollapsed: ko.Observable<boolean>;
private bounds: SplitterBounds;
private direction: SplitterDirection;
constructor(options: SplitterOptions) {
this.splitterId = options.splitterId;
this.leftSideId = options.leftId;
this.isCollapsed = ko.observable<boolean>(false);
this.bounds = options.bounds;
this.direction = options.direction;
this.initialize();
@@ -83,23 +77,4 @@ export class Splitter {
};
private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto");
public collapseLeft() {
this.lastX = $(this.splitter).position().left;
this.lastWidth = $(this.leftSide).width();
$(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft);
$(this.leftSide).css("width", "");
$(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible
$(this.splitter).removeClass("ui-resizable-e");
this.isCollapsed(true);
}
public expandLeft() {
$(this.splitter).addClass("ui-resizable-e");
$(this.leftSide).css("width", this.lastWidth);
$(this.splitter).css("left", this.lastX);
$(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing
$(this.leftSide).resizable("enable");
this.isCollapsed(false);
}
}

View File

@@ -1,7 +1,10 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
import ko from "knockout";
import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { Database } from "../../Contracts/ViewModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { updateUserContext } from "../../UserContext";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
@@ -23,6 +26,15 @@ describe("createCollection", () => {
} as DatabaseAccount,
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 () => {

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 { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import {
createUpdateCassandraTable,
getCassandraTable,
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import {
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 { getCollectionName } from "../../Utils/APITypeUtils";
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
@@ -59,6 +55,16 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
};
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;
switch (apiType) {
case "SQL":
@@ -77,23 +83,6 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams
};
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 resource: ARMTypes.SqlContainerResource = {
id: params.collectionId,
@@ -131,23 +120,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
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 resource: ARMTypes.MongoDBCollectionResource = {
id: params.collectionId,
@@ -189,23 +161,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
};
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 resource: ARMTypes.CassandraTableResource = {
id: params.collectionId,
@@ -233,23 +188,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams):
};
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 resource: ARMTypes.GremlinGraphResource = {
id: params.collectionId,
@@ -284,22 +222,6 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise<D
};
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 resource: ARMTypes.TableResource = {
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 { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { userContext } from "../../UserContext";
import {
createUpdateCassandraKeyspace,
getCassandraKeyspace,
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import {
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 { getDatabaseName } from "../../Utils/APITypeUtils";
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import {
CassandraKeyspaceCreateUpdateParameters,
CreateUpdateOptions,
@@ -48,6 +41,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
}
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;
switch (apiType) {
@@ -65,22 +63,6 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P
}
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 rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
@@ -101,22 +83,6 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
}
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 rpPayload: MongoDBDatabaseCreateUpdateParameters = {
properties: {
@@ -137,22 +103,6 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
}
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 rpPayload: CassandraKeyspaceCreateUpdateParameters = {
properties: {
@@ -173,22 +123,6 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
}
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 rpPayload: GremlinDatabaseCreateUpdateParameters = {
properties: {

View File

@@ -27,7 +27,6 @@ export interface ConfigContext {
hostedExplorerURL: string;
armAPIVersion?: string;
allowedJunoOrigins: string[];
enableSchemaAnalyzer: boolean;
msalRedirectURI?: string;
}
@@ -63,7 +62,6 @@ let configContext: Readonly<ConfigContext> = {
"https://tools-staging.cosmos.azure.com",
"https://localhost",
],
enableSchemaAnalyzer: false,
};
export function resetConfigContext(): void {
@@ -120,6 +118,14 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
const armAPIVersion = params.get("armAPIVersion") || "";
updateConfigContext({ armAPIVersion });
}
if (params.has("armEndpoint")) {
const ARM_ENDPOINT = params.get("armEndpoint") || "";
updateConfigContext({ ARM_ENDPOINT });
}
if (params.has("aadEndpoint")) {
const AAD_ENDPOINT = params.get("aadEndpoint") || "";
updateConfigContext({ AAD_ENDPOINT });
}
if (params.has("platform")) {
const platform = params.get("platform");
switch (platform) {

View File

@@ -9,6 +9,7 @@ export interface DatabaseAccount {
export interface DatabaseAccountExtendedProperties {
documentEndpoint?: string;
disableLocalAuth?: boolean;
tableEndpoint?: string;
gremlinEndpoint?: string;
cassandraEndpoint?: string;

View File

@@ -6,7 +6,7 @@ import {
UserDefinedFunctionDefinition,
} from "@azure/cosmos";
import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
@@ -89,7 +89,6 @@ export interface Database extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
selectDatabase(): void;
expandDatabase(): Promise<void>;
collapseDatabase(): void;
@@ -275,7 +274,6 @@ export interface TabOptions {
tabKind: CollectionTabKind;
title: string;
tabPath: string;
hashLocation: string;
isTabsContentExpanded?: ko.Observable<boolean>;
onLoadStartKey?: number;
@@ -286,6 +284,7 @@ export interface TabOptions {
rid?: string;
node?: TreeNode;
theme?: string;
index?: number;
}
export interface DocumentsTabOptions extends TabOptions {

View File

@@ -22,8 +22,8 @@ describe("The Heatmap Control", () => {
};
let heatmap: Heatmap;
let theme: PortalTheme = 1;
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
const theme: PortalTheme = 1;
const divElement = `<div id="${Heatmap.elementId}"></div>`;
describe("drawHeatmap rendering", () => {
beforeEach(() => {
@@ -100,7 +100,7 @@ describe("iframe rendering when there is no data", () => {
});
it("should show a no data message with a dark theme", () => {
let data = {
const data = {
data: {
signature: "pcIframe",
data: {
@@ -111,7 +111,7 @@ describe("iframe rendering when there is no data", () => {
},
};
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);
@@ -120,7 +120,7 @@ describe("iframe rendering when there is no data", () => {
});
it("should show a no data message with a white theme", () => {
let data = {
const data = {
data: {
signature: "pcIframe",
data: {
@@ -131,7 +131,7 @@ describe("iframe rendering when there is no data", () => {
},
};
const divElement: string = `<div id="${Heatmap.elementId}"></div>`;
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);

View File

@@ -39,7 +39,7 @@ export class Heatmap {
}
}
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color: string = "#838383"): FontSettings {
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
return {
family: StyleConstants.DataExplorerFont,
size,
@@ -78,9 +78,9 @@ export class Heatmap {
// go thru all rows and create 2d matrix for heatmap...
for (let i = 0; i < rows.length; i++) {
output.yAxisPoints.push(rows[i]);
let dataPoints: number[] = [];
const dataPoints: number[] = [];
for (let a = 0; a < output.xAxisPoints.length; a++) {
let row: PartitionTimeStampToData = data[rows[i]];
const row: PartitionTimeStampToData = data[rows[i]];
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
}
output.dataPoints.push(dataPoints);
@@ -193,7 +193,7 @@ export class Heatmap {
this._getLayoutSettings(),
this._getChartDisplaySettings()
);
let plotDiv: any = document.getElementById(Heatmap.elementId);
const plotDiv: any = document.getElementById(Heatmap.elementId);
plotDiv.on("plotly_click", (data: any) => {
let timeSelected: string = data.points[0].x;
timeSelected = timeSelected.replace(" ", "T");
@@ -205,7 +205,7 @@ export class Heatmap {
break;
}
}
let output = [];
const output = [];
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]);
}

View File

@@ -1,172 +0,0 @@
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import AddUdfIcon from "../../images/AddUdf.svg";
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
export interface CollectionContextMenuButtonParams {
databaseId: string;
collectionId: string;
}
export interface DatabaseContextMenuButtonParams {
databaseId: string;
}
/**
* New resource tree (in ReactJS)
*/
export class ResourceTreeContextMenuButtonFactory {
public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(databaseId),
label: `New ${getCollectionName()}`,
},
];
if (userContext.apiType !== "Tables") {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () => container.openDeleteDatabaseConfirmationPane(),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
}
return items;
}
public static createCollectionContextMenuButton(
container: Explorer,
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
label: "New SQL Query",
});
}
if (userContext.apiType === "Mongo") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),
label: "New Query",
});
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
},
label: "New Trigger",
});
}
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () => container.openDeleteCollectionConfirmationPane(),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
return items;
}
public static createStoreProcedureContextMenuItems(
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: "Delete Store Procedure",
},
];
}
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: "Delete Trigger",
},
];
}
public static createUserDefinedFunctionContextMenuItems(
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: "Delete User Defined Function",
},
];
}
}

View File

@@ -0,0 +1,190 @@
import React from "react";
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import AddUdfIcon from "../../images/AddUdf.svg";
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { useSidePanel } from "../hooks/useSidePanel";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import { useSelectedNode } from "./useSelectedNode";
export interface CollectionContextMenuButtonParams {
databaseId: string;
collectionId: string;
}
export interface DatabaseContextMenuButtonParams {
databaseId: string;
}
/**
* New resource tree (in ReactJS)
*/
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
const items: TreeNodeMenuItem[] = [
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked(databaseId),
label: `New ${getCollectionName()}`,
},
];
if (userContext.apiType !== "Tables") {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
}
return items;
};
export const createCollectionContextMenuButton = (
container: Explorer,
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] => {
const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
label: "New SQL Query",
});
}
if (userContext.apiType === "Mongo") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
label: "New Query",
});
items.push({
iconSrc: HostedTerminalIcon,
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: "New Trigger",
});
}
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
return items;
};
export const createStoreProcedureContextMenuItems = (
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] => {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: "Delete Store Procedure",
},
];
};
export const createTriggerContextMenuItems = (container: Explorer, trigger: Trigger): TreeNodeMenuItem[] => {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: "Delete Trigger",
},
];
};
export const createUserDefinedFunctionContextMenuItems = (
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] => {
if (userContext.apiType === "Cassandra") {
return [];
}
return [
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: "Delete User Defined Function",
},
];
};

View File

@@ -8,7 +8,9 @@ import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import * as Constants from "../../../Common/Constants";
export interface AccordionComponentProps {}
export interface AccordionComponentProps {
children: React.ReactNode;
}
export class AccordionComponent extends React.Component<AccordionComponentProps> {
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 });
};

View File

@@ -121,8 +121,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
if (!this.dropdownElt || !this.expandButtonElt) {
return;
}
const dropdownElt = $(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
}
private onKeyPress(event: React.KeyboardEvent): boolean {

View File

@@ -23,13 +23,75 @@ export interface DialogState {
dialogProps?: DialogProps;
openDialog: (props: DialogProps) => void;
closeDialog: () => void;
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
) => void;
showOkModalDialog: (title: string, subText: string) => void;
}
export const useDialog: UseStore<DialogState> = create((set) => ({
export const useDialog: UseStore<DialogState> = create((set, get) => ({
visible: false,
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
closeDialog: () =>
set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true),
set(
(state) => ({
visible: false,
openDialog: state.openDialog,
closeDialog: state.closeDialog,
showOkCancelModalDialog: state.showOkCancelModalDialog,
showOkModalDialog: state.showOkModalDialog,
}),
true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above
),
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
get().closeDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
get().closeDialog();
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
}),
showOkModalDialog: (title: string, subText: string): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
get().closeDialog();
},
onSecondaryButtonClick: undefined,
}),
}));
export interface TextFieldProps extends ITextFieldProps {

View File

@@ -1,6 +1,11 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react";
import { loadMonaco, monaco } from "../../LazyMonaco";
// import "./EditorReact.less";
interface EditorReactStates {
showEditor: boolean;
}
export interface EditorReactProps {
language: string;
content: string;
@@ -12,22 +17,26 @@ export interface EditorReactProps {
theme?: string; // Monaco editor theme
}
export class EditorReact extends React.Component<EditorReactProps> {
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
private rootNode: HTMLElement;
private editor: monaco.editor.IStandaloneCodeEditor;
private selectionListener: monaco.IDisposable;
public constructor(props: EditorReactProps) {
super(props);
this.state = {
showEditor: false,
};
}
public componentDidMount(): void {
this.createEditor(this.configureEditor.bind(this));
}
public shouldComponentUpdate(): boolean {
// Prevents component re-rendering
return false;
public componentDidUpdate(previous: EditorReactProps) {
if (this.props.content !== previous.content) {
this.editor.setValue(this.props.content);
}
}
public componentWillUnmount(): void {
@@ -35,14 +44,19 @@ export class EditorReact extends React.Component<EditorReactProps> {
}
public render(): JSX.Element {
return <div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />;
return (
<React.Fragment>
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
<div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />
</React.Fragment>
);
}
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor;
const queryEditorModel = this.editor.getModel();
if (!this.props.isReadOnly && this.props.onContentChanged) {
queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => {
queryEditorModel.onDidChangeContent(() => {
const queryEditorModel = this.editor.getModel();
this.props.onContentChanged(queryEditorModel.getValue());
});
@@ -76,6 +90,12 @@ export class EditorReact extends React.Component<EditorReactProps> {
this.rootNode.innerHTML = "";
const monaco = await loadMonaco();
createCallback(monaco.editor.create(this.rootNode, options));
if (this.rootNode.innerHTML) {
this.setState({
showEditor: true,
});
}
}
private setRef(element: HTMLElement): void {

View File

@@ -56,7 +56,7 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
return (
<>
<div className={"paneMainContent"}>{content}</div>
<div>{content}</div>
{!this.props.showAuthorizeAccess && (
<>
<div className={"paneFooter"} style={ContentFooterStyle}>

View File

@@ -1,20 +0,0 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent";
export class GitHubReposComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private props: GitHubReposComponentProps) {
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <GitHubReposComponent {...this.props} />;
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -5,6 +5,9 @@
display: inline-block;
width: 100%;
.input-type-head-text-field {
width: 100%;
}
textarea {
width: 100%;
line-height: 1;
@@ -21,4 +24,11 @@
}
}
}
.input-typeahead-chocies-container {
border: 1px solid lightgrey;
padding: 5px 10px 5px 10px;
cursor: pointer;
.choice-caption{
font-size: 14px;
}
}

View File

@@ -1,7 +1,7 @@
import React from "react";
import { shallow } from "enzyme";
import { InputTypeaheadComponent, InputTypeaheadComponentProps } from "./InputTypeaheadComponent";
import React from "react";
import "../../../../externals/jquery.typeahead.min.js";
import { InputTypeaheadComponent, InputTypeaheadComponentProps } from "./InputTypeaheadComponent";
describe("inputTypeahead", () => {
it("renders <input />", () => {
@@ -12,6 +12,12 @@ describe("inputTypeahead", () => {
],
placeholder: "placeholder",
useTextarea: false,
onNewValue: () => {
("");
},
submitFct: () => {
("");
},
};
const wrapper = shallow(<InputTypeaheadComponent {...props} />);
@@ -26,6 +32,12 @@ describe("inputTypeahead", () => {
],
placeholder: "placeholder",
useTextarea: true,
onNewValue: () => {
("");
},
submitFct: () => {
("");
},
};
const wrapper = shallow(<InputTypeaheadComponent {...props} />);

View File

@@ -6,14 +6,13 @@
* typeaheadOverrideOptions: { dynamic:false }
*
*/
import "jquery-typeahead";
import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react";
import * as React from "react";
import { KeyCodes } from "../../../Common/Constants";
import "./InputTypeahead.less";
export interface Item {
caption: string;
value: any;
value: string;
}
/**
@@ -34,7 +33,7 @@ export interface InputTypeaheadComponentProps {
/**
* The current string value of <input>
*/
onNewValue?: (newValue: string) => void;
onNewValue: (newValue?: string) => void;
// inputValue?:ko.Observable<string>;
/**
@@ -56,7 +55,7 @@ export interface InputTypeaheadComponentProps {
/**
* This function gets called when pressing ENTER on the input box
*/
submitFct?: (inputValue: string, selection: Item) => void;
submitFct?: (inputValue?: string, selection?: Item) => void;
/**
* Typehead comes with a Search button that we normally remove.
@@ -75,170 +74,125 @@ export interface InputTypeaheadComponentProps {
useTextarea?: boolean;
}
interface OnClickItem {
matchedKey: string;
value: any;
caption: string;
group: string;
interface InputTypeaheadComponentState {
isSuggestionVisible: boolean;
selectedChoice?: Item;
filteredChoices: Item[];
}
interface Cache {
inputValue: string;
selection: Item;
}
interface InputTypeaheadComponentState {}
export class InputTypeaheadComponent extends React.Component<
InputTypeaheadComponentProps,
InputTypeaheadComponentState
> {
private inputElt: HTMLElement;
private containerElt: HTMLElement;
private cache: Cache;
private inputValue: string;
private selection: Item;
public constructor(props: InputTypeaheadComponentProps) {
constructor(props: InputTypeaheadComponentProps) {
super(props);
this.cache = {
inputValue: null,
selection: null,
this.state = {
isSuggestionVisible: false,
filteredChoices: [],
selectedChoice: {
caption: "",
value: "",
},
};
}
/**
* Props have changed
* @param prevProps
* @param prevState
* @param snapshot
*/
public componentDidUpdate(
prevProps: InputTypeaheadComponentProps,
prevState: InputTypeaheadComponentState,
snapshot: any
): void {
if (prevProps.defaultValue !== this.props.defaultValue) {
$(this.inputElt).val(this.props.defaultValue);
this.initializeTypeahead();
}
}
/**
* Executed once react is done building the DOM for this component
*/
public componentDidMount(): void {
this.initializeTypeahead();
}
public render(): JSX.Element {
private onRenderCell = (item?: Item): JSX.Element => {
return (
<span className="input-typeahead-container">
<div
className="input-typehead"
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(event)}
>
<div className="typeahead__container" ref={(input) => (this.containerElt = input)}>
<div className="typeahead__field">
<span className="typeahead__query">
{this.props.useTextarea ? (
<textarea
rows={1}
name="q"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
) : (
<input
name="q"
type="search"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
)}
</span>
{this.props.showSearchButton && (
<span className="typeahead__button">
<button type="submit">
<span className="typeahead__search-icon" />
</button>
</span>
)}
</div>
</div>
</div>
</span>
<div className="input-typeahead-chocies-container" onClick={() => this.onChoiceClick(item)}>
<p className="choice-caption">{item?.caption}</p>
<span>{item?.value}</span>
</div>
);
}
};
private onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
if (event.keyCode === KeyCodes.Enter) {
private onChoiceClick = (item?: Item): void => {
this.props.onNewValue(item?.caption);
this.setState({ isSuggestionVisible: false, selectedChoice: item });
};
private handleChange = (value?: string): void => {
if (!value) {
this.setState({ isSuggestionVisible: true });
}
this.props.onNewValue(value);
const filteredChoices = this.filterChoiceByValue(this.props.choices, value);
this.setState({ filteredChoices });
};
private onSubmit = (event: React.KeyboardEvent<HTMLElement>): void => {
if (event.key === "Enter") {
if (this.props.submitFct) {
event.preventDefault();
event.stopPropagation();
this.props.submitFct(this.cache.inputValue, this.cache.selection);
$(this.containerElt).children(".typeahead__result").hide();
this.props.submitFct(this.props.defaultValue, this.state.selectedChoice);
this.setState({ isSuggestionVisible: false });
}
}
}
};
/**
* Must execute once ko is rendered, so that it can find the input element by id
*/
private initializeTypeahead(): void {
const props = this.props;
let cache = this.cache;
let options: any = {
input: this.inputElt,
order: "asc",
minLength: 0,
searchOnFocus: true,
source: {
display: "caption",
data: () => {
return props.choices;
},
},
callback: {
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
cache.selection = item;
private filterChoiceByValue = (choices: Item[], searchKeyword?: string): Item[] => {
return choices.filter((choice) =>
// @ts-ignore
Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase()))
);
};
if (props.onSelected) {
props.onSelected(item);
}
},
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
cache.inputValue = query;
if (props.onNewValue) {
props.onNewValue(query);
}
},
public render(): JSX.Element {
const { defaultValue, useTextarea, placeholder, onNewValue, submitFct } = this.props;
const { isSuggestionVisible, selectedChoice, filteredChoices } = this.state;
const theme = getTheme();
const iconButtonStyles = {
root: {
color: theme.palette.neutralPrimary,
marginLeft: "10px !important",
marginTop: "0px",
marginRight: "2px",
width: "42px",
},
template: (query: string, item: any) => {
// Don't display id if caption *IS* the id
return item.caption === item.value
? "<span>{{caption}}</span>"
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
rootHovered: {
color: theme.palette.neutralDark,
},
dynamic: true,
};
const cancelIcon: IIconProps = { iconName: "cancel" };
const searchIcon: IIconProps = { iconName: "Search" };
// Override options
if (props.typeaheadOverrideOptions) {
for (const p in props.typeaheadOverrideOptions) {
options[p] = props.typeaheadOverrideOptions[p];
}
}
if (props.hasOwnProperty("showCancelButton")) {
options.cancelButton = props.showCancelButton;
}
$(this.inputElt).typeahead(options);
return (
<div className="input-typeahead-container">
<Stack horizontal>
<TextField
multiline={useTextarea}
rows={1}
defaultValue={defaultValue}
ariaLabel="Input query"
placeholder={placeholder}
className="input-type-head-text-field"
value={defaultValue}
onKeyDown={this.onSubmit}
onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
/>
{this.props.showCancelButton && (
<IconButton
styles={iconButtonStyles}
iconProps={cancelIcon}
ariaLabel="cancel Button"
onClick={() => onNewValue("")}
/>
)}
{this.props.showSearchButton && (
<IconButton
styles={iconButtonStyles}
iconProps={searchIcon}
ariaLabel="Search Button"
onClick={() => submitFct && submitFct(defaultValue, selectedChoice)}
/>
)}
</Stack>
{filteredChoices.length && isSuggestionVisible ? (
<List items={filteredChoices} onRenderCell={this.onRenderCell} />
) : undefined}
</div>
);
}
}

View File

@@ -1,61 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`inputTypeahead renders <input /> 1`] = `
<span
<div
className="input-typeahead-container"
>
<div
className="input-typehead"
onKeyDown={[Function]}
<Stack
horizontal={true}
>
<div
className="typeahead__container"
>
<div
className="typeahead__field"
>
<span
className="typeahead__query"
>
<input
aria-label="Input query"
autoComplete="off"
name="q"
type="search"
/>
</span>
</div>
</div>
</div>
</span>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={false}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</Stack>
</div>
`;
exports[`inputTypeahead renders <textarea /> 1`] = `
<span
<div
className="input-typeahead-container"
>
<div
className="input-typehead"
onKeyDown={[Function]}
<Stack
horizontal={true}
>
<div
className="typeahead__container"
>
<div
className="typeahead__field"
>
<span
className="typeahead__query"
>
<textarea
aria-label="Input query"
autoComplete="off"
name="q"
rows={1}
/>
</span>
</div>
</div>
</div>
</span>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={true}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</Stack>
</div>
`;

View File

@@ -1,154 +1,90 @@
import { shallow } from "enzyme";
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import { NotebookTerminalComponent } from "./NotebookTerminalComponent";
import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent";
const createTestDatabaseAccount = (): DataModels.DatabaseAccount => {
return {
id: "testId",
kind: "testKind",
location: "testLocation",
name: "testName",
properties: {
cassandraEndpoint: null,
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
gremlinEndpoint: null,
tableEndpoint: null,
},
type: "testType",
};
const testAccount: DataModels.DatabaseAccount = {
id: "id",
kind: "kind",
location: "location",
name: "name",
properties: {
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
},
type: "type",
};
const createTestMongo32DatabaseAccount = (): DataModels.DatabaseAccount => {
return {
id: "testId",
kind: "testKind",
location: "testLocation",
name: "testName",
properties: {
cassandraEndpoint: null,
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
gremlinEndpoint: null,
tableEndpoint: null,
},
type: "testType",
};
const testMongo32Account: DataModels.DatabaseAccount = {
...testAccount,
};
const createTestMongo36DatabaseAccount = (): DataModels.DatabaseAccount => {
return {
id: "testId",
kind: "testKind",
location: "testLocation",
name: "testName",
properties: {
cassandraEndpoint: null,
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
gremlinEndpoint: null,
tableEndpoint: null,
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
},
type: "testType",
};
const testMongo36Account: DataModels.DatabaseAccount = {
...testAccount,
properties: {
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
},
};
const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => {
return {
id: "testId",
kind: "testKind",
location: "testLocation",
name: "testName",
properties: {
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
documentEndpoint: null,
gremlinEndpoint: null,
tableEndpoint: null,
},
type: "testType",
};
const testCassandraAccount: DataModels.DatabaseAccount = {
...testAccount,
properties: {
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
},
};
const createTerminal = (): NotebookTerminalComponent => {
return new NotebookTerminalComponent({
notebookServerInfo: {
authToken: "testAuthToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/",
},
databaseAccount: createTestDatabaseAccount(),
});
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
};
const createMongo32Terminal = (): NotebookTerminalComponent => {
return new NotebookTerminalComponent({
notebookServerInfo: {
authToken: "testAuthToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
},
databaseAccount: createTestMongo32DatabaseAccount(),
});
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
};
const createMongo36Terminal = (): NotebookTerminalComponent => {
return new NotebookTerminalComponent({
notebookServerInfo: {
authToken: "testAuthToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
},
databaseAccount: createTestMongo36DatabaseAccount(),
});
};
const createCassandraTerminal = (): NotebookTerminalComponent => {
return new NotebookTerminalComponent({
notebookServerInfo: {
authToken: "testAuthToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
},
databaseAccount: createTestCassandraDatabaseAccount(),
});
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
};
describe("NotebookTerminalComponent", () => {
it("getTerminalParams: Test for terminal", () => {
const terminal: NotebookTerminalComponent = createTerminal();
const params: Map<string, string> = terminal.getTerminalParams();
it("renders terminal", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testAccount,
notebookServerInfo: testNotebookServerInfo,
};
expect(params).toEqual(
new Map<string, string>([["terminal", "true"]])
);
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("getTerminalParams: Test for Mongo 3.2 terminal", () => {
const terminal: NotebookTerminalComponent = createMongo32Terminal();
const params: Map<string, string> = terminal.getTerminalParams();
it("renders mongo 3.2 shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo32Account,
notebookServerInfo: testMongoNotebookServerInfo,
};
expect(params).toEqual(
new Map<string, string>([
["terminal", "true"],
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host],
])
);
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("getTerminalParams: Test for Mongo 3.6 terminal", () => {
const terminal: NotebookTerminalComponent = createMongo36Terminal();
const params: Map<string, string> = terminal.getTerminalParams();
it("renders mongo 3.6 shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo36Account,
notebookServerInfo: testMongoNotebookServerInfo,
};
expect(params).toEqual(
new Map<string, string>([
["terminal", "true"],
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host],
])
);
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("getTerminalParams: Test for Cassandra terminal", () => {
const terminal: NotebookTerminalComponent = createCassandraTerminal();
const params: Map<string, string> = terminal.getTerminalParams();
it("renders cassandra shell", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testCassandraAccount,
notebookServerInfo: testCassandraNotebookServerInfo,
};
expect(params).toEqual(
new Map<string, string>([
["terminal", "true"],
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host],
])
);
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -2,12 +2,12 @@
* Wrapper around Notebook server terminal
*/
import postRobot from "post-robot";
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import * as StringUtils from "../../../Utils/StringUtils";
import { TerminalProps } from "../../../Terminal/TerminalProps";
import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import * as StringUtils from "../../../Utils/StringUtils";
export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
@@ -15,79 +15,69 @@ export interface NotebookTerminalComponentProps {
}
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
private terminalWindow: Window;
constructor(props: NotebookTerminalComponentProps) {
super(props);
}
componentDidMount(): void {
this.sendPropsToTerminalFrame();
}
public render(): JSX.Element {
return (
<div className="notebookTerminalContainer">
<iframe
title="Terminal to Notebook Server"
src={NotebookTerminalComponent.createNotebookAppSrc(this.props.notebookServerInfo, this.getTerminalParams())}
onLoad={(event) => this.handleFrameLoad(event)}
src="terminal.html"
/>
</div>
);
}
public getTerminalParams(): Map<string, string> {
let params: Map<string, string> = new Map<string, string>();
params.set(TerminalQueryParams.Terminal, "true");
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
if (terminalEndpoint) {
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
}
return params;
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
this.sendPropsToTerminalFrame();
}
public tryGetTerminalEndpoint(): string | null {
let terminalEndpoint: string | null;
sendPropsToTerminalFrame(): void {
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")) {
let mongoShellEndpoint: string = this.props.databaseAccount.properties.mongoEndpoint;
if (!mongoShellEndpoint) {
// mongoEndpoint is only available for Mongo 3.6 and higher.
// Fallback to documentEndpoint otherwise.
mongoShellEndpoint = this.props.databaseAccount.properties.documentEndpoint;
}
terminalEndpoint = mongoShellEndpoint;
// mongoEndpoint is only available for Mongo 3.6 and higher, fallback to documentEndpoint otherwise
terminalEndpoint =
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
terminalEndpoint = this.props.databaseAccount.properties.cassandraEndpoint;
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
}
if (terminalEndpoint) {
return new URL(terminalEndpoint).host;
}
return null;
}
public static createNotebookAppSrc(
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;
return undefined;
}
}

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

@@ -55,7 +55,7 @@ export class NotebookViewerComponent
databaseAccountName: undefined,
defaultExperience: "NotebookViewer",
isReadOnly: true,
cellEditorType: "monaco",
cellEditorType: "codemirror",
autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year
contentProvider: contents.JupyterContentProvider, // NotebookViewer only knows how to talk to Jupyter contents API
});

View File

@@ -29,8 +29,9 @@ import { QueriesClient } from "../../../Common/QueriesClient";
import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Dialog";
const title: string = "Open Saved Queries";
const title = "Open Saved Queries";
export interface QueriesGridComponentProps {
queriesClient: QueriesClient;
@@ -196,9 +197,9 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
{
key: "Action",
name: "Action",
fieldName: null,
fieldName: undefined,
minWidth: 70,
onRender: (query: Query, index: number, column: IColumn) => {
onRender: (query: Query) => {
const buttonProps: IButtonProps = {
iconProps: {
iconName: "More",
@@ -214,47 +215,50 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
{
key: "Open",
text: "Open query",
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, menuItem: any) => {
onClick: () => {
this.props.onQuerySelect(query);
},
},
{
key: "Delete",
text: "Delete query",
onClick: async (
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
menuItem: any
) => {
if (window.confirm("Are you sure you want to delete this query?")) {
const container = window.dataExplorer;
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
});
try {
await this.props.queriesClient.deleteQuery(query);
TelemetryProcessor.traceSuccess(
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
},
startKey
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
await this.fetchSavedQueries(); // get latest state
}
onClick: async () => {
useDialog.getState().showOkCancelModalDialog(
"Confirm delete",
"Are you sure you want to delete this query?",
"Delete",
async () => {
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
});
try {
await this.props.queriesClient.deleteQuery(query);
TelemetryProcessor.traceSuccess(
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
},
startKey
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
await this.fetchSavedQueries(); // get latest state
},
"Cancel",
undefined
);
},
},
],

View File

@@ -38,7 +38,6 @@ describe("SettingsComponent", () => {
title: "Scale & Settings",
tabPath: "",
node: undefined,
hashLocation: "settings",
}),
};
@@ -127,7 +126,6 @@ describe("SettingsComponent", () => {
isDatabaseExpanded: undefined,
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: undefined,
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,

View File

@@ -17,7 +17,6 @@ import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
@@ -122,7 +121,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private collection: ViewModels.Collection;
private database: ViewModels.Database;
private offer: DataModels.Offer;
private container: Explorer;
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
private shouldShowIndexingPolicyEditor: boolean;
@@ -134,7 +132,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.isCollectionSettingsTab = this.props.settingsTab.tabKind === ViewModels.CollectionTabKind.CollectionSettingsV2;
if (this.isCollectionSettingsTab) {
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.container = this.collection?.container;
this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
@@ -146,7 +143,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
userContext.apiType === "Mongo" && (!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
} else {
this.database = this.props.settingsTab.database;
this.container = this.database?.container;
this.offer = this.database?.offer();
}
@@ -294,7 +290,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean =>
userContext?.databaseAccount?.properties?.enableMultipleWriteLocations &&
@@ -884,7 +880,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const scaleComponentProps: ScaleComponentProps = {
collection: this.collection,
database: this.database,
container: this.container,
isFixedContainer: this.isFixedContainer,
onThroughputChange: this.onThroughputChange,
throughput: this.state.throughput,
@@ -912,7 +907,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const subSettingsComponentProps: SubSettingsComponentProps = {
collection: this.collection,
container: this.container,
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,
changeFeedPolicyVisible: this.changeFeedPolicyVisible,
timeToLive: this.state.timeToLive,
@@ -965,7 +959,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
collection: this.collection,
container: this.container,
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
conflictResolutionPolicyModeBaseline: this.state.conflictResolutionPolicyModeBaseline,
onConflictResolutionPolicyModeChange: this.onConflictResolutionPolicyModeChange,

View File

@@ -1,14 +0,0 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { SettingsComponent, SettingsComponentProps } from "./SettingsComponent";
export class SettingsComponentAdapter implements ReactAdapter {
public parameters: ko.Computed<boolean>;
constructor(private props: SettingsComponentProps) {}
public renderComponent(): JSX.Element {
return this.parameters() ? <SettingsComponent {...this.props} /> : <></>;
}
}

View File

@@ -1,13 +1,12 @@
import { shallow } from "enzyme";
import React from "react";
import { ConflictResolutionComponentProps, ConflictResolutionComponent } from "./ConflictResolutionComponent";
import { container, collection } from "../TestUtils";
import * as DataModels from "../../../../Contracts/DataModels";
import { collection } from "../TestUtils";
import { ConflictResolutionComponent, ConflictResolutionComponentProps } from "./ConflictResolutionComponent";
describe("ConflictResolutionComponent", () => {
const baseProps: ConflictResolutionComponentProps = {
collection: collection,
container: container,
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode.Custom,
onConflictResolutionPolicyModeChange: () => {

View File

@@ -1,21 +1,19 @@
import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import Explorer from "../../../Explorer";
import * as ViewModels from "../../../../Contracts/ViewModels";
import {
getTextFieldStyles,
conflictResolutionLwwTooltip,
conflictResolutionCustomToolTip,
subComponentStackProps,
conflictResolutionLwwTooltip,
getChoiceGroupStyles,
getTextFieldStyles,
subComponentStackProps,
} from "../SettingsRenderUtils";
import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
import { isDirty } from "../SettingsUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface ConflictResolutionComponentProps {
collection: ViewModels.Collection;
container: Explorer;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void;

View File

@@ -7,20 +7,17 @@ import * as SharedConstants from "../../../../Shared/Constants";
import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { throughputUnit } from "../SettingsRenderUtils";
import { collection, container } from "../TestUtils";
import { collection } from "../TestUtils";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer();
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
const targetThroughput = 6000;
const baseProps: ScaleComponentProps = {
collection: collection,
database: undefined,
container: container,
isFixedContainer: false,
onThroughputChange: () => {
return;
@@ -111,7 +108,7 @@ describe("ScaleComponent", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
let newProps = { ...baseProps, container: nonNationalCloudContainer };
let newProps = { ...baseProps };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
@@ -124,7 +121,7 @@ describe("ScaleComponent", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
const newProps = { ...baseProps, container: nonNationalCloudContainer };
const newProps = { ...baseProps };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
});

View File

@@ -7,7 +7,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import Explorer from "../../../Explorer";
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
import {
getTextFieldStyles,
getThroughputApplyLongDelayMessage,
@@ -23,7 +23,6 @@ import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents
export interface ScaleComponentProps {
collection: ViewModels.Collection;
database: ViewModels.Database;
container: Explorer;
isFixedContainer: boolean;
onThroughputChange: (newThroughput: number) => void;
throughput: number;
@@ -109,11 +108,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
};
public canThroughputExceedMaximumValue = (): boolean => {
return (
!this.props.isFixedContainer &&
configContext.platform === Platform.Portal &&
!this.props.container.isRunningOnNationalCloud()
);
return !this.props.isFixedContainer && configContext.platform === Platform.Portal && !isRunningOnNationalCloud();
};
public getInitialNotificationElement = (): JSX.Element => {

View File

@@ -4,14 +4,12 @@ import { DatabaseAccount } from "../../../../Contracts/DataModels";
import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { ChangeFeedPolicyState, GeospatialConfigType, TtlOff, TtlOn, TtlOnNoDefault, TtlType } from "../SettingsUtils";
import { collection, container } from "../TestUtils";
import { collection } from "../TestUtils";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
describe("SubSettingsComponent", () => {
const baseProps: SubSettingsComponentProps = {
collection: collection,
container: container,
timeToLive: TtlType.On,
timeToLiveBaseline: TtlType.On,
onTtlChange: () => {

View File

@@ -2,7 +2,6 @@ import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text,
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
changeFeedPolicyToolTip,
@@ -28,8 +27,6 @@ import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface SubSettingsComponentProps {
collection: ViewModels.Collection;
container: Explorer;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;

View File

@@ -36,7 +36,6 @@ describe("SettingsUtils", () => {
isDatabaseExpanded: ko.observable(false),
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: ko.observable(undefined),
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,

View File

@@ -30,23 +30,8 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@@ -54,43 +39,11 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],
@@ -110,73 +63,6 @@ exports[`SettingsComponent renders 1`] = `
"usageSizeInKB": [Function],
}
}
container={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
isAutoPilotSelected={false}
isFixedContainer={false}
onAutoPilotSelected={[Function]}
@@ -211,23 +97,8 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@@ -235,43 +106,11 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],
@@ -291,73 +130,6 @@ exports[`SettingsComponent renders 1`] = `
"usageSizeInKB": [Function],
}
}
container={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"collapsedResourceTreeWidth": 36,
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined,
"setNotificationConsoleData": undefined,
"sparkClusterConnectionInfo": [Function],
"splitter": Splitter {
"bounds": Object {
"max": 400,
"min": 240,
},
"direction": "vertical",
"isCollapsed": [Function],
"leftSideId": "resourcetree",
"onResizeStart": [Function],
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
geospatialConfigType="Geometry"
geospatialConfigTypeBaseline="Geometry"
isAnalyticalStorageEnabled={false}

View File

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

View File

@@ -58,7 +58,7 @@ export interface TreeComponentProps {
export class TreeComponent extends React.Component<TreeComponentProps> {
public render(): JSX.Element {
return (
<div style={this.props.style} className={`treeComponent ${this.props.className}`}>
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
</div>
);
@@ -172,6 +172,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
role="treeitem"
>
<div
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}

View File

@@ -3,6 +3,7 @@
exports[`TreeComponent renders a simple tree 1`] = `
<div
className="treeComponent tree"
role="tree"
>
<TreeNodeComponent
generation={0}
@@ -37,6 +38,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -137,6 +139,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
className="nodeClassname main12 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -285,6 +288,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -356,6 +360,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
className="nodeClassname main12 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "
@@ -523,6 +528,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
>
<div
className="treeNodeHeader "

View File

@@ -2,22 +2,22 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout";
import Q from "q";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve();
return explorerStub;
};
let explorerStub: Explorer;
beforeAll(() => {
explorerStub = {
refreshAllDatabases: () => {},
} as Explorer;
});
beforeEach(() => {
(createDocument as jest.Mock).mockResolvedValue(undefined);
@@ -59,8 +59,7 @@ describe("ContainerSampleGenerator", () => {
loadCollections: () => {},
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
const explorerStub = createExplorerStub(database);
useDatabases.getState().addDatabases([database]);
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
@@ -108,8 +107,8 @@ describe("ContainerSampleGenerator", () => {
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
collection.databaseId = database.id();
useDatabases.getState().addDatabases([database]);
const explorerStub = createExplorerStub(database);
updateUserContext({
databaseAccount: {
properties: {
@@ -126,7 +125,6 @@ describe("ContainerSampleGenerator", () => {
it("should not create any sample for Mongo API account", async () => {
const experience = "Sample generation not supported for this API Mongo";
const explorerStub = createExplorerStub(undefined);
updateUserContext({
databaseAccount: {
properties: {
@@ -141,7 +139,6 @@ describe("ContainerSampleGenerator", () => {
it("should not create any sample for Table API account", async () => {
const experience = "Sample generation not supported for this API Tables";
const explorerStub = createExplorerStub(undefined);
updateUserContext({
databaseAccount: {
properties: {
@@ -163,7 +160,6 @@ describe("ContainerSampleGenerator", () => {
},
} as DatabaseAccount,
});
const explorerStub = createExplorerStub(undefined);
// Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});

View File

@@ -7,6 +7,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import GraphTab from ".././Tabs/GraphTab";
import Explorer from "../Explorer";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import { useDatabases } from "../useDatabases";
interface SampleDataFile extends DataModels.CreateCollectionParams {
data: any[];
@@ -59,7 +60,7 @@ export class ContainerSampleGenerator {
await createCollection(createRequest);
await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) {
return undefined;
}

View File

@@ -2,6 +2,7 @@ import * as ko from "knockout";
import * as sinon from "sinon";
import { Collection, Database } from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { DataSamplesUtil } from "./DataSamplesUtil";
@@ -16,8 +17,7 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]),
} as Database;
const explorer = {} as Explorer;
explorer.databases = ko.observableArray<Database>([database]);
explorer.showOkModalDialog = () => {};
useDatabases.getState().addDatabases([database]);
const dataSamplesUtil = new DataSamplesUtil(explorer);
const fakeGenerator = sinon.createStubInstance<ContainerSampleGenerator>(ContainerSampleGenerator as any);

View File

@@ -1,7 +1,9 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil {
@@ -17,9 +19,9 @@ export class DataSamplesUtil {
const databaseName = generator.getDatabaseId();
const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, this.container.databases())) {
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg);
return;
}
@@ -28,7 +30,7 @@ export class DataSamplesUtil {
.createSampleContainerAsync()
.catch((error) => logConsoleError(`Error creating sample container: ${error}`));
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleInfo(msg);
}

View File

@@ -1,43 +0,0 @@
jest.mock("./../Common/dataAccess/deleteDatabase");
jest.mock("./../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase";
import * as ViewModels from "./../Contracts/ViewModels";
import Explorer from "./Explorer";
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: Explorer;
beforeAll(() => {
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
});
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if only 1 database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
const database = {} as ViewModels.Database;
const database2 = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
const database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,12 @@
*/
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 DeleteIcon from "../../../../images/delete.svg";
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 {
isSource: boolean;
@@ -83,11 +83,11 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
}
private removeCurrentNeighborEdge(index: number): void {
let sources = this.props.editedNeighbors.currentNeighbors;
let id = sources[index].edgeId;
const sources = this.props.editedNeighbors.currentNeighbors;
const id = sources[index].edgeId;
sources.splice(index, 1);
let droppedIds = this.props.editedNeighbors.droppedIds;
const droppedIds = this.props.editedNeighbors.droppedIds;
droppedIds.push(id);
this.onUpdateEdges();
}
@@ -215,7 +215,7 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
</td>
<td className="actionCol">
<span className="rightPaneTrashIcon rightPaneBtns">
<img src={DeleteIcon} alt="Delete" onClick={(e) => this.removeAddedEdgeToNeighbor(index)} />
<img src={DeleteIcon} alt="Delete" onClick={() => this.removeAddedEdgeToNeighbor(index)} />
</span>
</td>
</tr>

View File

@@ -1,12 +1,11 @@
import React from "react";
import { shallow } from "enzyme";
import { GraphHighlightedNodeData, EditedProperties } from "./GraphExplorer";
import React from "react";
import { EditorNodePropertiesComponent, EditorNodePropertiesComponentProps } from "./EditorNodePropertiesComponent";
describe("<EditorNodePropertiesComponent />", () => {
// Tests that: single value prop is rendered with a textbox and a delete button
// multi-value prop only a delete button (cannot be edited)
const onUpdateProperties = jest.fn();
it("renders component", () => {
const props: EditorNodePropertiesComponentProps = {
editedProperties: {
@@ -24,7 +23,6 @@ describe("<EditorNodePropertiesComponent />", () => {
{ value: true, type: "boolean" },
{ value: false, type: "boolean" },
{ value: undefined, type: "null" },
{ value: null, type: "null" },
],
},
],
@@ -41,14 +39,13 @@ describe("<EditorNodePropertiesComponent />", () => {
{ value: true, type: "boolean" },
{ value: false, type: "boolean" },
{ value: undefined, type: "null" },
{ value: null, type: "null" },
],
},
],
addedProperties: [],
droppedKeys: [],
},
onUpdateProperties: (editedProperties: EditedProperties): void => {},
onUpdateProperties,
};
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
expect(wrapper).toMatchSnapshot();
@@ -81,7 +78,7 @@ describe("<EditorNodePropertiesComponent />", () => {
addedProperties: [],
droppedKeys: [],
},
onUpdateProperties: (editedProperties: EditedProperties): void => {},
onUpdateProperties,
};
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
expect(wrapper).toMatchSnapshot();

View File

@@ -4,12 +4,12 @@
*/
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 { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
import DeleteIcon from "../../../../images/delete.svg";
import * as ViewModels from "../../../Contracts/ViewModels";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import { EditedProperties } from "./GraphExplorer";
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
export interface EditorNodePropertiesComponentProps {
editedProperties: EditedProperties;
@@ -48,7 +48,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
const editedProperties = this.props.editedProperties;
// search for it
for (let i = 0; i < editedProperties.existingProperties.length; i++) {
let ip = editedProperties.existingProperties[i];
const ip = editedProperties.existingProperties[i];
if (ip.key === key) {
editedProperties.existingProperties.splice(i, 1);
editedProperties.droppedKeys.push(key);
@@ -60,7 +60,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
private removeAddedProperty(index: number): void {
const editedProperties = this.props.editedProperties;
let ap = editedProperties.addedProperties;
const ap = editedProperties.addedProperties;
ap.splice(index, 1);
this.props.onUpdateProperties(editedProperties);
@@ -68,7 +68,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
private addProperty(): void {
const editedProperties = this.props.editedProperties;
let ap = editedProperties.addedProperties;
const ap = editedProperties.addedProperties;
ap.push({ key: "", values: [{ value: "", type: EditorNodePropertiesComponent.DEFAULT_PROPERTY_TYPE }] });
this.props.onUpdateProperties(editedProperties);
}
@@ -126,7 +126,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
onChange={(e) => {
singleValue.type = e.target.value as ViewModels.InputPropertyValueTypeString;
if (singleValue.type === "null") {
singleValue.value = null;
singleValue.value = undefined;
}
this.props.onUpdateProperties(this.props.editedProperties);
}}
@@ -144,7 +144,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
className="rightPaneTrashIcon rightPaneBtns"
as="span"
aria-label="Delete property"
onActivated={(e) => this.removeExistingProperty(key)}
onActivated={() => this.removeExistingProperty(key)}
>
<img src={DeleteIcon} alt="Delete" />
</AccessibleElement>
@@ -166,7 +166,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
className="rightPaneTrashIcon rightPaneBtns"
as="span"
aria-label="Remove existing property"
onActivated={(e) => this.removeExistingProperty(nodeProp.key)}
onActivated={() => this.removeExistingProperty(nodeProp.key)}
>
<img src={DeleteIcon} alt="Delete" />
</AccessibleElement>
@@ -206,7 +206,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
onChange={(e) => {
firstValue.value = e.target.value;
if (firstValue.type === "null") {
firstValue.value = null;
firstValue.value = undefined;
}
this.props.onUpdateProperties(this.props.editedProperties);
}}
@@ -235,7 +235,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
className="rightPaneTrashIcon rightPaneBtns"
as="span"
aria-label="Remove property"
onActivated={(e) => this.removeAddedProperty(index)}
onActivated={() => this.removeAddedProperty(index)}
>
<img src={DeleteIcon} alt="Delete" />
</AccessibleElement>

View File

@@ -10,7 +10,7 @@ import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPag
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
import GraphTab from "../../Tabs/GraphTab";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";

View File

@@ -18,7 +18,7 @@ import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Ut
import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { ArraysByKeyCache } from "./ArraysByKeyCache";
import * as D3ForceGraph from "./D3ForceGraph";

View File

@@ -1,74 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { GraphAccessor, GraphExplorer } from "./GraphExplorer";
interface Parameter {
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
onGraphAccessorCreated: (instance: GraphAccessor) => void;
onIsFilterQueryLoading: (isFilterQueryLoading: boolean) => void;
onIsValidQuery: (isValidQuery: boolean) => void;
onIsPropertyEditing: (isEditing: boolean) => void;
onIsGraphDisplayed: (isDisplayed: boolean) => void;
onResetDefaultGraphConfigValues: () => void;
collectionPartitionKeyProperty: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
masterKey: string;
onLoadStartKey: number;
onLoadStartKeyChange: (newKey: number) => void;
resourceId: string;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
setIConfigUiData?: (data: string[]) => void;
}
interface IGraphExplorerProps {
isChanged: boolean;
}
interface IGraphExplorerStates {
isChangedState: boolean;
}
export interface GraphExplorerAdapter
extends ReactAdapter,
React.Component<IGraphExplorerProps, IGraphExplorerStates> {}
export class GraphExplorerAdapter implements ReactAdapter {
public params: Parameter;
public parameters = {};
public isNewVertexDisabled: boolean;
public constructor(params: Parameter, props?: IGraphExplorerProps) {
this.params = params;
}
public renderComponent(): JSX.Element {
return (
<GraphExplorer
onIsNewVertexDisabledChange={this.params.onIsNewVertexDisabledChange}
onGraphAccessorCreated={this.params.onGraphAccessorCreated}
onIsFilterQueryLoadingChange={this.params.onIsFilterQueryLoading}
onIsValidQueryChange={this.params.onIsValidQuery}
onIsPropertyEditing={this.params.onIsPropertyEditing}
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
graphBackendEndpoint={this.params.graphBackendEndpoint}
databaseId={this.params.databaseId}
collectionId={this.params.collectionId}
masterKey={this.params.masterKey}
onLoadStartKey={this.params.onLoadStartKey}
onLoadStartKeyChange={this.params.onLoadStartKeyChange}
resourceId={this.params.resourceId}
igraphConfigUiData={this.params.igraphConfigUiData}
igraphConfig={this.params.igraphConfig}
setIConfigUiData={this.params.setIConfigUiData}
/>
);
}
}

View File

@@ -1,9 +1,7 @@
import * as GraphUtil from "./GraphUtil";
import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData";
import * as sinon from "sinon";
import { GraphData, GremlinEdge, GremlinVertex } from "./GraphData";
import { GraphExplorer } from "./GraphExplorer";
window.$ = window.jQuery = require("jquery");
import * as GraphUtil from "./GraphUtil";
const OUT_E_MATCHER = "g\\.V\\(.*\\).outE\\(\\).*\\.as\\('e'\\).inV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)";
const IN_E_MATCHER = "g\\.V\\(.*\\).inE\\(\\).*\\.as\\('e'\\).outV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)";

View File

@@ -5,21 +5,26 @@
*/
import * as React from "react";
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent";
import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as EditorNeighbors from "./EditorNeighborsComponent";
import EditIcon from "../../../../images/edit.svg";
import DeleteIcon from "../../../../images/delete.svg";
import CheckIcon from "../../../../images/check.svg";
import CancelIcon from "../../../../images/cancel.svg";
import { GraphExplorer } from "./GraphExplorer";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import CheckIcon from "../../../../images/check-1.svg";
import DeleteIcon from "../../../../images/delete.svg";
import EditIcon from "../../../../images/edit-1.svg";
import * as ViewModels from "../../../Contracts/ViewModels";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
import * as EditorNeighbors from "./EditorNeighborsComponent";
import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent";
import {
EditedEdges,
EditedProperties,
GraphExplorer,
GraphHighlightedNodeData,
PossibleVertex,
} from "./GraphExplorer";
import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent";
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
export enum Mode {
READONLY_PROP,

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import CloseIcon from "../../../../images/close-black.svg";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
export interface QueryContainerComponentProps {
initialQuery: string;
@@ -82,7 +82,7 @@ export class QueryContainerComponent extends React.Component<
<button
type="button"
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)}
>
Execute Gremlin Query

View File

@@ -4,9 +4,9 @@
*/
import * as React from "react";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import { GraphHighlightedNodeData, NeighborVertexBasicInfo } from "./GraphExplorer";
import * as GraphUtil from "./GraphUtil";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
export interface ReadOnlyNeighborsComponentProps {
node: GraphHighlightedNodeData;
@@ -48,7 +48,7 @@ export class ReadOnlyNeighborsComponent extends React.Component<ReadOnlyNeighbor
className="clickableLink"
as="a"
aria-label={_neighbor.name}
onActivated={(e) => this.props.selectNode(_neighbor.id)}
onActivated={() => this.props.selectNode(_neighbor.id)}
title={GraphUtil.getNeighborTitle(_neighbor)}
>
{_neighbor.name}

View File

@@ -4,8 +4,8 @@
*/
import * as React from "react";
import { GraphHighlightedNodeData } from "./GraphExplorer";
import * as ViewModels from "../../../Contracts/ViewModels";
import { GraphHighlightedNodeData } from "./GraphExplorer";
export interface ReadOnlyNodePropertiesComponentProps {
node: GraphHighlightedNodeData;

View File

@@ -37,7 +37,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
</td>
<td
className="valueCol"
title="efgh, 1234, true, false, undefined, null"
title="efgh, 1234, true, false, undefined"
>
<div
className="propertyValue"
@@ -69,12 +69,6 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
>
undefined
</div>
<div
className="propertyValue isNull"
key="null"
>
null
</div>
</td>
</tr>
<tr
@@ -178,12 +172,6 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
>
undefined
</div>
<div
className="propertyValue isNull"
key="null"
>
null
</div>
</td>
<td />
<td

View File

@@ -8,9 +8,10 @@ import * as React from "react";
import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useObservable } from "../../../hooks/useObservable";
import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
@@ -29,13 +30,13 @@ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
}));
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
useObservable(container.selectedNode);
const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons);
const backgroundColor = StyleConstants.BaseLight;
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container);
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
const contextButtons = (buttons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(container)
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState)
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
@@ -53,8 +54,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker", container.memoryUsageInfo));
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
}
return (

View File

@@ -1,17 +1,24 @@
import * as ko from "knockout";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer;
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -22,18 +29,10 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = () => false;
});
it("Account is not serverless - button should be visible", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@@ -41,9 +40,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Account is serverless - button should be hidden", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableServerless" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@@ -53,72 +57,77 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
portalEnv: "prod",
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
});
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
afterEach(() => {
updateUserContext({
portalEnv: "prod",
});
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Notebooks is already enabled - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
it("Account is running on one of the national clouds - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(false);
expect(enableNotebookBtn.tooltipText).toBe("");
//TODO: modify once notebooks are available
expect(enableNotebookBtn).toBeUndefined();
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(false);
//expect(enableNotebookBtn.tooltipText).toBe("");
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(true);
expect(enableNotebookBtn.tooltipText).toBe(
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
);
//TODO: modify once notebooks are available
expect(enableNotebookBtn).toBeUndefined();
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(true);
//expect(enableNotebookBtn.tooltipText).toBe(
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//);
});
});
describe("Open Mongo Shell button", () => {
const openMongoShellBtnLabel = "Open Mongo Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -129,87 +138,93 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
mockExplorer.isShellEnabled = ko.observable(true);
});
afterAll(() => {
updateUserContext({
apiType: "SQL",
});
useNotebook.getState().setIsShellEnabled(false);
});
beforeEach(() => {
updateUserContext({
apiType: "Mongo",
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
mockExplorer.isShellEnabled = ko.observable(true);
useNotebook.getState().setIsShellEnabled(true);
});
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Mongo Api not available - button should be hidden", () => {
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Running on a national cloud - button should be hidden", () => {
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
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);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
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);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe("");
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe("");
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isShellEnabled = ko.observable(false);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
useNotebook.getState().setIsShellEnabled(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -217,6 +232,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Cassandra Shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -227,10 +243,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -241,9 +253,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
});
afterEach(() => {
useNotebook.getState().setIsNotebookEnabled(false);
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
});
it("Cassandra Api not available - button should be hidden", () => {
@@ -254,59 +268,68 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
console.log(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Running on a national cloud - button should be hidden", () => {
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
updateUserContext({
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
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);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
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);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.tooltipText).toBe("");
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.tooltipText).toBe("");
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
});
});
describe("GitHub buttons", () => {
const connectToGitHubBtnLabel = "Connect to GitHub";
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@@ -318,36 +341,28 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
mockExplorer.isNotebookEnabled = ko.observable(false);
});
afterEach(() => {
jest.resetAllMocks();
useNotebook.getState().setIsNotebookEnabled(false);
});
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);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeDefined();
});
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);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel
);
@@ -355,7 +370,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeUndefined();
@@ -368,11 +383,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection);
useDatabases.setState({ resourceTokenCollection: mockCollection });
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
updateUserContext({
authType: AuthType.ResourceToken,
});
@@ -384,7 +402,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
expect(buttons[0].disabled).toBe(false);

View File

@@ -22,18 +22,32 @@ import * as Constants from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
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 { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { useDatabases } from "../../useDatabases";
import { SelectedNodeState } from "../../useSelectedNode";
let counter = 0;
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container);
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
}
const newCollectionBtn = createNewCollectionGroup(container);
@@ -53,49 +67,66 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
newCollectionBtn.children.push(newDatabaseBtn);
}
buttons.push(createDivider());
if (useNotebook.getState().isNotebookEnabled) {
buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = [];
if (container.isNotebookEnabled()) {
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton);
notebookButtons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) {
buttons.push(createManageGitHubAccountButton(container));
notebookButtons.push(createManageGitHubAccountButton(container));
}
buttons.push(createOpenTerminalButton(container));
notebookButtons.push(createOpenTerminalButton(container));
buttons.push(createNotebookWorkspaceResetButton(container));
notebookButtons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) ||
(userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
buttons.push(createDivider());
notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container));
notebookButtons.push(createOpenCassandraTerminalButton(container));
} else {
buttons.push(createOpenMongoTerminalButton(container));
notebookButtons.push(createOpenMongoTerminalButton(container));
}
}
notebookButtons.forEach((btn) => {
if (userContext.features.notebooksTemporarilyDown) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
} else {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
}
buttons.push(btn);
});
} else {
if (!container.isRunningOnNationalCloud()) {
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container));
}
}
if (!container.isDatabaseNodeOrNoneSelected()) {
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isQuerySupported) {
buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(container);
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
buttons.push(newSqlQueryBtn);
}
if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) {
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn);
}
@@ -105,16 +136,16 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(container);
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
buttons.push(newStoredProcedureBtn);
}
}
@@ -122,18 +153,23 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
return buttons;
}
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
export function createContextCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
if (!userContext.features.notebooksTemporarilyDown) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
@@ -141,7 +177,13 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
tooltipText:
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
};
buttons.push(newMongoShellBtn);
}
@@ -154,7 +196,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: container.openSettingPane,
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",
@@ -163,7 +205,10 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
},
];
if (container.isHostedDataExplorerEnabled()) {
const showOpenFullScreen =
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
if (showOpenFullScreen) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
iconSrc: OpenInTabIcon,
@@ -175,7 +220,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: !container.isHostedDataExplorerEnabled(),
disabled: !showOpenFullScreen,
className: "OpenFullScreen",
};
buttons.push(fullScreenButton);
@@ -234,7 +279,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
return undefined;
}
if (container.isServerlessEnabled()) {
if (isServerlessAccount()) {
return undefined;
}
@@ -254,7 +299,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: container.isSynapseLinkUpdating(),
disabled: useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
@@ -264,29 +309,28 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: () => {
container.openAddDatabasePane();
},
onCommandClick: () =>
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
}
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query";
return {
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
} else if (userContext.apiType === "Mongo") {
const label = "New Query";
@@ -294,23 +338,24 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
}
return undefined;
}
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
const shouldEnableScriptsCommands: boolean =
!selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
@@ -318,13 +363,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
@@ -335,13 +380,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
@@ -352,13 +397,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
@@ -366,6 +411,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
return buttons;
}
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
@@ -397,7 +449,8 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
onCommandClick: () => container.openBrowseQueriesPanel(),
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
@@ -405,12 +458,12 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
};
}
function createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane explorer={container} />),
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
@@ -430,12 +483,18 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
return {
iconSrc: EnableNotebooksIcon,
iconAlt: label,
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
label,
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
),
commandButtonLabel: label,
hasPopup: false,
disabled: !container.isNotebooksEnabledForAccount(),
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
ariaLabel: label,
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
};
}
@@ -459,15 +518,21 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
container.openSetupNotebooksPanel(title, description);
useSidePanel
.getState()
.openSidePanel(
title,
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
);
}
},
commandButtonLabel: label,
@@ -485,15 +550,21 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else {
container.openSetupNotebooksPanel(title, description);
useSidePanel
.getState()
.openSidePanel(
title,
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
);
}
},
commandButtonLabel: label,
@@ -520,10 +591,21 @@ function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonC
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
const junoClient = new JunoClient();
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => container.openGitHubReposPanel(label),
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
label,
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>
),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
@@ -531,19 +613,25 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
};
}
function createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(container);
function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton(container);
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
const isResourceTokenCollectionNodeSelected: boolean =
resourceTokenCollection?.id() === selectedNodeState.selectedNode?.id();
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
};
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
if (!openQueryBtn.disabled) {
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
}
return [newSqlQueryBtn, openQueryBtn];

View File

@@ -6,16 +6,14 @@ import {
IDropdownOption,
IDropdownStyles,
} from "@fluentui/react";
import { Observable } from "knockout";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { StyleConstants } from "../../../Common/Constants";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
import { MemoryTracker } from "./MemoryTrackerComponent";
/**
* Convert our NavbarButtonConfig to UI Fabric buttons
@@ -24,6 +22,13 @@ import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
const getFilter = (isDisabled: boolean): string => {
if (isDisabled) {
return StyleConstants.GrayScale;
}
return undefined;
};
return btns
.filter((btn) => btn)
.map(
@@ -39,6 +44,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
style: {
width: StyleConstants.CommandBarIconWidth, // 16
alignSelf: btn.iconName ? "baseline" : undefined,
filter: getFilter(btn.disabled),
},
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName,
@@ -125,8 +131,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
width: 12,
paddingLeft: 1,
paddingTop: 6,
filter: getFilter(btn.disabled),
},
imageProps: {
src: ChevronDownIcon,
alt: btn.iconAlt,
},
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt },
};
}
@@ -185,12 +195,9 @@ export const createDivider = (key: string): ICommandBarItemProps => {
};
};
export const createMemoryTracker = (
key: string,
memoryUsageInfo: Observable<MemoryUsageInfo>
): ICommandBarItemProps => {
export const createMemoryTracker = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />,
onRender: () => <MemoryTracker />,
};
};

View File

@@ -1,48 +1,29 @@
import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { Observable, Subscription } from "knockout";
import * as React from "react";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
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;
import { useNotebook } from "../../Notebook/useNotebook";
export const MemoryTracker: React.FC = (): JSX.Element => {
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
if (!memoryUsageInfo) {
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}
/>
<Spinner size={SpinnerSize.medium} />
</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,33 +0,0 @@
/**
* React component for control bar
*/
import * as React from "react";
import {
CommandButtonComponent,
CommandButtonComponentProps,
} from "../../Controls/CommandButton/CommandButtonComponent";
export interface ControlBarComponentProps {
buttons: CommandButtonComponentProps[];
}
export class ControlBarComponent extends React.Component<ControlBarComponentProps> {
private static renderButtons(commandButtonOptions: CommandButtonComponentProps[]): JSX.Element[] {
return commandButtonOptions.map(
(btn: CommandButtonComponentProps, index: number): JSX.Element => {
// Remove label
btn.commandButtonLabel = undefined;
return CommandButtonComponent.renderButton(btn, `${index}`);
}
);
}
public render(): JSX.Element {
if (!this.props.buttons || this.props.buttons.length < 1) {
return <React.Fragment />;
}
return <React.Fragment>{ControlBarComponent.renderButtons(this.props.buttons)}</React.Fragment>;
}
}

View File

@@ -1,28 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { ControlBarComponent } from "./ControlBarComponent";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class ControlBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private buttons: ko.ObservableArray<CommandButtonComponentProps>) {
this.buttons.subscribe(() => this.forceRender());
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <ControlBarComponent buttons={this.buttons()} />;
}
public forceRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -0,0 +1,16 @@
/**
* Interface for the data/content that will be recorded
*/
export interface ConsoleData {
type: ConsoleDataType;
date: string;
message: string;
id?: string;
}
export enum ConsoleDataType {
Info = 0,
Error = 1,
InProgress = 2,
}

View File

@@ -1,10 +1,7 @@
import { shallow } from "enzyme";
import React from "react";
import {
ConsoleDataType,
NotificationConsoleComponent,
NotificationConsoleComponentProps,
} from "./NotificationConsoleComponent";
import { ConsoleDataType } from "./ConsoleData";
import { NotificationConsoleComponent, NotificationConsoleComponentProps } from "./NotificationConsoleComponent";
describe("NotificationConsoleComponent", () => {
const createBlankProps = (): NotificationConsoleComponentProps => {

View File

@@ -6,7 +6,7 @@ import { Dropdown, IDropdownOption } from "@fluentui/react";
import * as React from "react";
import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear.svg";
import ClearIcon from "../../../../images/Clear-1.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
@@ -17,25 +17,7 @@ import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { userContext } from "../../../UserContext";
/**
* Log levels
*/
export enum ConsoleDataType {
Info = 0,
Error = 1,
InProgress = 2,
}
/**
* Interface for the data/content that will be recorded
*/
export interface ConsoleData {
type: ConsoleDataType;
date: string;
message: string;
id?: string;
}
import { ConsoleData, ConsoleDataType } from "./ConsoleData";
export interface NotificationConsoleComponentProps {
isConsoleExpanded: boolean;
@@ -323,14 +305,13 @@ const PrPreview = (props: { pr: string }) => {
);
};
export const NotificationConsole: React.FC<
Pick<NotificationConsoleComponentProps, "consoleData" | "inProgressConsoleDataIdToBeDeleted">
> = ({
consoleData,
inProgressConsoleDataIdToBeDeleted,
}: Pick<NotificationConsoleComponentProps, "consoleData" | "inProgressConsoleDataIdToBeDeleted">) => {
export const NotificationConsole: React.FC = () => {
const setIsExpanded = useNotificationConsole((state) => state.setIsExpanded);
const isExpanded = useNotificationConsole((state) => state.isExpanded);
const consoleData = useNotificationConsole((state) => state.consoleData);
const inProgressConsoleDataIdToBeDeleted = useNotificationConsole(
(state) => state.inProgressConsoleDataIdToBeDeleted
);
// TODO Refactor NotificationConsoleComponent into a functional component and remove this wrapper
// This component only exists so we can use hooks and pass them down to a non-functional component
return (

View File

@@ -21,7 +21,7 @@ import {
makeStateRecord,
makeTransformsRecord,
} from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { configOption, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable";
@@ -242,22 +242,27 @@ export class NotebookClientV2 {
);
// Additional configuration
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "monaco"));
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror"));
this.store.dispatch(
configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs)
);
createConfigCollection({
key: "monaco",
});
defineConfigOption({
label: "Show Line numbers",
key: "monaco.lineNumbers",
values: [
{ label: "Yes", value: true },
{ label: "No", value: false },
],
defaultValue: true,
});
this.store.dispatch(configOption("codeMirror.lineNumbers").action(true));
const readOnlyConfigOption = configOption("codeMirror.readOnly");
const readOnlyValue = params.isReadOnly ? "nocursor" : undefined;
if (!readOnlyConfigOption) {
defineConfigOption({
label: "Read-only",
key: "codeMirror.readOnly",
values: [
{ label: "Read-Only", value: "nocursor" },
{ label: "Not read-only", value: undefined },
],
defaultValue: readOnlyValue,
});
} else {
this.store.dispatch(readOnlyConfigOption.action(readOnlyValue));
}
}
/**

View File

@@ -1,10 +1,10 @@
.notebookComponentContainer {
text-transform:none;
line-height:1.28581;
letter-spacing:0;
font-size:14px;
font-weight:400;
color:#182026;
text-transform: none;
line-height: 1.28581;
letter-spacing: 0;
font-size: 14px;
font-weight: 400;
color: #182026;
height: 100%;
.hotKeys {

View File

@@ -277,6 +277,10 @@ export class NotebookComponentBootstrapper {
return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>);
}
public isNotebookUntrusted(): boolean {
return NotebookUtil.isNotebookUntrusted(this.getStore().getState(), this.contentRef);
}
/**
* For display purposes, only return non-killed kernels
*/

View File

@@ -1,12 +1,14 @@
import * as React from "react";
import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react";
import { connect } from "react-redux";
import { NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps {
kernelSpecName: string;
kernelStatus: string;
currentCellType: string;
isNotebookUntrusted: boolean;
onRender: () => void;
}
@@ -20,7 +22,8 @@ class VirtualCommandBarComponent extends React.Component<VirtualCommandBarCompon
return (
this.props.kernelStatus !== nextProps.kernelStatus ||
this.props.kernelSpecName !== nextProps.kernelSpecName ||
this.props.currentCellType !== nextProps.currentCellType
this.props.currentCellType !== nextProps.currentCellType ||
this.props.isNotebookUntrusted !== nextProps.isNotebookUntrusted
);
}
@@ -50,6 +53,7 @@ const makeMapStateToProps = (
kernelStatus,
kernelSpecName,
currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
} as VirtualCommandBarComponentProps;
}
@@ -69,6 +73,7 @@ const makeMapStateToProps = (
kernelStatus,
kernelSpecName,
currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
onRender: initialProps.onRender,
};
};

View File

@@ -2,7 +2,6 @@ import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import NotebookRenderer from "../../../NotebookRenderer/NotebookRenderer";
import * as TextFile from "./text-file";
@@ -32,14 +31,14 @@ interface FileProps {
export class File extends React.PureComponent<FileProps> {
getChoice = () => {
let choice = null;
let choice;
// notebooks don't report a mimetype so we'll use the content.type
if (this.props.type === "notebook") {
choice = <NotebookRenderer contentRef={this.props.contentRef} />;
} else if (this.props.type === "dummy") {
choice = null;
} else if (this.props.mimetype == null || !TextFile.handles(this.props.mimetype)) {
choice = undefined;
} else if (this.props.mimetype === undefined || !TextFile.handles(this.props.mimetype)) {
// This should not happen as we intercept mimetype upstream, but just in case
choice = (
<PaddedContainer>

View File

@@ -1,10 +1,10 @@
import * as StringUtils from "../../../../../Utils/StringUtils";
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
import * as StringUtils from "../../../../../Utils/StringUtils";
const EditorContainer = styled.div`
position: absolute;
@@ -37,7 +37,7 @@ interface TextFileState {
class EditorPlaceholder extends React.PureComponent<MonacoEditorProps> {
render(): JSX.Element {
// TODO: Show a little blocky placeholder
return null;
return undefined;
}
}
@@ -98,7 +98,7 @@ function makeMapStateToTextFileProps(
return {
contentRef,
mimetype: content.mimetype != null ? content.mimetype : "text/plain",
mimetype: content.mimetype !== undefined ? content.mimetype : "text/plain",
text,
};
};

View File

@@ -34,9 +34,11 @@ import {
import { webSocket } from "rxjs/webSocket";
import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants";
import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { useDialog } from "../../Controls/Dialog";
import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions";
import { NotebookUtil } from "../NotebookUtil";
@@ -685,10 +687,8 @@ const handleKernelConnectionLostEpic = (
logConsoleError(msg);
logFailureToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer;
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
useDialog.getState().showOkModalDialog("kernel restarts", msg);
return of(EMPTY);
}
@@ -772,15 +772,16 @@ const closeUnsupportedMimetypesEpic = (
ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap((action) => {
const mimetype = action.payload.model.mimetype;
const explorer = window.dataExplorer;
if (explorer && !TextFile.handles(mimetype)) {
if (!TextFile.handles(mimetype)) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.tabsManager.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
useTabs
.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.`;
explorer.showOkModalDialog("File cannot be rendered", msg);
useDialog.getState().showOkModalDialog("File cannot be rendered", msg);
logConsoleError(msg);
}
return EMPTY;
@@ -800,17 +801,16 @@ const closeContentFailedToFetchEpic = (
return action$.pipe(
ofType(actions.FETCH_CONTENT_FAILED),
mergeMap((action) => {
const explorer = window.dataExplorer;
if (explorer) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.tabsManager.closeTabsByComparator(
const filepath = action.payload.filepath;
// Close tab and show error message
useTabs
.getState()
.closeTabsByComparator(
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg);
logConsoleError(msg);
}
const msg = `Failed to load file: ${filepath}.`;
useDialog.getState().showOkModalDialog("Failure to load", msg);
logConsoleError(msg);
return EMPTY;
})
);

View File

@@ -6,26 +6,28 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean;
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
private onConnectionLost: () => void,
private onMemoryUsageInfoUpdate: (update: DataModels.MemoryUsageInfo) => void
) {
if (notebookServerInfo() && notebookServerInfo().notebookServerEndpoint) {
constructor(private onConnectionLost: () => void) {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else {
const subscription = notebookServerInfo.subscribe((newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo && newServerInfo.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
subscription.dispose();
});
const unsub = useNotebook.subscribe(
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
unsub();
},
(state) => state.notebookServerInfo
);
}
}
@@ -35,13 +37,14 @@ export class NotebookContainerClient {
private scheduleHeartbeat(delayMs: number): void {
setTimeout(() => {
this.getMemoryUsage()
.then((memoryUsageInfo) => this.onMemoryUsageInfoUpdate(memoryUsageInfo))
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
}, delayMs);
}
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";
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
return Promise.reject(error);
@@ -97,7 +100,8 @@ export class NotebookContainerClient {
}
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";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
return Promise.reject(error);
@@ -116,30 +120,28 @@ export class NotebookContainerClient {
}
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
let authToken: string,
notebookServerEndpoint = this.notebookServerInfo().notebookServerEndpoint,
token = this.notebookServerInfo().authToken;
if (token) {
authToken = `Token ${token}`;
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
return {
notebookServerEndpoint,
notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
authToken,
};
}
private async recreateNotebookWorkspaceAsync(): Promise<void> {
const explorer = window.dataExplorer;
const { databaseAccount } = userContext;
if (!databaseAccount?.id) {
throw new Error("DataExplorer not initialized");
}
const notebookWorkspaceManager = explorer.notebookWorkspaceManager;
try {
await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(databaseAccount?.id, "default");
await notebookWorkspaceManager.createNotebookWorkspaceAsync(databaseAccount?.id, "default");
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
await createOrUpdate(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
return Promise.reject(error);

View File

@@ -1,24 +1,31 @@
import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { cloneDeep } from "lodash";
import { AjaxResponse } from "rxjs/ajax";
import * as DataModels from "../../Contracts/DataModels";
import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContentClient {
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
public notebookBasePath: ko.Observable<string>,
private contentProvider: IContentProvider
) {}
constructor(private contentProvider: IContentProvider) {}
/**
* This updates the item and points all the children's parent to this 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) => {
item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item));
@@ -29,7 +36,7 @@ export class NotebookContentClient {
*
* @param parent parent folder
*/
public createNewNotebookFile(parent: NotebookContentItem): Promise<NotebookContentItem> {
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`);
}
@@ -50,6 +57,8 @@ export class NotebookContentClient {
const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) {
item.parent = parent;
parent.children.push(item);
@@ -59,18 +68,20 @@ export class NotebookContentClient {
});
}
public deleteContentItem(item: NotebookContentItem): Promise<void> {
return this.deleteNotebookFile(item.path).then((path: string) => {
if (!path || path !== item.path) {
throw new Error("No path provided");
}
public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
const path = await this.deleteNotebookFile(item.path);
useNotebook.getState().deleteNotebookItem(item, isGithubTree);
if (item.parent && item.parent.children) {
// Remove deleted child
const newChildren = item.parent.children.filter((child) => child.path !== path);
item.parent.children = newChildren;
}
});
// TODO: Delete once old resource tree is removed
if (!path || path !== item.path) {
throw new Error("No path provided");
}
if (item.parent && item.parent.children) {
// Remove deleted child
const newChildren = item.parent.children.filter((child) => child.path !== path);
item.parent.children = newChildren;
}
}
/**
@@ -82,7 +93,8 @@ export class NotebookContentClient {
public async uploadFileAsync(
name: string,
content: string,
parent: NotebookContentItem
parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`);
@@ -106,6 +118,8 @@ export class NotebookContentClient {
.then((xhr: AjaxResponse) => {
const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) {
item.parent = parent;
parent.children.push(item);
@@ -128,7 +142,11 @@ export class NotebookContentClient {
* @param sourcePath
* @param targetName is not prefixed with path
*/
public renameNotebook(item: NotebookContentItem, targetName: string): Promise<NotebookContentItem> {
public renameNotebook(
item: NotebookContentItem,
targetName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
const sourcePath = item.path;
// Match extension
if (sourcePath.indexOf(".") !== -1) {
@@ -154,6 +172,9 @@ export class NotebookContentClient {
item.name = notebookFile.name;
item.path = notebookFile.path;
item.timestamp = NotebookUtil.getCurrentTimestamp();
useNotebook.getState().updateNotebookItem(item, isGithubTree);
return item;
});
}
@@ -163,7 +184,11 @@ export class NotebookContentClient {
* @param parent
* @param newDirectoryName basename of the new directory
*/
public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise<NotebookContentItem> {
public async createDirectory(
parent: NotebookContentItem,
newDirectoryName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent is not a directory: ${parent.path}`);
}
@@ -190,8 +215,11 @@ export class NotebookContentClient {
const dir = xhr.response;
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
item.parent = parent;
parent.children?.push(item);
return item;
});
}
@@ -271,9 +299,10 @@ export class NotebookContentClient {
}
private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return {
endpoint: this.notebookServerInfo().notebookServerEndpoint,
token: this.notebookServerInfo().authToken,
endpoint: notebookServerInfo.notebookServerEndpoint,
token: notebookServerInfo.authToken,
crossDomain: true,
};
}

View File

@@ -4,13 +4,11 @@
import { ImmutableNotebook } from "@nteract/commutable";
import type { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react";
import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
@@ -20,8 +18,10 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getFullName } from "../../Utils/UserUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
@@ -30,6 +30,7 @@ import { SnapshotRequest } from "./NotebookComponent/types";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
import { useNotebook } from "./useNotebook";
type NotebookPaneContent = string | ImmutableNotebook;
@@ -37,7 +38,6 @@ export type { NotebookPaneContent };
export interface NotebookManagerOptions {
container: Explorer;
notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void;
refreshNotebookList: () => void;
@@ -81,23 +81,28 @@ export default class NotebookManager {
contents.JupyterContentProvider
);
this.notebookClient = new NotebookContainerClient(
this.params.container.notebookServerInfo,
() => this.params.container.initNotebooks(userContext?.databaseAccount),
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
this.notebookClient = new NotebookContainerClient(() =>
this.params.container.initNotebooks(userContext?.databaseAccount)
);
this.notebookContentClient = new NotebookContentClient(
this.params.container.notebookServerInfo,
this.params.notebookBasePath,
this.notebookContentProvider
);
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token);
if (this?.gitHubOAuthService.isLoggedIn()) {
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();
@@ -107,6 +112,7 @@ export default class NotebookManager {
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender();
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
});
this.refreshPinnedRepos();
}
@@ -138,6 +144,7 @@ export default class NotebookManager {
notebookContentRef={notebookContentRef}
onTakeSnapshot={onTakeSnapshot}
/>,
"440px",
onClosePanel
);
}
@@ -166,21 +173,33 @@ export default class NotebookManager {
if (error.status === HttpStatusCodes.Unauthorized) {
this.gitHubOAuthService.resetToken();
this.params.container.showOkCancelModalDialog(
undefined,
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
"Connect to GitHub",
() => this.params.container.openGitHubReposPanel("Connect to GitHub"),
"Cancel",
undefined
);
useDialog
.getState()
.showOkCancelModalDialog(
undefined,
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
"Connect to GitHub",
() =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={this.params.container}
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
junoClientProp={this.junoClient}
/>
),
"Cancel",
undefined
);
}
};
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
return new Promise<string>((resolve, reject) => {
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
this.params.container.showOkCancelModalDialog(
useDialog.getState().showOkCancelModalDialog(
title || "Commit",
undefined,
primaryButtonLabel || "Commit",

View File

@@ -1,56 +1,68 @@
.NotebookReadOnlyRender {
.nteract-cell-container {
margin-bottom: 10px;
}
.nteract-cell-container {
margin-bottom: 10px;
}
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
}
.nteract-cell {
padding: 0.5px;
border: 1px solid #ffffff;
border-left: 3px solid #ffffff;
}
.CodeMirror-scroll {
background-color: #f5f5f5;
}
.CodeMirror-scroll {
overflow: hidden !important;
}
.CodeMirror-lines {
cursor: default;
}
.CodeMirror-lines {
cursor: default;
}
.nteract-cell:hover {
border: 1px solid #0078d4;
border-left: 3px solid #0078d4;
.CodeMirror {
height: inherit;
}
.CodeMirror-scroll {
background-color: #ffffff;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #f5f5f5;
}
.nteract-cell-outputs {
border-top: 1px solid #d7d7d7;
}
.nteract-cell:hover {
border: 1px solid #0078d4;
border-left: 3px solid #0078d4;
.nteract-md-cell {
background-color: #ffffff;
}
.CodeMirror-scroll,
.CodeMirror-linenumber,
.CodeMirror-gutters {
background-color: #ffffff;
}
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
border-top: 1px solid #d7d7d7;
}
.nteract-md-cell {
background-color: #f5f5f5;
background-color: #ffffff;
}
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
.nteract-cell-outputs {
padding: 10px;
border-top: 1px solid #ffffff;
pre {
background-color: #ffffff;
border: none;
padding: 0px;
margin: 0px;
}
}
.nteract-md-cell {
background-color: #f5f5f5;
}
.nteract-cell:hover.nteract-md-cell {
background-color: #ffffff;
}
}

View File

@@ -1,6 +1,6 @@
import { actions, ContentRef } from "@nteract/core";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import * as React from "react";
@@ -67,8 +67,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
? () => <SandboxOutputs id={id} contentRef={contentRef} />
: undefined,
editor: {
monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
},
}}
</CodeCell>
@@ -84,8 +84,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<RawCell id={id} contentRef={contentRef} cell_type="raw">
{{
editor: {
monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor {...props} readOnly={true} editorType={"monaco"} />,
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
},
}}
</RawCell>

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