Compare commits

..

49 Commits

Author SHA1 Message Date
sunilyadav840
e230d286d9 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migration/move-document-tab-to-react 2021-07-26 11:42:02 +05:30
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
sunilyadav840
4e1cf23ac8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migration/move-document-tab-to-react 2021-07-23 20:37:19 +05:30
sunilyadav840
52807a9a0e Fixed sqldocument delete and update issue 2021-07-23 20:27:47 +05:30
victor-meng
fecac5625a Migrate resource tree for resource token to react (#956) 2021-07-22 17:11:19 -07:00
victor-meng
dc21032d69 Clean up EditTableEntityPanel (#955) 2021-07-22 16:18:48 -07:00
siddjoshi-ms
39a67dbc98 Sqlx estimated cost calculation (#925)
Adds cost estimate for the SqlX services in the Dedicated Gateway blade (queries the new FetchPrices API to retrieve price data)
2021-07-22 10:48:19 -07:00
Sunil Kumar Yadav
ed9cf01b50 Fixed resourse tree collapse issue (#953) 2021-07-22 11:12:35 -05:00
vaidankarswapnil
e443d17b2e Migrate Index page to React (#952) 2021-07-22 10:19:17 -05:00
victor-meng
401660ae15 Fix connect to github (#951) 2021-07-21 17:47:55 -07:00
Steve Faulkner
c5e4ee9c2b Upgrade Playwright (#939) 2021-07-21 19:32:53 -05:00
victor-meng
913fec4e69 Improve e2e stability (#949) 2021-07-21 16:22:31 -07:00
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
sunilyadav840
ea21ab2580 FIxed all PR change request 2021-07-19 18:11:54 +05:30
vaidankarswapnil
8a3929775b Strict mode SwitchAccount and SwitchSubscription files (#940) 2021-07-15 10:54:07 -05:00
vaidankarswapnil
b115bb34ca Fix GitHub repos panel issues (#875)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
Co-authored-by: Tanuj Mittal <tamitta@microsoft.com>
2021-07-15 09:44:25 -05:00
Sunil Kumar Yadav
397231dca2 Fixed typescript strict issue of Statusbar, hostedUtils, StringUtils etc (#785) 2021-07-15 09:35:51 -05:00
Hardikkumar Nai
0bbf9de963 Resolve ESLint Errors (#934) 2021-07-15 08:53:07 -05:00
Sunil Kumar Yadav
103b3bf6c9 fixed typescript strict of CqlUtilities.test.ts CapabilityUtils.ts file (#787)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-07-15 00:51:36 -05:00
Sunil Kumar Yadav
7dd8bd567f fixed typescript strict useSidePanel.ts PanelContainerComponent.tsx etc (#802)
* fixed typescript strict useSidePanel.ts PanelContainerComponent.tsx SchemaAnalyzerSplashScreen.tsx files

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

* Address feedback

* Format

* Update

* Add tests
2021-07-13 03:18:13 +05:30
Hardikkumar Nai
cfce78242c Remove Explorer.openBrowseQueriesPane (#889) 2021-07-12 09:40:43 -05:00
sunilyadav840
7d30268cd9 removed knockout DocumentTab file 2021-07-12 19:30:28 +05:30
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
sunilyadav840
42b7d8fe09 Merge branch 'master' 2021-07-12 01:26:43 +05:30
sunilyadav840
d9614cb751 refactor documentTab code 2021-07-12 01:03:03 +05:30
sunilyadav840
8f1f3c1c65 fixed mongo docuement create issue 2021-07-11 21:45:15 +05:30
victor-meng
f8ab0a82e0 Move tabs manager to zustand (#915) 2021-07-08 21:32:22 -07:00
Hardikkumar Nai
f4eef1b61b Resolve Eslint for Tables.Constants (#914)
* resolve_eslint_Tables.Constants

* correct spelling mistake
2021-07-08 21:10:29 -05:00
sunilyadav840
bbae8ae301 Refactor code 2021-07-08 17:37:18 +05:30
victor-meng
c486c1193e Fix scale component not showing in settings tab (#926) 2021-07-07 11:22:54 -07:00
victor-meng
db34024259 Move notebook flags to zustand (#912) 2021-07-06 15:21:23 -05:00
victor-meng
98d7bb37d5 Move resource token collection to useDatabases zustand store (#916) 2021-07-06 14:05:38 -05:00
Hardikkumar Nai
45d0b3f706 Resolve ESLint QueriesGridComponent (#923) 2021-07-06 09:12:41 -05:00
sunilyadav840
f4a24610fe Clean knockout code 2021-07-06 19:28:57 +05:30
sunilyadav840
a456318099 Add update delete sqlDocumentTab 2021-07-05 19:22:03 +05:30
sunilyadav840
c01a382b4f complete functionality documentMongoTab migration 2021-06-28 18:38:57 +05:30
sunilyadav840
4559496640 add load, create, update and delete Document 2021-06-24 17:57:31 +05:30
sunilyadav840
b4443ac89f configire documenttab in react 2021-06-16 11:19:37 +05:30
171 changed files with 7998 additions and 9934 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

@@ -71,7 +71,6 @@ 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
@@ -83,11 +82,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,17 +99,10 @@ src/Explorer/Notebook/NotebookContentClient.ts
src/Explorer/Notebook/NotebookContentItem.ts
src/Explorer/Notebook/NotebookUtil.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
@@ -141,17 +128,13 @@ 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/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/DocumentId.ts
@@ -160,96 +143,32 @@ 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/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/GraphVizComponent.tsx
@@ -257,46 +176,19 @@ 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

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

View File

@@ -3068,6 +3068,78 @@ settings-pane {
display: none;
height: 0px;
}
.react-editor {
height: 400px;
}
.documentTabSearchBar{
width: 80%;
margin: 15px;
}
.documentSqlTabSearchBar{
width: 68%;
margin: 15px;
}
.documentTabFiltetButton{
margin-top: 15px;
}
.filterSuggestions {
z-index: 1;
position: absolute;
top: 133px;
padding: 10px;
margin-left: 10px;
background: white;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
width: 20%;
height: auto;
}
.sqlFilterSuggestions {
margin-left: 10%;
}
.documentTabSuggestions {
padding: 5px;
cursor: pointer;
}
.documentTabNoFilterView {
margin: 15px;
}
.noFilterText {
margin-right: 10px;
}
.documentTabWatermark{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 10%;
margin-right: 12%;
}
.documentCreateText {
margin-top: 10px;
}
.documentLoadMore {
color: #0078D4;
font-size: 12px;
cursor: pointer;
margin-top: 10px;
}
.leftSplitter {
align-items: center;
text-align: center;
flex-direction: column;
}
.documentIdItem{
cursor: pointer;
}
.splitterWrapper .splitter-layout .layout-pane.layout-pane-primary{
max-height: 60%;
}
.queryText {
padding: 20px;
padding-left: 10px;
padding-right: 0px;
}
.spinner {
width: 100%;
position: absolute;

View File

@@ -208,4 +208,4 @@
}
.trigger-form {
padding: 10px 30px 10px 30px;
}
}

View File

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

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

29
package-lock.json generated
View File

@@ -5583,6 +5583,11 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
},
"@types/lodash": {
"version": "4.14.171",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz",
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg=="
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -20618,9 +20623,9 @@
}
},
"playwright": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.10.0.tgz",
"integrity": "sha512-b7SGBcCPq4W3pb4ImEDmNXtO0ZkJbZMuWiShsaNJd+rGfY/6fqwgllsAojmxGSgFmijYw7WxCoPiAIEDIH16Kw==",
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.13.0.tgz",
"integrity": "sha512-GA5OyEeKx1v/pRcANmYncCT67Y7Y4N5zLRU5E690dn/Id10sooR5hQZmCDYsjXlutZb/1q0R3sITALnvhEjCjg==",
"dev": true,
"requires": {
"commander": "^6.1.0",
@@ -20635,7 +20640,8 @@
"proxy-from-env": "^1.1.0",
"rimraf": "^3.0.2",
"stack-utils": "^2.0.3",
"ws": "^7.3.1"
"ws": "^7.4.6",
"yazl": "^2.5.1"
},
"dependencies": {
"commander": {
@@ -20667,6 +20673,12 @@
"requires": {
"escape-string-regexp": "^2.0.0"
}
},
"ws": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
"integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"dev": true
}
}
},
@@ -26157,6 +26169,15 @@
"fd-slicer": "~1.1.0"
}
},
"yazl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
"dev": true,
"requires": {
"buffer-crc32": "~0.2.3"
}
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -42,6 +42,7 @@
"@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",
@@ -163,7 +164,7 @@
"mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1",
"playwright": "1.10.0",
"playwright": "1.13.0",
"prettier": "2.2.1",
"raw-loader": "0.5.1",
"react-dev-utils": "11.0.4",

View File

@@ -94,7 +94,7 @@ 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";
}
export class AfecFeatures {

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

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

@@ -1,9 +1,9 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
const entityName: string = getEntityName();

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 {

View File

@@ -9,6 +9,7 @@ export interface DatabaseAccount {
export interface DatabaseAccountExtendedProperties {
documentEndpoint?: string;
disableLocalAuth?: boolean;
tableEndpoint?: string;
gremlinEndpoint?: string;
cassandraEndpoint?: string;
@@ -87,6 +88,9 @@ export interface SubscriptionPolicies {
}
export interface Resource {
_partitionKeyValue?: string;
_partitionKey?: string;
_attachments?: string;
_rid: string;
_self: string;
_etag: string;

View File

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

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

@@ -33,16 +33,20 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.createEditor(this.configureEditor.bind(this));
}
public componentDidUpdate(previous: EditorReactProps) {
if (this.props.content !== previous.content) {
this.editor.setValue(this.props.content);
}
public shouldComponentUpdate(): boolean {
return true;
}
public componentWillUnmount(): void {
this.selectionListener && this.selectionListener.dispose();
}
public componentDidUpdate(prevProps: EditorReactProps): void {
if (this.props.content !== prevProps.content) {
this.editor.setValue(this.props.content);
}
}
public render(): JSX.Element {
return (
<React.Fragment>
@@ -56,7 +60,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
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());
});

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

@@ -30,7 +30,7 @@ import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
const title: string = "Open Saved Queries";
const title = "Open Saved Queries";
export interface QueriesGridComponentProps {
queriesClient: QueriesClient;
@@ -196,9 +196,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,19 +214,15 @@ 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
) => {
onClick: async () => {
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,

View File

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

View File

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

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}

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

@@ -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,6 +8,7 @@ import * as React from "react";
import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
@@ -53,8 +54,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (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

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

View File

@@ -22,15 +22,22 @@ 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;
@@ -62,7 +69,7 @@ export function createStaticCommandBarButtons(
buttons.push(createDivider());
if (container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton);
@@ -76,7 +83,7 @@ export function createStaticCommandBarButtons(
buttons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" &&
container.isShellEnabled() &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
@@ -138,13 +145,13 @@ export function createContextCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (container.isShellEnabled()) {
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
@@ -269,7 +276,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled: container.isSynapseLinkUpdating(),
disabled: useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
@@ -279,9 +286,8 @@ 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,
@@ -413,7 +419,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,
@@ -446,12 +453,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,
};
}
@@ -475,15 +488,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,
@@ -501,15 +520,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,
@@ -536,10 +561,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,
@@ -554,12 +590,12 @@ function createStaticCommandBarButtonsForResourceToken(
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton(container);
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
const isResourceTokenCollectionNodeSelected: boolean =
container.resourceTokenCollection() &&
container.resourceTokenCollection().id() === selectedNodeState.selectedNode?.id();
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);
};

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
@@ -185,12 +183,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,5 +1,5 @@
import * as React from "react";
import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react";
import { connect } from "react-redux";
import * as NteractUtil from "../NTeractUtil";

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,6 +34,7 @@ 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";
@@ -776,9 +777,11 @@ const closeUnsupportedMimetypesEpic = (
if (explorer && !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);
logConsoleError(msg);
@@ -804,9 +807,11 @@ const closeContentFailedToFetchEpic = (
if (explorer) {
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 = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg);
logConsoleError(msg);

View File

@@ -8,25 +8,26 @@ 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
);
}
}
@@ -36,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);
@@ -98,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);
@@ -117,15 +120,11 @@ 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,
};
}

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));
@@ -59,18 +66,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): Promise<void> {
const path = await this.deleteNotebookFile(item.path);
useNotebook.getState().deleteNotebookItem(item);
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;
}
}
/**
@@ -271,9 +280,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";
@@ -22,6 +20,7 @@ import { userContext } from "../../UserContext";
import { getFullName } from "../../Utils/UserUtils";
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 +29,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 +37,6 @@ export type { NotebookPaneContent };
export interface NotebookManagerOptions {
container: Explorer;
notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void;
refreshNotebookList: () => void;
@@ -81,23 +80,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 +111,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 +143,7 @@ export default class NotebookManager {
notebookContentRef={notebookContentRef}
onTakeSnapshot={onTakeSnapshot}
/>,
"440px",
onClosePanel
);
}
@@ -170,7 +176,17 @@ export default class NotebookManager {
undefined,
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
"Connect to GitHub",
() => this.params.container.openGitHubReposPanel("Connect to GitHub"),
() =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={this.params.container}
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
junoClientProp={this.junoClient}
/>
),
"Cancel",
undefined
);

View File

@@ -5,7 +5,7 @@ import "./Prompt.less";
export const promptContent = (props: PassedPromptProps): JSX.Element => {
if (props.status === "busy") {
const stopButtonText: string = "Stop cell execution";
const stopButtonText = "Stop cell execution";
return (
<div
style={{ position: "sticky", width: "100%", maxHeight: "100%", left: 0, top: 0, zIndex: 300 }}
@@ -23,7 +23,7 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
</div>
);
} else if (props.isHovered) {
const playButtonText: string = "Run cell";
const playButtonText = "Run cell";
return (
<IconButton
className="runCellButton"

View File

@@ -1,6 +1,5 @@
import { shallow } from "enzyme";
import React from "react";
import { StatusBar } from "./StatusBar";
describe("StatusBar", () => {
@@ -28,8 +27,8 @@ describe("StatusBar", () => {
kernelSpecDisplayName: "javascript",
kernelStatus: "kernelStatus",
},
null,
null
undefined,
undefined
);
expect(shouldUpdate).toBe(true);
});
@@ -47,8 +46,8 @@ describe("StatusBar", () => {
kernelSpecDisplayName: "python3",
kernelStatus: "kernelStatus",
},
null,
null
undefined,
undefined
);
expect(shouldUpdate).toBe(true);
});

View File

@@ -2,6 +2,7 @@ import { AppState, ContentRef, selectors } from "@nteract/core";
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { StyleConstants } from "../../../Common/Constants";
interface Props {
@@ -12,8 +13,6 @@ interface Props {
const NOT_CONNECTED = "not connected";
import styled from "styled-components";
export const LeftStatus = styled.div`
float: left;
display: block;
@@ -80,7 +79,7 @@ interface InitialProps {
contentRef: ContentRef;
}
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
const makeMapStateToProps = (_initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
@@ -90,26 +89,26 @@ const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps)
return {
kernelStatus: NOT_CONNECTED,
kernelSpecDisplayName: "no kernel",
lastSaved: null,
lastSaved: undefined,
};
}
const kernelRef = content.model.kernelRef;
let kernel = null;
let kernel;
if (kernelRef) {
kernel = selectors.kernel(state, { kernelRef });
}
const lastSaved = content && content.lastSaved ? content.lastSaved : null;
const lastSaved = content && content.lastSaved ? content.lastSaved : undefined;
const kernelStatus = kernel != null && kernel.status != null ? kernel.status : NOT_CONNECTED;
const kernelStatus = kernel?.status || NOT_CONNECTED;
// TODO: We need kernels associated to the kernelspec they came from
// so we can pluck off the display_name and provide it here
let kernelSpecDisplayName = " ";
if (kernelStatus === NOT_CONNECTED) {
kernelSpecDisplayName = "no kernel";
} else if (kernel != null && kernel.kernelSpecName != null) {
} else if (kernel?.kernelSpecName) {
kernelSpecDisplayName = kernel.kernelSpecName;
} else if (content && content.type === "notebook") {
kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " ";

View File

@@ -27,7 +27,7 @@ interface DispatchProps {
moveCell: (destinationId: CellId, above: boolean) => void;
clearOutputs: () => void;
deleteCell: () => void;
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) => void;
takeNotebookSnapshot: (payload: SnapshotRequest) => void;
}
@@ -203,7 +203,7 @@ const mapDispatchToProps = (
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) =>
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data })),
takeNotebookSnapshot: (request: SnapshotRequest) => dispatch(cdbActions.takeNotebookSnapshot(request)),
});

View File

@@ -1,8 +1,7 @@
import { ContentRef } from "@nteract/core";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { ContentRef } from "@nteract/core";
import * as actions from "../../NotebookComponent/actions";
interface ComponentProps {
@@ -29,10 +28,7 @@ class HoverableCell extends React.Component<ComponentProps & DispatchProps> {
}
}
const mapDispatchToProps = (
dispatch: Dispatch,
{ id, contentRef }: { id: string; contentRef: ContentRef }
): DispatchProps => ({
const mapDispatchToProps = (dispatch: Dispatch, { id }: { id: string }): DispatchProps => ({
hover: () => dispatch(actions.setHoveredCell({ cellId: id })),
unHover: () => dispatch(actions.setHoveredCell({ cellId: undefined })),
});

View File

@@ -0,0 +1,235 @@
import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
interface NotebookState {
isNotebookEnabled: boolean;
isNotebooksEnabledForAccount: boolean;
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
isSynapseLinkUpdating: boolean;
memoryUsageInfo: DataModels.MemoryUsageInfo;
isShellEnabled: boolean;
notebookBasePath: string;
isInitializingNotebooks: boolean;
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void;
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
updateNotebookItem: (item: NotebookContentItem) => void;
deleteNotebookItem: (item: NotebookContentItem) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isNotebookEnabled: false,
isNotebooksEnabledForAccount: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
password: undefined,
endpoints: [],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
set({ isNotebooksEnabledForAccount: false });
return;
}
const firstWriteLocation =
databaseAccount?.properties?.writeLocations &&
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
body: JSON.stringify({
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
}),
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.contentType]: "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to fetch disallowed locations");
}
const disallowedLocations: string[] = await response.json();
if (!disallowedLocations) {
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: true });
return;
}
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: false });
}
},
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
const currentItem = root || get().myNotebooksContentRoot;
if (currentItem) {
if (currentItem.path === item.path && currentItem.name === item.name) {
return currentItem;
}
if (currentItem.children) {
for (const childItem of currentItem.children) {
const result = get().findItem(childItem, item);
if (result) {
return result;
}
}
}
}
return undefined;
},
updateNotebookItem: (item: NotebookContentItem): void => {
const root = cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item);
item.parent = parentItem;
set({ myNotebooksContentRoot: root });
},
deleteNotebookItem: (item: NotebookContentItem): void => {
const root = cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const myNotebooksContentRoot = {
name: "My Notebooks",
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos",
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
}
: undefined;
set({
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
});
if (get().notebookServerInfo?.notebookServerEndpoint) {
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
set({ myNotebooksContentRoot: updatedRoot });
if (updatedRoot?.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
updatedRoot.children.forEach((notebookItem) => {
switch (notebookItem.type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
}
},
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
if (gitHubNotebooksContentRoot) {
gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
children: [],
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
});
});
gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
set({ gitHubNotebooksContentRoot });
}
},
}));

View File

@@ -113,7 +113,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: "",
enableIndexing: true,
isSharded: userContext.apiType !== "Tables",
partitionKey: "",
partitionKey:
(userContext.features.partitionKeyDefault && userContext.apiType === "SQL") ||
(userContext.features.partitionKeyDefault && userContext.apiType === "Mongo")
? "/id"
: "",
enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
useHashV2: false,
@@ -413,6 +417,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</TooltipHost>
</Stack>
<Text variant="small" aria-label="pkDescription">
{this.getPartitionKeySubtext()}
</Text>
<input
type="text"
id="addCollection-partitionKeyValue"
@@ -807,6 +815,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return tooltipText;
}
private getPartitionKeySubtext(): string {
if (
userContext.features.partitionKeyDefault &&
(userContext.apiType === "SQL" || userContext.apiType === "Mongo")
) {
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
return subtext;
}
return "";
}
private getAnalyticalStorageTooltipContent(): JSX.Element {
return (
<Text variant="small">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,6 +120,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
}
}
useSidePanel.getState().closeSidePanel();
}
public resetData(): void {
@@ -144,11 +145,18 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
private setup(forceShowConnectToGitHub = false): void {
forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn()
? this.setupForConnectToGitHub()
? this.setupForConnectToGitHub(forceShowConnectToGitHub)
: this.setupForManageRepos();
}
private setupForConnectToGitHub(): void {
private setupForConnectToGitHub(forceShowConnectToGitHub: boolean): void {
if (forceShowConnectToGitHub) {
const newState = { ...this.state.gitHubReposState };
newState.showAuthorizeAccess = forceShowConnectToGitHub;
this.setState({
gitHubReposState: newState,
});
}
this.setState({
isExecuting: false,
});
@@ -368,46 +376,28 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
isLoading: true,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
};
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
branchesProps: {
...this.state.gitHubReposState.reposListProps.branchesProps,
[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]: this.branchesProps[item.key],
},
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
this.loadMoreBranches(item.repo);
} else {
if (this.isAddedRepo === false) {
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
}
}
});
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
branchesProps: {
...this.branchesProps,
},
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
this.isAddedRepo = false;
}

View File

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

View File

@@ -10,7 +10,6 @@
padding: 0 34px;
margin: 20px 0;
overflow: auto;
min-height: 30px;
& > :not(.collapsibleSection) {
margin-bottom: @DefaultSpace;
@@ -151,9 +150,6 @@
.backImageIcon {
margin-top: 8px;
}
.entityValueTextField {
margin: 24px;
}
.addEntityDatePicker {
max-width: 145px;
}

View File

@@ -4,8 +4,8 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useSidePanel } from "../../hooks/useSidePanel";
export interface PanelContainerProps {
headerText: string;
panelContent: JSX.Element;
headerText?: string;
panelContent?: JSX.Element;
isConsoleExpanded: boolean;
isOpen: boolean;
panelWidth?: string;
@@ -66,8 +66,8 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
);
}
private onDissmiss = (ev?: React.SyntheticEvent<HTMLElement>): void => {
if ((ev.target as HTMLElement).id === "notificationConsoleHeader") {
private onDissmiss = (ev?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
ev.preventDefault();
} else {
useSidePanel.getState().closeSidePanel();
@@ -85,11 +85,12 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
export const SidePanel: React.FC = () => {
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
const { isOpen, panelContent, headerText } = useSidePanel((state) => {
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
return {
isOpen: state.isOpen,
panelContent: state.panelContent,
headerText: state.headerText,
panelWidth: state.panelWidth,
};
});
// TODO Refactor PanelContainerComponent into a functional component and remove this wrapper
@@ -100,6 +101,7 @@ export const SidePanel: React.FC = () => {
panelContent={panelContent}
headerText={headerText}
isConsoleExpanded={isConsoleExpanded}
panelWidth={panelWidth}
/>
);
};

View File

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

View File

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

View File

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

View File

@@ -9,17 +9,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
@@ -27,21 +18,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
inProgressMessage="Creating directory "

View File

@@ -1,11 +1,11 @@
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
@@ -15,7 +15,7 @@ import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../.
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { PanelContainerComponent } from "../PanelContainerComponent";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import {
attributeNameLabel,
attributeValueLabel,
@@ -30,9 +30,7 @@ import {
getCassandraDefaultEntities,
getDefaultEntities,
getEntityValuePlaceholder,
getPanelTitle,
imageProps,
isValidEntities,
options,
} from "./Validators/EntityTableHelper";
@@ -61,7 +59,6 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
tableEntityListViewModel,
cassandraApiClient,
}: AddTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
@@ -70,6 +67,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
isEntityValuePanelOpen,
{ setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse },
] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
/* Get default and previous saved entity headers */
useEffect(() => {
@@ -98,19 +97,36 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
};
/* Add new entity attribute */
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
if (!isValidEntities(entities)) {
return undefined;
}
event.preventDefault();
const onSubmit = async (): Promise<void> => {
for (let i = 0; i < entities.length; i++) {
const { property, type } = entities[i];
if (property === "" || property === undefined) {
setFormError(`Property name cannot be empty. Please enter a property name`);
return;
}
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
}
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
try {
await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
handleError(errorMessage, "AddTableRow");
throw error;
} finally {
setIsExecuting(false);
}
closeSidePanel();
};
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -200,110 +216,80 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsEntityValuePanelTrue();
};
const renderPanelContent = (): JSX.Element => {
return (
<form className="panelFormWrapper">
<div className="panelFormWrapper">
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
<div className="paneFooter">
<div className="leftpanel-okbut">
<input
type="submit"
onClick={submit}
className="genericPaneSubmitBtn"
value={getButtonLabel(userContext.apiType)}
/>
</div>
</div>
</div>
</form>
);
};
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => {
return (
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
);
};
if (isEntityValuePanelOpen) {
return (
<PanelContainerComponent
headerText=""
onRenderNavigationContent={onRenderNavigationContent}
panelWidth="700px"
isOpen={true}
panelContent={
<TextField
multiline
rows={5}
className="entityValueTextField"
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value");
setEntityAttributeValue(newInput);
}}
/>
}
isConsoleExpanded={false}
/>
<Stack style={{ padding: "20px 34px" }}>
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
<TextField
multiline
rows={5}
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value");
setEntityAttributeValue(newInput);
}}
/>
</Stack>
);
}
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: getButtonLabel(userContext.apiType),
onSubmit,
};
return (
<PanelContainerComponent
headerText={getPanelTitle(userContext.apiType)}
panelWidth="700px"
isOpen={true}
panelContent={renderPanelContent()}
isConsoleExpanded={false}
/>
<RightPaneForm {...props}>
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
</RightPaneForm>
);
};

View File

@@ -1,11 +1,11 @@
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
@@ -14,7 +14,7 @@ import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { PanelContainerComponent } from "../PanelContainerComponent";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import {
attributeNameLabel,
attributeValueLabel,
@@ -29,7 +29,6 @@ import {
getEntityValuePlaceholder,
getFormattedTime,
imageProps,
isValidEntities,
options,
} from "./Validators/EntityTableHelper";
@@ -59,12 +58,13 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
tableEntityListViewModel,
cassandraApiClient,
}: EditTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({});
const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [
isEntityValuePanelOpen,
@@ -190,26 +190,44 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
return displayValue;
};
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
if (!isValidEntities(entities)) {
return undefined;
const onSubmit = async (): Promise<void> => {
for (let i = 0; i < entities.length; i++) {
const { property, type } = entities[i];
if (property === "" || property === undefined) {
setFormError(`Property name cannot be empty. Please enter a property name`);
return;
}
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
}
event.preventDefault();
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient;
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
queryTablesTab.collection,
originalDocumentData,
entity
);
await tableEntityListViewModel.updateCachedEntity(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
try {
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
queryTablesTab.collection,
originalDocumentData,
entity
);
await tableEntityListViewModel.updateCachedEntity(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
} catch (error) {
const errorMessage = getErrorMessage(error);
handleError(errorMessage, "EditTableRow");
throw error;
} finally {
setIsExecuting(false);
}
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
closeSidePanel();
};
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -299,109 +317,81 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
setIsEntityValuePanelTrue();
};
const renderPanelContent = (): JSX.Element => {
return (
<form className="panelFormWrapper">
<div className="panelFormWrapper">
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value?.toString()}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
{renderPanelFooter()}
</div>
</form>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<input type="submit" onClick={submit} className="genericPaneSubmitBtn" value="Update Entity" />
</div>
</div>
);
};
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => (
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
);
if (isEntityValuePanelOpen) {
return (
<PanelContainerComponent
headerText=""
onRenderNavigationContent={onRenderNavigationContent}
panelWidth="700px"
isOpen={true}
panelContent={
<TextField
multiline
rows={5}
className="entityValueTextField"
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
setEntityAttributeValue(newInput);
entityChange(newInput, selectedRow, "value");
}}
/>
}
isConsoleExpanded={false}
/>
<Stack style={{ padding: "20px 34px" }}>
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
<TextField
multiline
rows={5}
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
setEntityAttributeValue(newInput);
entityChange(newInput, selectedRow, "value");
}}
/>
</Stack>
);
}
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: "Update",
onSubmit,
};
return (
<PanelContainerComponent
headerText="Edit Table Entity"
panelWidth="700px"
isOpen={true}
panelContent={renderPanelContent()}
isConsoleExpanded={false}
/>
<RightPaneForm {...props}>
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value?.toString()}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
</RightPaneForm>
);
};

View File

@@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2
export const columnProps: Partial<IStackProps> = {
tokens: { childrenGap: 10 },
styles: { root: { width: 680 } },
styles: { root: { marginBottom: 8 } },
};
// helper functions
@@ -134,8 +134,8 @@ export const getEntityValuePlaceholder = (entityType: string | number): string =
export const isValidEntities = (entities: EntityRowType[]): boolean => {
for (let i = 0; i < entities.length; i++) {
const { property } = entities[i];
if (property === "" || property === undefined) {
const { property, type } = entities[i];
if (property === "" || property === undefined || !type) {
return false;
}
}
@@ -170,13 +170,6 @@ export const getDefaultEntities = (headers: string[], entityTypes: EntityType):
return defaultEntities;
};
export const getPanelTitle = (apiType: string): string => {
if (apiType === "Cassandra") {
return "Add Table Row";
}
return "Add Table Row";
};
export const getAddButtonLabel = (apiType: string): string => {
if (apiType === "Cassandra") {
return "Add Row";

View File

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

View File

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

View File

@@ -16,12 +16,16 @@ import CollectionIcon from "../../../images/tree-collection.svg";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { userContext } from "../../UserContext";
import { getCollectionName, getDatabaseName } from "../../Utils/APITypeUtils";
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
import { AddDatabasePanel } from "../Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@@ -61,8 +65,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() {
this.subscriptions.push(
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
{
dispose: useNotebook.subscribe(
() => this.setState({}),
(state) => state.isNotebookEnabled
),
},
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }
);
}
@@ -167,7 +176,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
</li>
))}
<li>
<a role="link" href={SplashScreen.seeMoreItemUrl} target="_blank" tabIndex={0}>
<a role="link" href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}>
{SplashScreen.seeMoreItemTitle}
</a>
</li>
@@ -210,7 +219,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
});
}
if (this.container.isNotebookEnabled()) {
if (useNotebook.getState().isNotebookEnabled) {
heroes.push({
iconSrc: NewNotebookIcon,
title: "New Notebook",
@@ -235,20 +244,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined);
},
title: "New SQL Query",
description: null,
description: undefined,
});
} else if (userContext.apiType === "Mongo") {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
},
title: "New Query",
description: null,
description: undefined,
});
}
@@ -256,8 +265,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
items.push({
iconSrc: OpenQueryIcon,
title: "Open Query",
description: null,
onClick: () => this.container.openBrowseQueriesPanel(),
description: undefined,
onClick: () =>
useSidePanel
.getState()
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.container} />),
});
}
@@ -265,22 +277,22 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
items.push({
iconSrc: NewStoredProcedureIcon,
title: "New Stored Procedure",
description: null,
description: undefined,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
});
}
/* Scale & Settings */
const isShared = useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
const isShared = useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const label = isShared ? "Settings" : "Scale & Settings";
items.push({
iconSrc: ScaleAndSettingsIcon,
title: label,
description: null,
description: undefined,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onSettingsClick();
@@ -290,8 +302,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
items.push({
iconSrc: AddDatabaseIcon,
title: "New " + getDatabaseName(),
description: null,
onClick: () => this.container.openAddDatabasePane(),
description: undefined,
onClick: () =>
useSidePanel
.getState()
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
});
}
@@ -342,19 +357,19 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
private createTipsItems(): SplashScreenItem[] {
return [
{
iconSrc: null,
iconSrc: undefined,
title: "Data Modeling",
description: "Learn more about modeling",
onClick: () => window.open(SplashScreen.dataModelingUrl),
},
{
iconSrc: null,
iconSrc: undefined,
title: "Cost & Throughput Calculation",
description: "Learn more about cost calculation",
onClick: () => window.open(SplashScreen.throughputEstimatorUrl),
},
{
iconSrc: null,
iconSrc: undefined,
title: "Configure automatic failover",
description: "Learn more about Cosmos DB high-availability",
onClick: () => window.open(SplashScreen.failoverUrl),

View File

@@ -1,4 +1,4 @@
export var TableType = {
export const TableType = {
String: "String",
Boolean: "Boolean",
Binary: "Binary",
@@ -9,7 +9,7 @@ export var TableType = {
Int64: "Int64",
};
export var CassandraType = {
export const CassandraType = {
Ascii: "Ascii",
Bigint: "Bigint",
Blob: "Blob",
@@ -27,12 +27,12 @@ export var CassandraType = {
Tinyint: "Tinyint",
};
export var ClauseRule = {
export const ClauseRule = {
And: "And",
Or: "Or",
};
export var Operator = {
export const Operator = {
EqualTo: "==",
GreaterThan: ">",
GreaterThanOrEqualTo: ">=",
@@ -42,7 +42,7 @@ export var Operator = {
Equal: "=",
};
export var ODataOperator = {
export const ODataOperator = {
EqualTo: "eq",
GreaterThan: "gt",
GreaterThanOrEqualTo: "ge",
@@ -51,7 +51,7 @@ export var ODataOperator = {
NotEqualTo: "ne",
};
export var timeOptions = {
export const timeOptions = {
lastHour: "Last hour",
last24Hours: "Last 24 hours",
last7Days: "Last 7 days",
@@ -62,7 +62,7 @@ export var timeOptions = {
custom: "Custom...",
};
export var htmlSelectors = {
export const htmlSelectors = {
dataTableSelector: "#storageTable",
dataTableAllRowsSelector: "#storageTable tbody tr",
dataTableHeadRowSelector: ".dataTable thead tr",
@@ -84,9 +84,9 @@ export var htmlSelectors = {
selectAllDropdownSelector: "#select-all-dropdown",
};
export var defaultHeader = " ";
export const defaultHeader = " ";
export var EntityKeyNames = {
export const EntityKeyNames = {
PartitionKey: "PartitionKey",
RowKey: "RowKey",
Timestamp: "Timestamp",
@@ -94,7 +94,7 @@ export var EntityKeyNames = {
Etag: "etag",
};
export var htmlAttributeNames = {
export const htmlAttributeNames = {
dataTableNameAttr: "name_attr",
dataTableContentTypeAttr: "contentType_attr",
dataTableSnapshotAttr: "snapshot_attr",
@@ -103,14 +103,14 @@ export var htmlAttributeNames = {
dataTableHeaderIndex: "data-column-index",
};
export var cssColors = {
export const cssColors = {
commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */,
};
export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
export var transparentColor = "transparent";
export const clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
export const transparentColor = "transparent";
export var keyCodes = {
export const keyCodes = {
RightClick: 3,
Enter: 13,
Esc: 27,
@@ -163,7 +163,7 @@ export var keyCodes = {
Dash: 189,
};
export var InputType = {
export const InputType = {
Text: "text",
// Chrome doesn't support datetime, instead, datetime-local is supported.
DateTime: "datetime-local",

View File

@@ -1,4 +1,5 @@
export function getQuotedCqlIdentifier(identifier: string): string {
// Added return type optional undefined because passing undefined from test cases.
export function getQuotedCqlIdentifier(identifier: string | undefined): string | undefined {
let result = identifier;
if (!identifier) {
return result;

View File

@@ -0,0 +1,145 @@
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import { IColumn, IImageProps, ImageFit } from "@fluentui/react";
import { Resource } from "../../../src/Contracts/DataModels";
import * as DataModels from "../../Contracts/DataModels";
import DocumentId from "../Tree/DocumentId";
export interface IDocumentsTabContentState {
columns: IColumn[];
isModalSelection: boolean;
isCompactMode: boolean;
announcedMessage?: string;
isSuggestionVisible: boolean;
filter: string;
isFilterOptionVisible: boolean;
isEditorVisible: boolean;
documentContent: string;
selectedSqlDocumentContent: string;
documentIds: Array<DocumentId>;
documentSqlIds: Array<Resource>;
selectedDocumentId?: DocumentId;
selectedSqlDocumentId?: DocumentId;
isEditorContentEdited: boolean;
isAllDocumentsVisible: boolean;
}
export interface IDocument {
value: string;
id: string;
}
export interface IButton {
visible: boolean;
enabled: boolean;
isSelected?: boolean;
}
export const imageProps: Partial<IImageProps> = {
imageFit: ImageFit.centerContain,
width: 40,
height: 40,
style: { marginTop: "15px" },
};
export function hasShardKeySpecified(
document: string,
partitionKey: DataModels.PartitionKey,
partitionKeyProperty: string
): boolean {
return Boolean(
extractPartitionKey(
document,
getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition
)
);
}
export function getPartitionKeyDefinition(
partitionKey: DataModels.PartitionKey,
partitionKeyProperty: string
): DataModels.PartitionKey {
if (partitionKey?.paths?.[0]?.indexOf("$v") > -1) {
// Convert BsonSchema2 to /path format
partitionKey = {
kind: partitionKey.kind,
paths: ["/" + partitionKeyProperty.replace(/\./g, "/")],
version: partitionKey.version,
};
}
return partitionKey;
}
export function formatDocumentContent(row: DocumentId): string {
const { partitionKeyProperty, partitionKeyValue, id } = row;
const documentContent = JSON.stringify({
_id: id(),
[partitionKeyProperty]: partitionKeyValue || "",
});
const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}");
return formattedDocumentContent;
}
export function formatSqlDocumentContent(row: Resource): string {
const { id, _rid, _self, _ts, _etag, _partitionKey, _attachments } = row;
const documentContent = JSON.stringify({
id: id || "",
_rid: _rid || "",
_self: _self || "",
_ts: _ts || "",
_etag: _etag || "",
_attachments: _attachments || "",
_partitionKey: _partitionKey || "",
});
const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}");
return formattedDocumentContent;
}
export function getFilterPlaceholder(isPreferredApiMongoDB: boolean): string {
const filterPlaceholder = isPreferredApiMongoDB
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.";
return filterPlaceholder;
}
export function getFilterSuggestions(isPreferredApiMongoDB: boolean): { value: string }[] {
const filterSuggestions = isPreferredApiMongoDB
? [{ value: `{"_id": "foo"}` }, { value: "{ qty: { $gte: 20 } }" }]
: [
{ value: 'WHERE c.id = "foo"' },
{ value: "ORDER BY c._ts DESC" },
{ value: 'WHERE c.id = "foo" ORDER BY c._ts DESC' },
];
return filterSuggestions;
}
export function getDocumentItems(
isPreferredApiMongoDB: boolean,
documentIds: Array<DocumentId>,
documentSqlIds: Array<Resource>,
isAllDocumentsVisible: boolean
): Array<DocumentId> | Array<Resource> {
if (isPreferredApiMongoDB) {
return isAllDocumentsVisible ? documentIds : documentIds.slice(0, 5);
}
return isAllDocumentsVisible ? documentSqlIds : documentSqlIds.slice(0, 5);
}
export const assignTabButtonVisibility = (
visible: boolean,
enabled: boolean
): { visible: boolean; enabled: boolean } => {
return {
visible,
enabled,
};
};
export const getfilterText = (isPreferredApiMongoDB: boolean, filter: string): string => {
if (isPreferredApiMongoDB) {
if (filter) {
return `Filter : ${filter}`;
}
return "No filter applied";
}
return `Select * from C ${filter}`;
};

View File

@@ -1,221 +0,0 @@
<div
class="tab-pane active tabdocuments flexContainer"
data-bind="
setTemplateReady: true,
attr:{
id: tabId
},
visible: isActive"
role="tabpanel"
>
<!-- ko if: false -->
<!-- Messagebox Ok Cancel- Start -->
<div class="messagebox-background">
<div class="messagebox">
<h2 class="messagebox-title">Title</h2>
<div class="messagebox-text" tabindex="0">Text</div>
<div class="messagebox-buttons">
<div class="messagebox-buttons-container">
<button value="ok" class="messagebox-button-primary">Ok</button>
<button value="cancel" class="messagebox-button-default">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Messagebox OK Cancel - End -->
<!-- /ko -->
<!-- Filter - Start -->
<div class="filterdivs" data-bind="visible: isFilterCreated">
<!-- Read-only Filter - Start -->
<div class="filterDocCollapsed" data-bind="visible: !isFilterExpanded() && !isPreferredApiMongoDB">
<span class="selectQuery">SELECT * FROM c</span>
<span class="appliedQuery" data-bind="text: appliedFilter"></span>
<button class="filterbtnstyle queryButton" data-bind="click: onShowFilterClick">Edit Filter</button>
</div>
<div
class="filterDocCollapsed"
data-bind="
visible: !isFilterExpanded() && isPreferredApiMongoDB"
>
<span
class="selectQuery"
data-bind="
visible: appliedFilter().length > 0"
>Filter :
</span>
<span
class="noFilterApplied"
data-bind="
visible: !appliedFilter().length > 0"
>No filter applied</span
>
<span class="appliedQuery" data-bind="text: appliedFilter"></span>
<button
class="filterbtnstyle queryButton"
data-bind="
click: onShowFilterClick"
>
Edit Filter
</button>
</div>
<!-- Read-only Filter - End -->
<!-- Editable Filter - start -->
<div
class="filterDocExpanded"
data-bind="
visible: isFilterExpanded"
>
<div>
<div class="editFilterContainer">
<span class="filterspan" data-bind="visible: !isPreferredApiMongoDB"> SELECT * FROM c </span>
<input
type="text"
list="filtersList"
class="querydropdown"
title="Type a query predicate or choose one from the list."
data-bind="
attr:{
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
},
css: { placeholderVisible: filterContent().length === 0 },
textInput: filterContent"
/>
<datalist
id="filtersList"
data-bind="
foreach: lastFilterContents"
>
<option
data-bind="
value: $data"
></option>
</datalist>
<span class="filterbuttonpad">
<button
class="filterbtnstyle queryButton"
data-bind="
click: refreshDocumentsGrid,
enable: applyFilterButton.enabled"
aria-label="Apply filter"
tabindex="0"
>
Apply Filter
</button>
</span>
<span
class="filterclose"
role="button"
aria-label="close filter"
tabindex="0"
data-bind="
click: onHideFilterClick, event: { keydown: onCloseButtonKeyDown }"
>
<img src="/close-black.svg" style="height: 14px; width: 14px" alt="Hide filter" />
</span>
</div>
</div>
</div>
<!-- Editable Filter - End -->
</div>
<!-- Filter - End -->
<!-- Ids and Editor - Start -->
<div class="documentsTabGridAndEditor">
<div class="documentsContainerWithSplitter" , data-bind="attr: { id: documentContentsContainerId }">
<div class="flexContainer">
<!-- Document Ids - Start -->
<div
class="documentsGridHeaderContainer tabdocuments scrollable"
data-bind="
attr: {
id: documentContentsGridId,
tabindex: documentIds().length <= 0 ? -1 : 0
},
style: { height: dataContentsGridScrollHeight },
event: { keydown: accessibleDocumentList.onKeyDown }"
>
<table id="tabsTable" class="table table-hover can-select dataTable">
<thead id="theadcontent">
<tr>
<th class="documentsGridHeader" data-bind="text: idHeader" tabindex="0"></th>
<!-- ko if: showPartitionKey -->
<th
class="documentsGridHeader documentsGridPartition evenlySpacedHeader"
data-bind="
attr: {
title: partitionKeyPropertyHeader
},
text: partitionKeyPropertyHeader"
tabindex="0"
></th>
<!-- /ko -->
<th
class="refreshColHeader"
role="button"
aria-label="Refresh documents"
data-bind="event: { keydown: onRefreshButtonKeyDown }"
>
<img
class="refreshcol"
src="/refresh-cosmos.svg"
data-bind="click: refreshDocumentsGrid"
alt="Refresh documents"
tabindex="0"
/>
</th>
</tr>
</thead>
<tbody id="tbodycontent">
<!-- ko foreach: documentIds -->
<tr
class="pointer accessibleListElement"
data-bind="
click: $data.click,
css: {
gridRowSelected: $parent.selectedDocumentId && $parent.selectedDocumentId() && $parent.selectedDocumentId().rid === $data.rid,
gridRowHighlighted: $parent.accessibleDocumentList.currentItem() && $parent.accessibleDocumentList.currentItem().rid === $data.rid
}"
tabindex="0"
>
<td class="tabdocumentsGridElement"><a data-bind="text: $data.id, attr: { title: $data.id }"></a></td>
<!-- ko if: $data.partitionKeyProperty -->
<td class="tabdocumentsGridElement" colspan="2">
<a
data-bind="text: $data.stringPartitionKeyValue, attr: { title: $data.stringPartitionKeyValue }"
></a>
</td>
<!-- /ko -->
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
<div class="loadMore">
<a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
>Load more</a
>
</div>
<!-- Document Ids - End -->
<!-- Splitter -->
</div>
<div class="splitter ui-resizable-handle ui-resizable-e colResizePointer" id="h_splitter2"></div>
</div>
<div class="documentWaterMark" data-bind="visible: !shouldShowEditor()">
<p><img src="/DocumentWaterMark.svg" alt="Document WaterMark" /></p>
<p class="documentWaterMarkText">Create new or work with existing document(s).</p>
</div>
<!-- Editor - Start -->
<json-editor
class="editorDivContent"
data-bind="visible: shouldShowEditor, css: { mongoDocumentEditor: isPreferredApiMongoDB }"
params="{content: initialDocumentContent, isReadOnly: false,lineNumbers: 'on',ariaLabel: 'Document editor',
updatedContent: selectedDocumentContent}"
></json-editor>
<!-- Editor - End -->
</div>
<!-- Ids and Editor - End -->
</div>

View File

@@ -1,152 +0,0 @@
import * as ko from "knockout";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
describe("Documents tab", () => {
describe("buildQuery", () => {
it("should generate the right select query for SQL API", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.buildQuery("")).toContain("select");
});
});
describe("showPartitionKey", () => {
const explorer = new Explorer();
const mongoExplorer = new Explorer();
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
container: explorer,
});
const collectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: explorer,
});
const collectionWithNonSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: false,
},
container: explorer,
});
const mongoCollectionWithSystemPartitionKey = <ViewModels.Collection>(<unknown>{
id: ko.observable<string>("foo"),
database: {
id: ko.observable<string>("foo"),
},
partitionKey: {
paths: ["/foo"],
kind: "Hash",
version: 2,
systemKey: true,
},
container: mongoExplorer,
});
it("should be false for null or undefined collection", () => {
const documentsTab = new DocumentsTab({
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be false for null or undefined partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithoutPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-Mongo accounts with system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
it("should be false for Mongo accounts with system partitionKey", () => {
updateUserContext({
apiType: "Mongo",
});
const documentsTab = new DocumentsTab({
collection: mongoCollectionWithSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(false);
});
it("should be true for non-system partitionKey", () => {
const documentsTab = new DocumentsTab({
collection: collectionWithNonSystemPartitionKey,
partitionKey: null,
documentIds: ko.observableArray<DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "",
tabPath: "",
});
expect(documentsTab.showPartitionKey).toBe(true);
});
});
});

View File

@@ -1,924 +0,0 @@
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import DiscardIcon from "../../../images/discard.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
import * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import DocumentId from "../Tree/DocumentId";
import { useSelectedNode } from "../useSelectedNode";
import template from "./DocumentsTab.html";
import TabsBase from "./TabsBase";
export default class DocumentsTab extends TabsBase {
public readonly html = template;
public selectedDocumentId: ko.Observable<DocumentId>;
public selectedDocumentContent: ViewModels.Editable<string>;
public initialDocumentContent: ko.Observable<string>;
public documentContentsGridId: string;
public documentContentsContainerId: string;
public filterContent: ko.Observable<string>;
public appliedFilter: ko.Observable<string>;
public lastFilterContents: ko.ObservableArray<string>;
public isFilterExpanded: ko.Observable<boolean>;
public isFilterCreated: ko.Observable<boolean>;
public applyFilterButton: ViewModels.Button;
public isEditorDirty: ko.Computed<boolean>;
public editorState: ko.Observable<ViewModels.DocumentExplorerState>;
public newDocumentButton: ViewModels.Button;
public saveNewDocumentButton: ViewModels.Button;
public saveExisitingDocumentButton: ViewModels.Button;
public discardNewDocumentChangesButton: ViewModels.Button;
public discardExisitingDocumentChangesButton: ViewModels.Button;
public deleteExisitingDocumentButton: ViewModels.Button;
public displayedError: ko.Observable<string>;
public accessibleDocumentList: AccessibleVerticalList;
public dataContentsGridScrollHeight: ko.Observable<string>;
public isPreferredApiMongoDB: boolean;
public shouldShowEditor: ko.Computed<boolean>;
public splitter: Splitter;
public showPartitionKey: boolean;
public idHeader: string;
// TODO need to refactor
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public documentIds: ko.ObservableArray<DocumentId>;
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
private _resourceTokenPartitionKey: string;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
this.documentContentsGridId = `documentContentsGrid${this.tabId}`;
this.documentContentsContainerId = `documentContentsContainer${this.tabId}`;
this.editorState = ko.observable<ViewModels.DocumentExplorerState>(
ViewModels.DocumentExplorerState.noDocumentSelected
);
this.selectedDocumentId = ko.observable<DocumentId>();
this.selectedDocumentContent = editable.observable<string>("");
this.initialDocumentContent = ko.observable<string>("");
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.documentIds = options.documentIds;
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: null;
this.isFilterExpanded = ko.observable<boolean>(false);
this.isFilterCreated = ko.observable<boolean>(true);
this.filterContent = ko.observable<string>("");
this.appliedFilter = ko.observable<string>("");
this.displayedError = ko.observable<string>("");
this.lastFilterContents = ko.observableArray<string>([
'WHERE c.id = "foo"',
"ORDER BY c._ts DESC",
'WHERE c.id = "foo" ORDER BY c._ts DESC',
]);
this.dataContentsGridScrollHeight = ko.observable<string>(null);
// initialize splitter only after template has been loaded so dom elements are accessible
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady) {
const tabContainer: HTMLElement = document.getElementById("content");
const splitterBounds: SplitterBounds = {
min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth,
max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth,
};
this.splitter = new Splitter({
splitterId: "h_splitter2",
leftId: this.documentContentsContainerId,
bounds: splitterBounds,
direction: SplitterDirection.Vertical,
});
}
});
this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds());
this.accessibleDocumentList.setOnSelect(
(selectedDocument: DocumentId) => selectedDocument && selectedDocument.click()
);
this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) =>
this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId)
);
this.documentIds.subscribe((newDocuments: DocumentId[]) => {
this.accessibleDocumentList.updateItemList(newDocuments);
if (newDocuments.length > 0) {
this.dataContentsGridScrollHeight(
newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
);
} else {
this.dataContentsGridScrollHeight(
DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
);
}
});
this.isEditorDirty = ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return false;
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
return true;
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return (
this.selectedDocumentContent.getEditableOriginalValue() !==
this.selectedDocumentContent.getEditableCurrentValue()
);
default:
return false;
}
});
this.newDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.saveNewDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
};
this.discardNewDocumentChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
return true;
}
return false;
}),
};
this.saveExisitingDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.discardExisitingDocumentChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.deleteExisitingDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return true;
}
return false;
}),
};
this.applyFilterButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.buildCommandBarOptions();
this.shouldShowEditor = ko.computed<boolean>(() => {
const documentHasContent: boolean =
this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0;
const isNewDocument: boolean =
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid;
return documentHasContent || isNewDocument;
});
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.showPartitionKey = this._shouldShowPartitionKey();
}
private _shouldShowPartitionKey(): boolean {
if (!this.collection) {
return false;
}
if (!this.collection.partitionKey) {
return false;
}
if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) {
return false;
}
return true;
}
public onShowFilterClick(): Q.Promise<any> {
this.isFilterCreated(true);
this.isFilterExpanded(true);
$(".filterDocExpanded").addClass("active");
$("#content").addClass("active");
$(".querydropdown").focus();
return Q();
}
public onHideFilterClick(): Q.Promise<any> {
this.isFilterExpanded(false);
$(".filterDocExpanded").removeClass("active");
$("#content").removeClass("active");
$(".queryButton").focus();
return Q();
}
public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.onHideFilterClick();
event.stopPropagation();
return false;
}
return true;
};
public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid
this.documentIds([]);
try {
// reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage();
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus();
} catch (error) {
window.alert(getErrorMessage(error));
}
}
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.refreshDocumentsGrid();
event.stopPropagation();
return false;
}
return true;
};
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
}
public onNewDocumentClick = (): Q.Promise<any> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
return Q();
}
this.selectedDocumentId(null);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
return Q();
};
public onSaveNewDocumentClick = (): Promise<any> => {
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
const document = JSON.parse(this.selectedDocumentContent());
this.isExecuting(true);
return createDocument(this.collection, document)
.then(
(savedDocument: any) => {
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.initialDocumentContent(value);
const partitionKeyValueArray = extractPartitionKey(
savedDocument,
this.partitionKey as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
let id = new DocumentId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
this.selectedDocumentId(id);
this.documentIds(ids);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
TelemetryProcessor.traceSuccess(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onRevertNewDocumentClick = (): Q.Promise<any> => {
this.initialDocumentContent("");
this.selectedDocumentContent("");
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
return Q();
};
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent());
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
selectedDocumentId.partitionKeyValue = partitionKeyValue;
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
this.isExecuting(true);
return updateDocument(this.collection, selectedDocumentId, documentContent)
.then(
(updatedDocument: any) => {
const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.initialDocumentContent(value);
this.documentIds().forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) {
documentId.id(updatedDocument.id);
}
});
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
TelemetryProcessor.traceSuccess(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onRevertExisitingDocumentClick = (): Q.Promise<any> => {
this.selectedDocumentContent.setBaseline(this.initialDocumentContent());
this.initialDocumentContent.valueHasMutated();
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
};
public onDeleteExisitingDocumentClick = async (): Promise<void> => {
const selectedDocumentId = this.selectedDocumentId();
const msg = !this.isPreferredApiMongoDB
? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?";
if (window.confirm(msg)) {
await this._deleteDocument(selectedDocumentId);
}
};
public onValidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
) {
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid);
return Q();
}
public onInvalidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid
) {
this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid);
return Q();
}
if (
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits ||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid
) {
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid);
return Q();
}
return Q();
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
public async onActivate(): Promise<void> {
super.onActivate();
if (!this._documentsIterator) {
try {
this._documentsIterator = this.createIterator();
await this.loadNextPage();
} catch (error) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
}
}
private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg);
};
protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection, documentId);
}
private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
this.isExecuting(true);
return this.__deleteDocument(selectedDocumentId)
.then(
() => {
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
this.selectedDocumentContent("");
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
TelemetryProcessor.traceSuccess(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
console.error(error);
TelemetryProcessor.traceFailure(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
}
public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter);
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey;
}
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
}
public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId);
const content = await readDocument(this.collection, documentId);
this.initDocumentEditor(documentId, content);
}
public loadNextPage(): Q.Promise<any> {
this.isExecuting(true);
this.isExecutionError(false);
return this._loadNextPageInternal()
.then(
(documentsIdsResponse = []) => {
const currentDocuments = this.documentIds();
const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid);
const nextDocumentIds = documentsIdsResponse
// filter documents already loaded in observable
.filter((d: any) => {
return currentDocumentsRids.indexOf(d._rid) < 0;
})
// map raw response to view model
.map((rawDocument: any) => {
const partitionKeyValue = rawDocument._partitionKeyValue;
return new DocumentId(this, rawDocument, partitionKeyValue);
});
const merged = currentDocuments.concat(nextDocumentIds);
this.documentIds(merged);
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
logConsoleError(errorMessage);
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
)
.finally(() => this.isExecuting(false));
}
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
if (event.key === " " || event.key === "Enter") {
const focusElement = document.getElementById(this.documentContentsGridId);
this.loadNextPage();
focusElement && focusElement.focus();
event.stopPropagation();
event.preventDefault();
}
};
protected _loadNextPageInternal(): Q.Promise<DataModels.DocumentId[]> {
return Q(this._documentsIterator.fetchNext().then((response) => response.resources));
}
protected _onEditorContentChange(newContent: string) {
try {
let parsed: any = JSON.parse(newContent);
this.onValidDocumentEdit();
} catch (e) {
this.onInvalidDocumentEdit();
}
}
public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise<any> {
if (documentId) {
const content: string = this.renderObjectForEditor(documentContent, null, 4);
this.selectedDocumentContent.setBaseline(content);
this.initialDocumentContent(content);
const newState = documentId
? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits
: ViewModels.DocumentExplorerState.newDocumentValid;
this.editorState(newState);
}
return Q();
}
public buildQuery(filter: string): string {
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey);
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
if (this.newDocumentButton.visible()) {
buttons.push({
iconSrc: NewDocumentIcon,
iconAlt: label,
onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.newDocumentButton.enabled(),
});
}
if (this.saveNewDocumentButton.visible()) {
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveNewDocumentButton.enabled(),
});
}
if (this.discardNewDocumentChangesButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardNewDocumentChangesButton.enabled(),
});
}
if (this.saveExisitingDocumentButton.visible()) {
const label = "Update";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveExisitingDocumentButton.enabled(),
});
}
if (this.discardExisitingDocumentChangesButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardExisitingDocumentChangesButton.enabled(),
});
}
if (this.deleteExisitingDocumentButton.visible()) {
const label = "Delete";
buttons.push({
iconSrc: DeleteDocumentIcon,
iconAlt: label,
onCommandClick: this.onDeleteExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.deleteExisitingDocumentButton.enabled(),
});
}
if (!this.isPreferredApiMongoDB) {
buttons.push(DocumentsTab._createUploadButton(this.collection.container));
}
return buttons;
}
protected buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([
this.newDocumentButton.visible,
this.newDocumentButton.enabled,
this.saveNewDocumentButton.visible,
this.saveNewDocumentButton.enabled,
this.discardNewDocumentChangesButton.visible,
this.discardNewDocumentChangesButton.enabled,
this.saveExisitingDocumentButton.visible,
this.saveExisitingDocumentButton.enabled,
this.discardExisitingDocumentChangesButton.visible,
this.discardExisitingDocumentChangesButton.enabled,
this.deleteExisitingDocumentButton.visible,
this.deleteExisitingDocumentButton.enabled,
])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
private _getPartitionKeyPropertyHeader(): string {
return (
(this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0]) ||
null
);
}
public static _createUploadButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload Item";
return {
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
};
}
}

View File

@@ -0,0 +1,50 @@
import * as ko from "knockout";
import React from "react";
import "react-splitter-layout/lib/index.css";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import DocumentsTabContent from "./DocumentsTabContent";
import TabsBase from "./TabsBase";
export default class DocumentsTab extends TabsBase {
public documentContentsGridId: string;
public documentContentsContainerId: string;
public displayedError: ko.Observable<string>;
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public _resourceTokenPartitionKey: string;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.documentContentsGridId = `documentContentsGrid${this.tabId}`;
this.documentContentsContainerId = `documentContentsContainer${this.tabId}`;
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: undefined;
this.displayedError = ko.observable<string>("");
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons();
}
private _getPartitionKeyPropertyHeader(): string {
return this.partitionKey?.paths?.[0];
}
render(): JSX.Element {
return <DocumentsTabContent {...this} />;
}
}

View File

@@ -0,0 +1,793 @@
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import {
DetailsList,
DetailsListLayoutMode,
IColumn,
IIconProps,
Image,
List,
PrimaryButton,
SelectionMode,
Stack,
Text,
TextField,
} from "@fluentui/react";
import * as React from "react";
import SplitterLayout from "react-splitter-layout";
import CloseIcon from "../../../images/close-black.svg";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import DiscardIcon from "../../../images/discard.svg";
import DocumentWaterMark from "../../../images/DocumentWaterMark.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
import { Resource } from "../../../src/Contracts/DataModels";
import * as NotificationConsoleUtils from "../../../src/Utils/NotificationConsoleUtils";
import { Areas } from "../../Common/Constants";
import { createDocument as createSqlDocuments } from "../../Common/dataAccess/createDocument";
import { deleteDocument as deleteSqlDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments as querySqlDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { updateDocument as updateSqlDocuments } from "../../Common/dataAccess/updateDocument";
import { getEntityName } from "../../Common/DocumentUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { logError } from "../../Common/Logger";
import { createDocument, deleteDocument, queryDocuments, updateDocument } from "../../Common/MongoProxyClient";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId";
import { useSelectedNode } from "../useSelectedNode";
import DocumentsTab from "./DocumentsTab";
import {
assignTabButtonVisibility,
formatDocumentContent,
formatSqlDocumentContent,
getDocumentItems,
getFilterPlaceholder,
getFilterSuggestions,
getfilterText,
getPartitionKeyDefinition,
hasShardKeySpecified,
IDocumentsTabContentState,
imageProps,
} from "./DocumentTabUtils";
const filterIcon: IIconProps = { iconName: "Filter" };
let newDocumentButton = assignTabButtonVisibility(true, true);
let saveNewDocumentButton = assignTabButtonVisibility(false, true);
let discardNewDocumentChangesButton = assignTabButtonVisibility(false, false);
let saveExisitingDocumentButton = assignTabButtonVisibility(false, false);
let discardExisitingDocumentChangesButton = assignTabButtonVisibility(false, false);
let deleteExisitingDocumentButton = assignTabButtonVisibility(false, false);
export default class DocumentsTabContent extends React.Component<DocumentsTab, IDocumentsTabContentState> {
public initialDocumentContent: string;
constructor(props: DocumentsTab) {
super(props);
const isPreferredApiMongoDB = userContext.apiType === "Mongo";
const columns: IColumn[] = [
{
key: "_id",
name: isPreferredApiMongoDB ? "_id" : "id",
minWidth: 50,
maxWidth: 100,
isResizable: true,
isCollapsible: true,
data: "string",
onRender: (item: DocumentId) => {
return (
<div onClick={() => this.handleRow(item)} className="documentIdItem">
{isPreferredApiMongoDB ? item.id() : item.id}
</div>
);
},
isPadded: true,
},
{
key: "column2",
name: props.partitionKeyPropertyHeader,
minWidth: 50,
maxWidth: 100,
isResizable: true,
isCollapsible: true,
data: "number",
onRender: (item: DocumentId) => {
return (
<div onClick={() => this.handleRow(item)} className="documentIdItem">
{isPreferredApiMongoDB ? item.partitionKeyValue : item._partitionKeyValue}
</div>
);
},
},
];
this.initialDocumentContent = `{ \n ${
isPreferredApiMongoDB ? '"_id"' : '"id"'
}: "replace_with_new_document_id" \n }`;
this.state = {
columns: columns,
isModalSelection: false,
isCompactMode: false,
announcedMessage: undefined,
isSuggestionVisible: false,
filter: "",
isFilterOptionVisible: true,
isEditorVisible: false,
documentContent: this.initialDocumentContent,
documentIds: [],
documentSqlIds: [],
isEditorContentEdited: false,
isAllDocumentsVisible: false,
selectedSqlDocumentContent: this.initialDocumentContent,
};
}
componentDidMount(): void {
this.props.isExecuting(true);
this.updateTabButton();
if (userContext.apiType === "Mongo") {
this.queryDocumentsData();
} else {
this.querySqlDocumentsData();
}
}
public buildQuery(filter: string): string {
return QueryUtils.buildDocumentsQuery(filter, this.props.partitionKeyProperty, this.props.partitionKey);
}
private querySqlDocumentsData = async (): Promise<void> => {
this.props.isExecuting(true);
this.props.isExecutionError(false);
const { filter } = this.state;
const query: string = this.buildQuery(filter);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
if (this.props._resourceTokenPartitionKey) {
options.partitionKey = this.props._resourceTokenPartitionKey;
}
try {
const sqlQuery = querySqlDocuments(this.props.collection.databaseId, this.props.collection.id(), query, options);
const querySqlDocumentsData = await sqlQuery.fetchNext();
this.setState({ documentSqlIds: querySqlDocumentsData.resources?.length ? querySqlDocumentsData.resources : [] });
} catch (error) {
this.props.isExecutionError(true);
const errorMessage = getErrorMessage(error);
NotificationConsoleUtils.logConsoleError(errorMessage);
} finally {
this.props.isExecuting(false);
}
};
private queryDocumentsData = async (): Promise<void> => {
this.props.isExecuting(true);
this.props.isExecutionError(false);
try {
const { filter } = this.state;
const query: string = filter || "{}";
const queryDocumentsData = await queryDocuments(
this.props.collection.databaseId,
this.props.collection as ViewModels.Collection,
true,
query,
undefined
);
if (queryDocumentsData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nextDocumentIds = queryDocumentsData.documents.map((rawDocument: any) => {
const partitionKeyValue = rawDocument._partitionKeyValue;
return new DocumentId(this.props as DocumentsTab, rawDocument, partitionKeyValue);
});
this.setState({ documentIds: nextDocumentIds });
}
if (this.props.onLoadStartKey !== undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseName: this.props.collection.databaseId,
collectionName: this.props.collection.id(),
dataExplorerArea: Areas.Tab,
tabTitle: this.props.tabTitle(),
},
this.props.onLoadStartKey
);
}
} catch (error) {
if (this.props.onLoadStartKey !== undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.props.collection.databaseId,
collectionName: this.props.collection.id(),
dataExplorerArea: Areas.Tab,
tabTitle: this.props.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
this.props.onLoadStartKey
);
}
} finally {
this.props.isExecuting(false);
}
};
private handleRow = (row: DocumentId | Resource): void => {
if (this.state.isEditorContentEdited) {
this.props.collection.container.showOkCancelModalDialog(
"Are you sure you want to continue?",
"Your unsaved changes will be lost.",
"Okay",
() => {
this.handleRowContent(row);
this.setState({ isEditorContentEdited: false });
return;
},
"Cancel",
undefined
);
} else {
this.handleRowContent(row);
}
};
private handleRowContent = (row: DocumentId | Resource): void => {
userContext.apiType === "Mongo"
? this.updateContent(row as DocumentId, formatDocumentContent(row as DocumentId))
: this.updateSqlContent(row as Resource);
this.setDefaultUpdateTabButtonVisibility();
};
private updateContent = (row: DocumentId, formattedDocumentContent: string): void => {
this.setState(
{
documentContent: formattedDocumentContent,
isEditorVisible: true,
selectedDocumentId: row,
},
() => {
this.updateTabButton();
}
);
};
private updateSqlContent = async (row: Resource): Promise<void> => {
const selectedDocumentId: DocumentId = new DocumentId(this.props as DocumentsTab, row, row._partitionKeyValue);
const content = await readDocument(this.props.collection, selectedDocumentId);
const formattedDocumentContent = formatSqlDocumentContent((content as unknown) as Resource);
this.setState(
{
documentContent: formattedDocumentContent,
isEditorVisible: true,
selectedSqlDocumentContent: formattedDocumentContent,
selectedSqlDocumentId: selectedDocumentId,
},
() => {
this.updateTabButton();
}
);
};
private handleFilter = (): void => {
userContext.apiType === "Mongo" ? this.queryDocumentsData() : this.querySqlDocumentsData();
this.setState({
isSuggestionVisible: false,
});
};
async updateSqlDocument(): Promise<void> {
const { isExecutionError, isExecuting, collection } = this.props;
const { documentContent, selectedSqlDocumentId } = this.state;
isExecutionError(false);
const startKey: number = this.getStartKey(Action.UpdateDocument);
try {
isExecuting(true);
const updateSqlDocumentRes = await updateSqlDocuments(
collection as ViewModels.Collection,
selectedSqlDocumentId,
JSON.parse(documentContent)
);
if (updateSqlDocumentRes) {
this.setTraceSuccess(Action.UpdateDocument, startKey);
this.querySqlDocumentsData();
}
} catch (error) {
NotificationConsoleUtils.logConsoleError(getErrorMessage(error));
this.setTraceFail(Action.UpdateDocument, startKey, error);
}
}
private async updateMongoDocument(): Promise<void> {
const { selectedDocumentId, documentContent, documentIds } = this.state;
const { isExecutionError, isExecuting, collection, partitionKey, partitionKeyProperty } = this.props;
isExecutionError(false);
isExecuting(true);
const startKey: number = this.getStartKey(Action.UpdateDocument);
try {
const updatedDocument = await updateDocument(
collection.databaseId,
collection as ViewModels.Collection,
selectedDocumentId,
documentContent
);
documentIds.forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) {
const partitionKeyArray = extractPartitionKey(
updatedDocument,
getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
const id = new ObjectId(this.props as DocumentsTab, updatedDocument, partitionKeyValue);
documentId.id(id.id());
}
});
this.setTraceSuccess(Action.UpdateDocument, startKey);
this.setState({ isEditorContentEdited: false });
} catch (error) {
NotificationConsoleUtils.logConsoleError(getErrorMessage(error));
this.setTraceFail(Action.UpdateDocument, startKey, error);
}
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = `New ${getEntityName()}`;
if (newDocumentButton.visible) {
buttons.push({
iconSrc: NewDocumentIcon,
iconAlt: label,
onCommandClick: this.onNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !newDocumentButton.enabled,
});
}
if (saveNewDocumentButton.visible) {
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !saveNewDocumentButton.enabled,
});
}
if (discardNewDocumentChangesButton.visible) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !discardNewDocumentChangesButton.enabled,
});
}
if (saveExisitingDocumentButton.visible) {
const label = "Update";
buttons.push({
...this,
updateMongoDocument: this.updateMongoDocument,
updateSqlDocument: this.updateSqlDocument,
setState: this.setState,
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !saveExisitingDocumentButton.enabled,
});
}
if (discardExisitingDocumentChangesButton.visible) {
const label = "Discard";
buttons.push({
...this,
setState: this.setState,
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !discardExisitingDocumentChangesButton.enabled,
});
}
if (deleteExisitingDocumentButton.visible) {
const label = "Delete";
buttons.push({
...this,
setState: this.setState,
iconSrc: DeleteDocumentIcon,
iconAlt: label,
onCommandClick: this.onDeleteExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !deleteExisitingDocumentButton.enabled,
});
}
if (userContext.apiType !== "Mongo") {
const { collection } = this.props;
const label = "Upload Item";
buttons.push({
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && collection.container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
});
}
return buttons;
}
private onSaveExisitingDocumentClick(): void {
userContext.apiType === "Mongo" ? this.updateMongoDocument() : this.updateSqlDocument();
}
private async onDeleteExisitingDocumentClick(): Promise<void> {
const confirmationMessage = `Are you sure you want to delete the selected ${getEntityName()} ?`;
const { isExecuting, collection } = this.props;
const startKey: number = this.getStartKey(Action.DeleteDocument);
this.props.collection.container.showOkCancelModalDialog(
confirmationMessage,
`This ${getEntityName()} will be deleted immediately. You can't undo this action`,
"Delete",
async () => {
try {
isExecuting(true);
if (userContext.apiType === "Mongo") {
await deleteDocument(
collection.databaseId,
collection as ViewModels.Collection,
this.state.selectedDocumentId
);
this.queryDocumentsData();
} else {
const { selectedSqlDocumentId } = this.state;
await deleteSqlDocument(collection as ViewModels.Collection, selectedSqlDocumentId);
this.querySqlDocumentsData();
}
this.setTraceSuccess(Action.DeleteDocument, startKey);
this.setState({ isEditorVisible: false });
} catch (error) {
this.setTraceFail(Action.DeleteDocument, startKey, error);
}
},
"Cancel",
undefined
);
}
private onRevertExisitingDocumentClick(): void {
const { selectedDocumentId, selectedSqlDocumentContent } = this.state;
const documentContent =
userContext.apiType === "Mongo" ? formatDocumentContent(selectedDocumentId) : selectedSqlDocumentContent;
this.setState({
documentContent: documentContent,
});
discardExisitingDocumentChangesButton = assignTabButtonVisibility(true, false);
saveExisitingDocumentButton = assignTabButtonVisibility(true, false);
this.updateTabButton();
}
private onNewDocumentClick = () => {
newDocumentButton = assignTabButtonVisibility(true, false);
saveNewDocumentButton = assignTabButtonVisibility(true, true);
discardNewDocumentChangesButton = assignTabButtonVisibility(true, true);
saveExisitingDocumentButton = assignTabButtonVisibility(false, false);
discardExisitingDocumentChangesButton = assignTabButtonVisibility(false, false);
deleteExisitingDocumentButton = assignTabButtonVisibility(false, false);
this.updateTabButton();
this.setState({
documentContent: this.initialDocumentContent,
isEditorVisible: true,
});
};
private onSaveNewDocumentClick = () => {
if (userContext.apiType === "Mongo") {
this.onSaveNewMongoDocumentClick();
} else {
this.onSaveSqlNewMongoDocumentClick();
}
};
public onSaveSqlNewMongoDocumentClick = async (): Promise<void> => {
const { isExecutionError, isExecuting, collection } = this.props;
isExecutionError(false);
const startKey: number = this.getStartKey(Action.CreateDocument);
const document = JSON.parse(this.state.documentContent);
isExecuting(true);
try {
const savedDocument = await createSqlDocuments(collection, document);
if (savedDocument) {
const formattedDocumentContent = formatSqlDocumentContent((savedDocument as unknown) as Resource);
this.setState({ documentContent: formattedDocumentContent });
this.setDefaultUpdateTabButtonVisibility();
this.setTraceSuccess(Action.CreateDocument, startKey);
}
this.querySqlDocumentsData();
} catch (error) {
NotificationConsoleUtils.logConsoleError(getErrorMessage(error));
this.setTraceFail(Action.CreateDocument, startKey, error);
}
};
private setDefaultUpdateTabButtonVisibility = (): void => {
newDocumentButton = assignTabButtonVisibility(true, true);
saveNewDocumentButton = assignTabButtonVisibility(false, false);
discardNewDocumentChangesButton = assignTabButtonVisibility(false, false);
saveExisitingDocumentButton = assignTabButtonVisibility(true, false);
discardExisitingDocumentChangesButton = assignTabButtonVisibility(true, false);
deleteExisitingDocumentButton = assignTabButtonVisibility(true, true);
this.updateTabButton();
};
public onSaveNewMongoDocumentClick = async (): Promise<void> => {
const parsedDocumentContent = JSON.parse(this.state.documentContent);
const {
partitionKey,
partitionKeyProperty,
displayedError,
isExecutionError,
isExecuting,
collection,
} = this.props;
displayedError("");
const startKey: number = this.getStartKey(Action.CreateDocument);
if (
partitionKeyProperty &&
partitionKeyProperty !== "_id" &&
!hasShardKeySpecified(parsedDocumentContent, partitionKey, partitionKeyProperty)
) {
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
displayedError(message);
this.setTraceFail(Action.CreateDocument, startKey, message);
logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
throw new Error("Document without shard key");
}
isExecutionError(false);
isExecuting(true);
try {
const savedDocument = await createDocument(
collection.databaseId,
collection as ViewModels.Collection,
partitionKeyProperty,
parsedDocumentContent
);
if (savedDocument) {
this.handleLoadMoreDocument();
this.setDefaultUpdateTabButtonVisibility();
this.setTraceSuccess(Action.CreateDocument, startKey);
}
this.setState({ isEditorContentEdited: false });
this.queryDocumentsData();
} catch (error) {
window.alert(getErrorMessage(error));
this.setTraceFail(Action.CreateDocument, startKey, error);
}
};
private getStartKey = (action: number): number => {
const startKey: number = TelemetryProcessor.traceStart(action, {
dataExplorerArea: Areas.Tab,
tabTitle: this.props.tabTitle(),
});
return startKey;
};
private setTraceSuccess = (action: number, startKey: number): void => {
const { isExecuting, tabTitle } = this.props;
TelemetryProcessor.traceSuccess(
action,
{
dataExplorerArea: Areas.Tab,
tabTitle: tabTitle(),
},
startKey
);
isExecuting(false);
};
private setTraceFail = (action: number, startKey: number, error: Error | string): void => {
const { isExecuting, tabTitle, isExecutionError } = this.props;
TelemetryProcessor.traceFailure(
action,
{
dataExplorerArea: Areas.Tab,
tabTitle: tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
isExecuting(false);
isExecutionError(true);
};
private onRevertNewDocumentClick = () => {
newDocumentButton = assignTabButtonVisibility(true, true);
saveNewDocumentButton = assignTabButtonVisibility(true, false);
discardNewDocumentChangesButton = assignTabButtonVisibility(true, false);
this.updateTabButton();
this.setState({
isEditorVisible: false,
isEditorContentEdited: false,
});
};
private onRenderCell = (item: { value: string }): JSX.Element => {
return (
<div
className="documentTabSuggestions"
onClick={() =>
this.setState({
filter: item.value,
isSuggestionVisible: false,
})
}
>
<Text>{item.value}</Text>
</div>
);
};
private handleLoadMoreDocument = (): void => {
userContext.apiType === "Mongo" ? this.queryDocumentsData() : this.querySqlDocumentsData();
this.setState({
isSuggestionVisible: false,
isAllDocumentsVisible: true,
});
};
private updateTabButton = (): void => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
};
private getKey(item: DocumentId): string {
return item.rid;
}
private handleDocumentContentChange = (newContent: string): void => {
if (saveExisitingDocumentButton.visible && newContent !== this.state.documentContent) {
saveExisitingDocumentButton = assignTabButtonVisibility(true, true);
discardExisitingDocumentChangesButton = assignTabButtonVisibility(true, true);
}
this.setState(
{
documentContent: newContent,
isEditorContentEdited: true,
},
() => {
this.updateTabButton();
}
);
};
public render(): JSX.Element {
const {
columns,
isCompactMode,
isSuggestionVisible,
filter,
isFilterOptionVisible,
isEditorVisible,
documentContent,
documentIds,
documentSqlIds,
isAllDocumentsVisible,
} = this.state;
const isPreferredApiMongoDB = userContext.apiType === "Mongo";
return (
<>
{isFilterOptionVisible && (
<>
<div>
<Stack horizontal verticalFill wrap>
{!isPreferredApiMongoDB && <Text className="queryText">SELECT * FROM c</Text>}
<TextField
iconProps={filterIcon}
placeholder={getFilterPlaceholder(isPreferredApiMongoDB)}
className={isPreferredApiMongoDB ? "documentTabSearchBar" : "documentSqlTabSearchBar"}
onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newInput?: string) => {
this.setState({ filter: newInput });
}}
value={filter}
/>
<PrimaryButton text="Apply Filter" onClick={this.handleFilter} className="documentTabFiltetButton" />
<Image
src={CloseIcon}
alt="Close icon"
{...imageProps}
onClick={() => this.setState({ isFilterOptionVisible: false })}
/>
</Stack>
</div>
{isSuggestionVisible && (
<div className={isPreferredApiMongoDB ? "filterSuggestions" : "filterSuggestions sqlFilterSuggestions"}>
<List items={getFilterSuggestions(isPreferredApiMongoDB)} onRenderCell={this.onRenderCell} />
</div>
)}
</>
)}
{!isFilterOptionVisible && (
<Stack horizontal verticalFill wrap className="documentTabNoFilterView">
<Text className="noFilterText">{getfilterText(isPreferredApiMongoDB, filter)}</Text>
<PrimaryButton text="Edit Filter" onClick={() => this.setState({ isFilterOptionVisible: true })} />
</Stack>
)}
<div className="splitterWrapper" onClick={() => this.setState({ isSuggestionVisible: false })}>
<SplitterLayout primaryIndex={0} secondaryInitialSize={1000}>
<div className="leftSplitter">
<DetailsList
items={getDocumentItems(isPreferredApiMongoDB, documentIds, documentSqlIds, isAllDocumentsVisible)}
compact={isCompactMode}
columns={columns}
selectionMode={SelectionMode.none}
getKey={this.getKey}
setKey="none"
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}
/>
<Text onClick={this.handleLoadMoreDocument} className="documentLoadMore" block={true}>
Load More
</Text>
</div>
{isEditorVisible ? (
<div className="react-editor">
<EditorReact
language={"json"}
content={documentContent}
isReadOnly={false}
ariaLabel={"Document json"}
onContentChanged={this.handleDocumentContentChange}
lineNumbers="on"
/>
</div>
) : (
<div className="documentTabWatermark">
<Image src={DocumentWaterMark} alt="Document watermark" />
<Text className="documentCreateText">Create new or work with existing document(s).</Text>
</div>
)}
</SplitterLayout>
</div>
</>
);
}
}

View File

@@ -0,0 +1,90 @@
import * as ko from "knockout";
import { PartitionKey, Resource } from "../../../src/Contracts/DataModels";
import DocumentId from "../Tree/DocumentId";
import * as DocumentTabUtils from "./DocumentTabUtils";
describe("DocumentTabUtils", () => {
describe("getfilterText()", () => {
it("Should return filter if isPreferredApiMongoDB is true and filter is applied ", () => {
const filteredText: string = DocumentTabUtils.getfilterText(true, `{"_id": "foo"}`);
expect(filteredText).toBe(`Filter : {"_id": "foo"}`);
});
it("Should return `No filter applied` if isPreferredApiMongoDB is true and filter is not applied ", () => {
const filteredText: string = DocumentTabUtils.getfilterText(true, "");
expect(filteredText).toBe("No filter applied");
});
it("Should return `Select * from C` with filter if isPreferredApiMongoDB is false and filter is applied ", () => {
const filteredText: string = DocumentTabUtils.getfilterText(false, `WHERE c.id = "foo"`);
expect(filteredText).toBe(`Select * from C WHERE c.id = "foo"`);
});
});
describe("formatDocumentContent()", () => {
const fakeDocumentData = {} as DocumentId;
fakeDocumentData.partitionKeyProperty = "test";
fakeDocumentData.id = ko.observable("id");
it("should return formatted content with new line each property.", () => {
fakeDocumentData.partitionKeyValue = "partitionValue";
const formattedContent: string = DocumentTabUtils.formatDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(`{\n"_id":"id",\n"test":"partitionValue"\n}`);
});
it("should return formatted content with empty partitionKeyValue when partitionKeyValue is undefined.", () => {
fakeDocumentData.partitionKeyValue = undefined;
const formattedContent: string = DocumentTabUtils.formatDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(`{\n"_id":"id",\n"test":""\n}`);
});
});
describe("formatSqlDocumentContent()", () => {
const fakeDocumentData = {} as Resource;
it("should return formatted content with new line each property.", () => {
fakeDocumentData.id = "testId";
fakeDocumentData._rid = "testRid";
fakeDocumentData._self = "testSelf";
fakeDocumentData._ts = "testTs";
fakeDocumentData._etag = "testEtag";
fakeDocumentData._attachments = "testAttachments";
fakeDocumentData._partitionKey = "testPartitionKey";
const formattedContent: string = DocumentTabUtils.formatSqlDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(
`{\n"id":"testId",\n"_rid":"testRid",\n"_self":"testSelf",\n"_ts":"testTs",\n"_etag":"testEtag",\n"_attachments":"testAttachments",\n"_partitionKey":"testPartitionKey"\n}`
);
});
it("should return formatted content with empty value when key value is undefined.", () => {
fakeDocumentData.id = undefined;
fakeDocumentData._rid = undefined;
fakeDocumentData._self = undefined;
fakeDocumentData._ts = undefined;
fakeDocumentData._etag = undefined;
fakeDocumentData._attachments = undefined;
fakeDocumentData._partitionKey = undefined;
const formattedContent: string = DocumentTabUtils.formatSqlDocumentContent(fakeDocumentData);
expect(formattedContent).toBe(
`{\n"id":"",\n"_rid":"",\n"_self":"",\n"_ts":"",\n"_etag":"",\n"_attachments":"",\n"_partitionKey":""\n}`
);
});
});
describe("getPartitionKeyDefinition()", () => {
const partitionKey = {} as PartitionKey;
partitionKey.kind = "Hash";
partitionKey.version = 1;
partitionKey.systemKey = true;
const partitionKeyProperty = "testPartitionKey";
it("should return formatted partitionKey with formatted path.", () => {
partitionKey.paths = ["test"];
const formattedPartitionKey = DocumentTabUtils.getPartitionKeyDefinition(partitionKey, partitionKeyProperty);
expect(formattedPartitionKey).toEqual({ kind: "Hash", version: 1, systemKey: true, paths: ["test"] });
});
it("should return partitionKey with undefined paths if paths is undefined.", () => {
partitionKey.paths = undefined;
const formattedPartitionKey = DocumentTabUtils.getPartitionKeyDefinition(partitionKey, partitionKeyProperty);
expect(formattedPartitionKey).toEqual({ kind: "Hash", version: 1, systemKey: true, paths: undefined });
});
});
});

View File

@@ -1,33 +1,10 @@
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import {
createDocument,
deleteDocument,
queryDocuments,
readDocument,
updateDocument,
} from "../../Common/MongoProxyClient";
import MongoUtility from "../../Common/MongoUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId";
import DocumentsTab from "./DocumentsTab";
export default class MongoDocumentsTab extends DocumentsTab {
public collection: ViewModels.Collection;
private continuationToken: string;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.lastFilterContents = ko.observableArray<string>(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]);
if (this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
}
@@ -38,279 +15,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
}
this.isFilterExpanded = ko.observable<boolean>(true);
super.buildCommandBarOptions.bind(this);
super.buildCommandBarOptions();
}
public onSaveNewDocumentClick = (): Promise<any> => {
const documentContent = JSON.parse(this.selectedDocumentContent());
this.displayedError("");
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
if (
this.partitionKeyProperty &&
this.partitionKeyProperty !== "_id" &&
!this._hasShardKeySpecified(documentContent)
) {
const message = `The document is lacking the shard property: ${this.partitionKeyProperty}`;
this.displayedError(message);
let that = this;
setTimeout(() => {
that.displayedError("");
}, Constants.ClientDefaults.errorNotificationTimeoutMs);
this.isExecutionError(true);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: message,
},
startKey
);
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
throw new Error("Document without shard key");
}
this.isExecutionError(false);
this.isExecuting(true);
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
.then(
(savedDocument: any) => {
let partitionKeyArray = extractPartitionKey(
savedDocument,
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
let id = new ObjectId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
delete savedDocument._self;
let value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.selectedDocumentId(id);
this.documentIds(ids);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
TelemetryProcessor.traceSuccess(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = this.selectedDocumentContent();
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
.then(
(updatedDocument: any) => {
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.documentIds().forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) {
const partitionKeyArray = extractPartitionKey(
updatedDocument,
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
const id = new ObjectId(this, updatedDocument, partitionKeyValue);
documentId.id(id.id());
}
});
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
TelemetryProcessor.traceSuccess(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
startKey
);
},
(error) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public buildQuery(filter: string): string {
return filter || "{}";
}
public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId);
const content = await readDocument(this.collection.databaseId, this.collection, documentId);
this.initDocumentEditor(documentId, content);
}
public loadNextPage(): Q.Promise<any> {
this.isExecuting(true);
this.isExecutionError(false);
const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter);
return Q(queryDocuments(this.collection.databaseId, this.collection, true, query, this.continuationToken))
.then(
({ continuationToken, documents }) => {
this.continuationToken = continuationToken;
let currentDocuments = this.documentIds();
const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid);
const nextDocumentIds = documents
.filter((d: any) => {
return currentDocumentsRids.indexOf(d._rid) < 0;
})
.map((rawDocument: any) => {
const partitionKeyValue = rawDocument._partitionKeyValue;
return new DocumentId(this, rawDocument, partitionKeyValue);
});
const merged = currentDocuments.concat(nextDocumentIds);
this.documentIds(merged);
currentDocuments = this.documentIds();
if (this.filterContent().length > 0 && currentDocuments.length > 0) {
currentDocuments[0].click();
} else {
this.selectedDocumentContent("");
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
}
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
},
(error: any) => {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
)
.finally(() => this.isExecuting(false));
}
protected _onEditorContentChange(newContent: string) {
try {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid
) {
let parsed: any = JSON.parse(newContent);
}
// Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit
this.onValidDocumentEdit();
} catch (e) {
this.onInvalidDocumentEdit();
}
}
/** Renders a Javascript object to be displayed inside Monaco Editor */
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
return MongoUtility.tojson(value, null, false);
}
private _hasShardKeySpecified(document: any): boolean {
return Boolean(extractPartitionKey(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
}
private _getPartitionKeyDefinition(): DataModels.PartitionKey {
let partitionKey: DataModels.PartitionKey = this.partitionKey;
if (
this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0].indexOf("$v") > -1
) {
// Convert BsonSchema2 to /path format
partitionKey = {
kind: partitionKey.kind,
paths: ["/" + this.partitionKeyProperty.replace(/\./g, "/")],
version: partitionKey.version,
};
}
return partitionKey;
}
protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection.databaseId, this.collection, documentId);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,14 +22,16 @@ import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import * as QueryUtils from "../../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
import TabsBase from "../TabsBase";
import { TabsManager } from "../TabsManager";
import "./QueryTabComponent.less";
enum ToggleState {
@@ -65,7 +67,6 @@ export interface IQueryTabComponentProps {
partitionKey: DataModels.PartitionKey;
container: Explorer;
activeTab?: TabsBase;
tabManager?: TabsManager;
onTabAccessor: (instance: ITabAccessor) => void;
isPreferredApiMongoDB?: boolean;
monacoEditorSetting?: string;
@@ -391,13 +392,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
};
public onSaveQueryClick = (): void => {
this.props.collection && this.props.collection.container && this.props.collection.container.openSaveQueryPanel();
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
};
public onSavedQueriesClick = (): void => {
this.props.collection &&
this.props.collection.container &&
this.props.collection.container.openBrowseQueriesPanel();
useSidePanel
.getState()
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
};
public async onFetchNextPageClick(): Promise<void> {

View File

@@ -137,13 +137,14 @@ export default class QueryTablesTab extends TabsBase {
useSidePanel
.getState()
.openSidePanel(
"Add Table Entity",
"Add Table Row",
<AddTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>
/>,
"700px"
);
};
@@ -157,7 +158,8 @@ export default class QueryTablesTab extends TabsBase {
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>
/>,
"700px"
);
};

View File

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

View File

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

View File

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

View File

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

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