mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
1 Commits
create_dat
...
fix_400%_z
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6043260d8 |
15
.env.example
15
.env.example
@@ -1 +1,16 @@
|
||||
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
|
||||
112
.eslintignore
112
.eslintignore
@@ -71,6 +71,7 @@ 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
|
||||
@@ -82,6 +83,11 @@ 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
|
||||
@@ -99,10 +105,17 @@ 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
|
||||
@@ -128,13 +141,17 @@ 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
|
||||
@@ -143,32 +160,96 @@ 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
|
||||
@@ -176,21 +257,46 @@ 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/ResourceTree.tsx
|
||||
src/Explorer/Tree/DatabasesResourceTree.tsx
|
||||
src/Explorer/Tree/NotebooksResourceTree.tsx
|
||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
||||
- ./test/mongo/container.spec.ts
|
||||
- ./test/mongo/container32.spec.ts
|
||||
- ./test/selfServe/selfServeExample.spec.ts
|
||||
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
|
||||
- ./test/notebooks/upload.spec.ts
|
||||
- ./test/sql/resourceToken.spec.ts
|
||||
- ./test/tables/container.spec.ts
|
||||
steps:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"enableSchemaAnalyzer": true
|
||||
}
|
||||
|
Before Width: | Height: | Size: 842 B After Width: | Height: | Size: 842 B |
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 371 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -9,7 +9,6 @@
|
||||
|
||||
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
@GrayScale: "grayscale()";
|
||||
|
||||
@xSmallFontSize: 4px;
|
||||
@smallFontSize: 8px;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
.dataResourceTree {
|
||||
margin-left: @MediumSpace;
|
||||
overflow: auto;
|
||||
|
||||
.databaseHeader {
|
||||
font-size: 14px;
|
||||
@@ -19,10 +18,6 @@
|
||||
.notebookHeader {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clickDisabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
271
less/tree.less
271
less/tree.less
@@ -1,270 +1,273 @@
|
||||
@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;
|
||||
|
||||
.collectionList {
|
||||
padding-left: (2 * @MediumSpace);
|
||||
.databaseList {
|
||||
list-style-type: none;
|
||||
padding-left: 0px;
|
||||
|
||||
.collectionList {
|
||||
padding-left:(2 * @MediumSpace);
|
||||
}
|
||||
|
||||
.collectionChildList {
|
||||
padding-left: @LargeSpace;
|
||||
}
|
||||
|
||||
.databaseDocuments {
|
||||
padding-left: (5 * @MediumSpace);
|
||||
}
|
||||
}
|
||||
|
||||
.collectionChildList {
|
||||
padding-left: @LargeSpace;
|
||||
}
|
||||
|
||||
.databaseDocuments {
|
||||
padding-left: (5 * @MediumSpace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pointerCursor {
|
||||
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;
|
||||
}
|
||||
4670
package-lock.json
generated
4670
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -42,15 +42,14 @@
|
||||
"@octokit/rest": "17.9.2",
|
||||
"@phosphor/widgets": "1.9.3",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@types/lodash": "4.14.171",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"applicationinsights": "1.8.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "file:./canvas",
|
||||
"clean-webpack-plugin": "3.0.0",
|
||||
"clean-webpack-plugin": "0.1.19",
|
||||
"clipboard-copy": "4.0.1",
|
||||
"copy-webpack-plugin": "9.0.1",
|
||||
"copy-webpack-plugin": "6.0.2",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"d3": "6.1.1",
|
||||
@@ -81,10 +80,10 @@
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
"react": "16.14.0",
|
||||
"react": "16.13.1",
|
||||
"react-animate-height": "2.0.8",
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dnd": "9.4.0",
|
||||
"react-dnd-html5-backend": "9.4.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-hotkeys": "2.0.0",
|
||||
"react-i18next": "11.8.5",
|
||||
@@ -98,7 +97,7 @@
|
||||
"sanitize-html": "2.3.3",
|
||||
"styled-components": "4.3.2",
|
||||
"swr": "0.4.0",
|
||||
"terser-webpack-plugin": "5.1.4",
|
||||
"terser-webpack-plugin": "3.1.0",
|
||||
"underscore": "1.9.1",
|
||||
"utility-types": "3.10.0",
|
||||
"zustand": "3.5.0"
|
||||
@@ -132,7 +131,6 @@
|
||||
"@types/underscore": "1.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "4.22.0",
|
||||
"@typescript-eslint/parser": "4.22.0",
|
||||
"@webpack-cli/serve": "1.5.2",
|
||||
"babel-jest": "24.9.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"buffer": "5.1.0",
|
||||
@@ -154,45 +152,44 @@
|
||||
"html-inline-css-webpack-plugin": "1.11.0",
|
||||
"html-loader": "0.5.5",
|
||||
"html-loader-jest": "0.2.1",
|
||||
"html-webpack-plugin": "5.3.2",
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"html-webpack-plugin": "4.5.2",
|
||||
"jest": "25.5.4",
|
||||
"jest-canvas-mock": "2.1.0",
|
||||
"jest-playwright-preset": "1.5.1",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.1.0",
|
||||
"less-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.1.0",
|
||||
"mini-css-extract-plugin": "0.4.3",
|
||||
"monaco-editor-webpack-plugin": "1.7.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"playwright": "1.13.0",
|
||||
"playwright": "1.10.0",
|
||||
"prettier": "2.2.1",
|
||||
"process": "0.11.10",
|
||||
"raw-loader": "0.5.1",
|
||||
"react-dev-utils": "11.0.4",
|
||||
"rimraf": "3.0.0",
|
||||
"sinon": "3.2.1",
|
||||
"style-loader": "0.23.0",
|
||||
"ts-loader": "9.2.4",
|
||||
"ts-loader": "6.2.2",
|
||||
"tslint": "5.11.0",
|
||||
"tslint-microsoft-contrib": "6.0.0",
|
||||
"typedoc": "0.20.36",
|
||||
"typescript": "4.3.4",
|
||||
"url-loader": "1.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "5.47.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "4.7.2",
|
||||
"webpack-dev-server": "3.11.2"
|
||||
"webpack": "4.46.0",
|
||||
"webpack-bundle-analyzer": "3.6.1",
|
||||
"webpack-cli": "3.3.10",
|
||||
"webpack-dev-server": "3.11.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --mode development",
|
||||
"start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js",
|
||||
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
||||
"build:dataExplorer:ci": "npm run build:ci",
|
||||
"build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
|
||||
"build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
|
||||
"pack:prod": "webpack --mode production",
|
||||
"pack:fast": "webpack --mode development --progress",
|
||||
"pack:prod": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode production",
|
||||
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress",
|
||||
"copyToConsumers": "node copyToConsumers",
|
||||
"test": "rimraf coverage && jest",
|
||||
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
||||
|
||||
@@ -94,8 +94,7 @@ export class Flights {
|
||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||
public static readonly MongoIndexing = "mongoindexing";
|
||||
public static readonly AutoscaleTest = "autoscaletest";
|
||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||
public static readonly SchemaAnalyzer = "schemaanalyzer";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
@@ -350,11 +349,6 @@ export class Notebook {
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
||||
public static readonly mongoShellTemporarilyDownMsg =
|
||||
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||
public static readonly cassandraShellTemporarilyDownMsg =
|
||||
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
import * as HeadersUtility from "./HeadersUtility";
|
||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
|
||||
describe("Headers Utility", () => {
|
||||
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {
|
||||
|
||||
@@ -2,22 +2,17 @@ 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 ResourceTreeContainerProps {
|
||||
export interface ResourceTreeProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
isLeftPaneExpanded: boolean;
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
|
||||
export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
|
||||
toggleLeftPaneExpanded,
|
||||
isLeftPaneExpanded,
|
||||
container,
|
||||
}: ResourceTreeContainerProps): JSX.Element => {
|
||||
}: ResourceTreeProps): JSX.Element => {
|
||||
return (
|
||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||
{/* Collections Window - - Start */}
|
||||
@@ -53,9 +48,9 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
</div>
|
||||
</div>
|
||||
{userContext.authType === AuthType.ResourceToken ? (
|
||||
<ResourceTokenTree />
|
||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
|
||||
) : (
|
||||
<ResourceTree container={container} />
|
||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||
)}
|
||||
</div>
|
||||
{/* Collections Window - End */}
|
||||
@@ -1,10 +1,7 @@
|
||||
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";
|
||||
@@ -26,15 +23,6 @@ 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 () => {
|
||||
|
||||
@@ -4,16 +4,20 @@ 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 { 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 {
|
||||
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 * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
@@ -55,16 +59,6 @@ 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":
|
||||
@@ -83,6 +77,23 @@ 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,
|
||||
@@ -120,6 +131,23 @@ 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,
|
||||
@@ -161,6 +189,23 @@ 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,
|
||||
@@ -188,6 +233,23 @@ 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,
|
||||
@@ -222,6 +284,22 @@ 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,
|
||||
|
||||
@@ -2,13 +2,20 @@ 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 { 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 {
|
||||
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 {
|
||||
CassandraKeyspaceCreateUpdateParameters,
|
||||
CreateUpdateOptions,
|
||||
@@ -41,11 +48,6 @@ 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) {
|
||||
@@ -63,6 +65,22 @@ 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: {
|
||||
@@ -83,6 +101,22 @@ 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: {
|
||||
@@ -103,6 +137,22 @@ 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: {
|
||||
@@ -123,6 +173,22 @@ 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: {
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface ConfigContext {
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
allowedJunoOrigins: string[];
|
||||
enableSchemaAnalyzer: boolean;
|
||||
msalRedirectURI?: string;
|
||||
}
|
||||
|
||||
@@ -62,6 +63,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
"https://tools-staging.cosmos.azure.com",
|
||||
"https://localhost",
|
||||
],
|
||||
enableSchemaAnalyzer: false,
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface DatabaseAccount {
|
||||
|
||||
export interface DatabaseAccountExtendedProperties {
|
||||
documentEndpoint?: string;
|
||||
disableLocalAuth?: boolean;
|
||||
tableEndpoint?: string;
|
||||
gremlinEndpoint?: string;
|
||||
cassandraEndpoint?: string;
|
||||
|
||||
@@ -16,7 +16,6 @@ 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";
|
||||
@@ -50,10 +49,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />
|
||||
),
|
||||
.openSidePanel("Delete " + getDatabaseName(), <DeleteDatabaseConfirmationPanel explorer={container} />),
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
});
|
||||
@@ -83,16 +79,15 @@ export const createCollectionContextMenuButton = (
|
||||
|
||||
items.push({
|
||||
iconSrc: HostedTerminalIcon,
|
||||
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
if (container.isShellEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
},
|
||||
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
|
||||
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,7 +105,7 @@ export const createCollectionContextMenuButton = (
|
||||
iconSrc: AddUdfIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New UDF",
|
||||
});
|
||||
@@ -130,10 +125,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
|
||||
),
|
||||
.openSidePanel("Delete " + getCollectionName(), <DeleteCollectionConfirmationPane explorer={container} />),
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
|
||||
@@ -8,9 +8,7 @@ import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
|
||||
export interface AccordionComponentProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export interface AccordionComponentProps {}
|
||||
|
||||
export class AccordionComponent extends React.Component<AccordionComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
@@ -80,7 +78,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
||||
);
|
||||
}
|
||||
|
||||
private onHeaderClick = (): void => {
|
||||
private onHeaderClick = (_event: React.MouseEvent<HTMLDivElement>): void => {
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
};
|
||||
|
||||
|
||||
@@ -121,7 +121,8 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
|
||||
if (!this.dropdownElt || !this.expandButtonElt) {
|
||||
return;
|
||||
}
|
||||
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||
|
||||
const dropdownElt = $(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||
}
|
||||
|
||||
private onKeyPress(event: React.KeyboardEvent): boolean {
|
||||
|
||||
@@ -23,75 +23,13 @@ export interface DialogState {
|
||||
dialogProps?: DialogProps;
|
||||
openDialog: (props: DialogProps) => void;
|
||||
closeDialog: () => void;
|
||||
showOkCancelModalDialog: (
|
||||
title: string,
|
||||
subText: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean
|
||||
) => void;
|
||||
showOkModalDialog: (title: string, subText: string) => void;
|
||||
}
|
||||
|
||||
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
export const useDialog: UseStore<DialogState> = create((set) => ({
|
||||
visible: false,
|
||||
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
|
||||
closeDialog: () =>
|
||||
set(
|
||||
(state) => ({
|
||||
visible: false,
|
||||
openDialog: state.openDialog,
|
||||
closeDialog: state.closeDialog,
|
||||
showOkCancelModalDialog: state.showOkCancelModalDialog,
|
||||
showOkModalDialog: state.showOkModalDialog,
|
||||
}),
|
||||
true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above
|
||||
),
|
||||
showOkCancelModalDialog: (
|
||||
title: string,
|
||||
subText: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean
|
||||
): void =>
|
||||
get().openDialog({
|
||||
isModal: true,
|
||||
title,
|
||||
subText,
|
||||
primaryButtonText: okLabel,
|
||||
secondaryButtonText: cancelLabel,
|
||||
onPrimaryButtonClick: () => {
|
||||
get().closeDialog();
|
||||
onOk && onOk();
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
get().closeDialog();
|
||||
onCancel && onCancel();
|
||||
},
|
||||
choiceGroupProps,
|
||||
textFieldProps,
|
||||
primaryButtonDisabled,
|
||||
}),
|
||||
showOkModalDialog: (title: string, subText: string): void =>
|
||||
get().openDialog({
|
||||
isModal: true,
|
||||
title,
|
||||
subText,
|
||||
primaryButtonText: "Close",
|
||||
secondaryButtonText: undefined,
|
||||
onPrimaryButtonClick: () => {
|
||||
get().closeDialog();
|
||||
},
|
||||
onSecondaryButtonClick: undefined,
|
||||
}),
|
||||
set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true),
|
||||
}));
|
||||
|
||||
export interface TextFieldProps extends ITextFieldProps {
|
||||
|
||||
@@ -56,7 +56,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(() => {
|
||||
queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => {
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.props.onContentChanged(queryEditorModel.getValue());
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{content}</div>
|
||||
<div className={"paneMainContent"}>{content}</div>
|
||||
{!this.props.showAuthorizeAccess && (
|
||||
<>
|
||||
<div className={"paneFooter"} style={ContentFooterStyle}>
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.input-type-head-text-field {
|
||||
width: 100%;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
@@ -24,11 +21,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.input-typeahead-chocies-container {
|
||||
border: 1px solid lightgrey;
|
||||
padding: 5px 10px 5px 10px;
|
||||
cursor: pointer;
|
||||
.choice-caption{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
* typeaheadOverrideOptions: { dynamic:false }
|
||||
*
|
||||
*/
|
||||
import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react";
|
||||
import "jquery-typeahead";
|
||||
import * as React from "react";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import "./InputTypeahead.less";
|
||||
|
||||
export interface Item {
|
||||
caption: string;
|
||||
value: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,125 +75,170 @@ export interface InputTypeaheadComponentProps {
|
||||
useTextarea?: boolean;
|
||||
}
|
||||
|
||||
interface InputTypeaheadComponentState {
|
||||
isSuggestionVisible: boolean;
|
||||
selectedChoice: Item;
|
||||
filteredChoices: Item[];
|
||||
interface OnClickItem {
|
||||
matchedKey: string;
|
||||
value: any;
|
||||
caption: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
inputValue: string;
|
||||
selection: Item;
|
||||
}
|
||||
|
||||
interface InputTypeaheadComponentState {}
|
||||
|
||||
export class InputTypeaheadComponent extends React.Component<
|
||||
InputTypeaheadComponentProps,
|
||||
InputTypeaheadComponentState
|
||||
> {
|
||||
constructor(props: InputTypeaheadComponentProps) {
|
||||
private inputElt: HTMLElement;
|
||||
private containerElt: HTMLElement;
|
||||
|
||||
private cache: Cache;
|
||||
private inputValue: string;
|
||||
private selection: Item;
|
||||
|
||||
public constructor(props: InputTypeaheadComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSuggestionVisible: false,
|
||||
filteredChoices: [],
|
||||
selectedChoice: {
|
||||
caption: "",
|
||||
value: "",
|
||||
},
|
||||
this.cache = {
|
||||
inputValue: null,
|
||||
selection: null,
|
||||
};
|
||||
}
|
||||
|
||||
private onRenderCell = (item: Item): JSX.Element => {
|
||||
return (
|
||||
<div className="input-typeahead-chocies-container" onClick={() => this.onChoiceClick(item)}>
|
||||
<p className="choice-caption">{item.caption}</p>
|
||||
<span>{item.value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private onChoiceClick = (item: Item): void => {
|
||||
this.props.onNewValue(item.caption);
|
||||
this.setState({ isSuggestionVisible: false, selectedChoice: item });
|
||||
};
|
||||
|
||||
private handleChange = (value: string): void => {
|
||||
if (!value) {
|
||||
this.setState({ isSuggestionVisible: true });
|
||||
/**
|
||||
* Props have changed
|
||||
* @param prevProps
|
||||
* @param prevState
|
||||
* @param snapshot
|
||||
*/
|
||||
public componentDidUpdate(
|
||||
prevProps: InputTypeaheadComponentProps,
|
||||
prevState: InputTypeaheadComponentState,
|
||||
snapshot: any
|
||||
): void {
|
||||
if (prevProps.defaultValue !== this.props.defaultValue) {
|
||||
$(this.inputElt).val(this.props.defaultValue);
|
||||
this.initializeTypeahead();
|
||||
}
|
||||
this.props.onNewValue(value);
|
||||
const filteredChoices = this.filterChoiceByValue(this.props.choices, value);
|
||||
this.setState({ filteredChoices });
|
||||
};
|
||||
}
|
||||
|
||||
private onSubmit = (event: React.KeyboardEvent<HTMLElement>): void => {
|
||||
if (event.key === "Enter") {
|
||||
/**
|
||||
* Executed once react is done building the DOM for this component
|
||||
*/
|
||||
public componentDidMount(): void {
|
||||
this.initializeTypeahead();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<span className="input-typeahead-container">
|
||||
<div
|
||||
className="input-typehead"
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(event)}
|
||||
>
|
||||
<div className="typeahead__container" ref={(input) => (this.containerElt = input)}>
|
||||
<div className="typeahead__field">
|
||||
<span className="typeahead__query">
|
||||
{this.props.useTextarea ? (
|
||||
<textarea
|
||||
rows={1}
|
||||
name="q"
|
||||
autoComplete="off"
|
||||
aria-label="Input query"
|
||||
ref={(input) => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
name="q"
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
aria-label="Input query"
|
||||
ref={(input) => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{this.props.showSearchButton && (
|
||||
<span className="typeahead__button">
|
||||
<button type="submit">
|
||||
<span className="typeahead__search-icon" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
if (this.props.submitFct) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.submitFct(this.props.defaultValue, this.state.selectedChoice);
|
||||
this.setState({ isSuggestionVisible: false });
|
||||
this.props.submitFct(this.cache.inputValue, this.cache.selection);
|
||||
$(this.containerElt).children(".typeahead__result").hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private filterChoiceByValue = (choices: Item[], searchKeyword: string): Item[] => {
|
||||
return choices.filter((choice) =>
|
||||
// @ts-ignore
|
||||
Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase()))
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { defaultValue, useTextarea, placeholder, onNewValue } = this.props;
|
||||
const { isSuggestionVisible, selectedChoice, filteredChoices } = this.state;
|
||||
const theme = getTheme();
|
||||
|
||||
const iconButtonStyles = {
|
||||
root: {
|
||||
color: theme.palette.neutralPrimary,
|
||||
marginLeft: "10px !important",
|
||||
marginTop: "0px",
|
||||
marginRight: "2px",
|
||||
width: "42px",
|
||||
/**
|
||||
* Must execute once ko is rendered, so that it can find the input element by id
|
||||
*/
|
||||
private initializeTypeahead(): void {
|
||||
const props = this.props;
|
||||
let cache = this.cache;
|
||||
let options: any = {
|
||||
input: this.inputElt,
|
||||
order: "asc",
|
||||
minLength: 0,
|
||||
searchOnFocus: true,
|
||||
source: {
|
||||
display: "caption",
|
||||
data: () => {
|
||||
return props.choices;
|
||||
},
|
||||
},
|
||||
rootHovered: {
|
||||
color: theme.palette.neutralDark,
|
||||
callback: {
|
||||
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
|
||||
cache.selection = item;
|
||||
|
||||
if (props.onSelected) {
|
||||
props.onSelected(item);
|
||||
}
|
||||
},
|
||||
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
|
||||
cache.inputValue = query;
|
||||
if (props.onNewValue) {
|
||||
props.onNewValue(query);
|
||||
}
|
||||
},
|
||||
},
|
||||
template: (query: string, item: any) => {
|
||||
// Don't display id if caption *IS* the id
|
||||
return item.caption === item.value
|
||||
? "<span>{{caption}}</span>"
|
||||
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
|
||||
},
|
||||
dynamic: true,
|
||||
};
|
||||
const cancelIcon: IIconProps = { iconName: "cancel" };
|
||||
const searchIcon: IIconProps = { iconName: "Search" };
|
||||
|
||||
return (
|
||||
<div className="input-typeahead-container">
|
||||
<Stack horizontal>
|
||||
<TextField
|
||||
multiline={useTextarea}
|
||||
rows={1}
|
||||
defaultValue={defaultValue}
|
||||
ariaLabel="Input query"
|
||||
placeholder={placeholder}
|
||||
className="input-type-head-text-field"
|
||||
value={defaultValue}
|
||||
onKeyDown={this.onSubmit}
|
||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||
/>
|
||||
{this.props.showCancelButton && (
|
||||
<IconButton
|
||||
styles={iconButtonStyles}
|
||||
iconProps={cancelIcon}
|
||||
ariaLabel="cancel Button"
|
||||
onClick={() => onNewValue("")}
|
||||
/>
|
||||
)}
|
||||
{this.props.showSearchButton && (
|
||||
<IconButton
|
||||
styles={iconButtonStyles}
|
||||
iconProps={searchIcon}
|
||||
ariaLabel="Search Button"
|
||||
onClick={() => this.props.submitFct(defaultValue, selectedChoice)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{filteredChoices.length && isSuggestionVisible ? (
|
||||
<List items={filteredChoices} onRenderCell={this.onRenderCell} />
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
// Override options
|
||||
if (props.typeaheadOverrideOptions) {
|
||||
for (const p in props.typeaheadOverrideOptions) {
|
||||
options[p] = props.typeaheadOverrideOptions[p];
|
||||
}
|
||||
}
|
||||
|
||||
if (props.hasOwnProperty("showCancelButton")) {
|
||||
options.cancelButton = props.showCancelButton;
|
||||
}
|
||||
|
||||
$(this.inputElt).typeahead(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,61 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`inputTypeahead renders <input /> 1`] = `
|
||||
<div
|
||||
<span
|
||||
className="input-typeahead-container"
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
<div
|
||||
className="input-typehead"
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
className="typeahead__container"
|
||||
>
|
||||
<div
|
||||
className="typeahead__field"
|
||||
>
|
||||
<span
|
||||
className="typeahead__query"
|
||||
>
|
||||
<input
|
||||
aria-label="Input query"
|
||||
autoComplete="off"
|
||||
name="q"
|
||||
type="search"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`inputTypeahead renders <textarea /> 1`] = `
|
||||
<div
|
||||
<span
|
||||
className="input-typeahead-container"
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
<div
|
||||
className="input-typehead"
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
className="typeahead__container"
|
||||
>
|
||||
<div
|
||||
className="typeahead__field"
|
||||
>
|
||||
<span
|
||||
className="typeahead__query"
|
||||
>
|
||||
<textarea
|
||||
aria-label="Input query"
|
||||
autoComplete="off"
|
||||
name="q"
|
||||
rows={1}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@@ -1,90 +1,154 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent";
|
||||
import { NotebookTerminalComponent } from "./NotebookTerminalComponent";
|
||||
|
||||
const testAccount: DataModels.DatabaseAccount = {
|
||||
id: "id",
|
||||
kind: "kind",
|
||||
location: "location",
|
||||
name: "name",
|
||||
properties: {
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
},
|
||||
type: "type",
|
||||
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 testMongo32Account: DataModels.DatabaseAccount = {
|
||||
...testAccount,
|
||||
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 testMongo36Account: DataModels.DatabaseAccount = {
|
||||
...testAccount,
|
||||
properties: {
|
||||
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
|
||||
},
|
||||
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 testCassandraAccount: DataModels.DatabaseAccount = {
|
||||
...testAccount,
|
||||
properties: {
|
||||
cassandraEndpoint: "https://testCassandraEndpoint.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 testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
||||
const createTerminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/",
|
||||
},
|
||||
databaseAccount: createTestDatabaseAccount(),
|
||||
});
|
||||
};
|
||||
|
||||
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
const createMongo32Terminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
},
|
||||
databaseAccount: createTestMongo32DatabaseAccount(),
|
||||
});
|
||||
};
|
||||
|
||||
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||
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(),
|
||||
});
|
||||
};
|
||||
|
||||
describe("NotebookTerminalComponent", () => {
|
||||
it("renders terminal", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testAccount,
|
||||
notebookServerInfo: testNotebookServerInfo,
|
||||
};
|
||||
it("getTerminalParams: Test for terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([["terminal", "true"]])
|
||||
);
|
||||
});
|
||||
|
||||
it("renders mongo 3.2 shell", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo32Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
};
|
||||
it("getTerminalParams: Test for Mongo 3.2 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo32Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("renders mongo 3.6 shell", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo36Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
};
|
||||
it("getTerminalParams: Test for Mongo 3.6 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo36Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("renders cassandra shell", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testCassandraAccount,
|
||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||
};
|
||||
it("getTerminalParams: Test for Cassandra terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createCassandraTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host],
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { TerminalProps } from "../../../Terminal/TerminalProps";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as StringUtils from "../../../Utils/StringUtils";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { TerminalQueryParams } from "../../../Common/Constants";
|
||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
@@ -15,69 +15,79 @@ 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"
|
||||
onLoad={(event) => this.handleFrameLoad(event)}
|
||||
src="terminal.html"
|
||||
src={NotebookTerminalComponent.createNotebookAppSrc(this.props.notebookServerInfo, this.getTerminalParams())}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
||||
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
|
||||
this.sendPropsToTerminalFrame();
|
||||
}
|
||||
public getTerminalParams(): Map<string, string> {
|
||||
let params: Map<string, string> = new Map<string, string>();
|
||||
params.set(TerminalQueryParams.Terminal, "true");
|
||||
|
||||
sendPropsToTerminalFrame(): void {
|
||||
if (!this.terminalWindow) {
|
||||
return;
|
||||
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
|
||||
if (terminalEndpoint) {
|
||||
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
public tryGetTerminalEndpoint(): string | undefined {
|
||||
let terminalEndpoint: string | undefined;
|
||||
public tryGetTerminalEndpoint(): string | null {
|
||||
let terminalEndpoint: string | null;
|
||||
|
||||
const notebookServerEndpoint = this.props.notebookServerInfo?.notebookServerEndpoint;
|
||||
const notebookServerEndpoint: string = this.props.notebookServerInfo.notebookServerEndpoint;
|
||||
if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) {
|
||||
// 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;
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -29,9 +29,8 @@ import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { useDialog } from "../Dialog";
|
||||
|
||||
const title = "Open Saved Queries";
|
||||
const title: string = "Open Saved Queries";
|
||||
|
||||
export interface QueriesGridComponentProps {
|
||||
queriesClient: QueriesClient;
|
||||
@@ -197,9 +196,9 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
|
||||
{
|
||||
key: "Action",
|
||||
name: "Action",
|
||||
fieldName: undefined,
|
||||
fieldName: null,
|
||||
minWidth: 70,
|
||||
onRender: (query: Query) => {
|
||||
onRender: (query: Query, index: number, column: IColumn) => {
|
||||
const buttonProps: IButtonProps = {
|
||||
iconProps: {
|
||||
iconName: "More",
|
||||
@@ -215,50 +214,47 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
|
||||
{
|
||||
key: "Open",
|
||||
text: "Open query",
|
||||
onClick: () => {
|
||||
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, menuItem: any) => {
|
||||
this.props.onQuerySelect(query);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Delete",
|
||||
text: "Delete query",
|
||||
onClick: async () => {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
"Are you sure you want to delete this query?",
|
||||
"Delete",
|
||||
async () => {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
});
|
||||
try {
|
||||
await this.props.queriesClient.deleteQuery(query);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
await this.fetchSavedQueries(); // get latest state
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
onClick: async (
|
||||
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
|
||||
menuItem: any
|
||||
) => {
|
||||
if (window.confirm("Are you sure you want to delete this query?")) {
|
||||
const container = window.dataExplorer;
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
});
|
||||
try {
|
||||
await this.props.queriesClient.deleteQuery(query);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: title,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
await this.fetchSavedQueries(); // get latest state
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,6 +16,7 @@ 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";
|
||||
@@ -109,6 +110,7 @@ export interface SettingsComponentState {
|
||||
|
||||
initialNotification: DataModels.Notification;
|
||||
selectedTab: SettingsV2TabTypes;
|
||||
offerLoaded: boolean;
|
||||
}
|
||||
|
||||
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
|
||||
@@ -193,6 +195,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
initialNotification: undefined,
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||
offerLoaded: !!this.offer,
|
||||
};
|
||||
|
||||
this.saveSettingsButton = {
|
||||
@@ -214,6 +217,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
if (this.isCollectionSettingsTab) {
|
||||
this.refreshIndexTransformationProgress();
|
||||
this.loadMongoIndexes();
|
||||
this.loadCollectionOffer();
|
||||
}
|
||||
|
||||
this.setAutoPilotStates();
|
||||
@@ -368,6 +372,34 @@ 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) => {
|
||||
@@ -905,6 +937,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.offerLoaded) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const subSettingsComponentProps: SubSettingsComponentProps = {
|
||||
collection: this.collection,
|
||||
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,
|
||||
|
||||
@@ -30,8 +30,17 @@ 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],
|
||||
@@ -39,11 +48,21 @@ 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],
|
||||
@@ -97,8 +116,17 @@ 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],
|
||||
@@ -106,11 +134,21 @@ 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],
|
||||
|
||||
@@ -58,7 +58,7 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
||||
as="span"
|
||||
className={className}
|
||||
role="presentation"
|
||||
onActivated={() => this.setActiveTab(index)}
|
||||
onActivated={(e) => this.setActiveTab(index)}
|
||||
aria-label={`Select tab: ${tab.title}`}
|
||||
>
|
||||
{tab.title}
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("DataSampleUtils", () => {
|
||||
collections: ko.observableArray<Collection>([collection]),
|
||||
} as Database;
|
||||
const explorer = {} as Explorer;
|
||||
explorer.showOkModalDialog = () => {};
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
const dataSamplesUtil = new DataSamplesUtil(explorer);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
@@ -21,7 +20,7 @@ export class DataSamplesUtil {
|
||||
const containerName = generator.getCollectionId();
|
||||
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
|
||||
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
|
||||
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
logConsoleError(msg);
|
||||
return;
|
||||
}
|
||||
@@ -30,7 +29,7 @@ export class DataSamplesUtil {
|
||||
.createSampleContainerAsync()
|
||||
.catch((error) => logConsoleError(`Error creating sample container: ${error}`));
|
||||
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
|
||||
useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
logConsoleInfo(msg);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
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 { 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 { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
|
||||
export interface EditorNeighborsComponentProps {
|
||||
isSource: boolean;
|
||||
@@ -83,11 +83,11 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
|
||||
}
|
||||
|
||||
private removeCurrentNeighborEdge(index: number): void {
|
||||
const sources = this.props.editedNeighbors.currentNeighbors;
|
||||
const id = sources[index].edgeId;
|
||||
let sources = this.props.editedNeighbors.currentNeighbors;
|
||||
let id = sources[index].edgeId;
|
||||
sources.splice(index, 1);
|
||||
|
||||
const droppedIds = this.props.editedNeighbors.droppedIds;
|
||||
let 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={() => this.removeAddedEdgeToNeighbor(index)} />
|
||||
<img src={DeleteIcon} alt="Delete" onClick={(e) => this.removeAddedEdgeToNeighbor(index)} />
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { GraphHighlightedNodeData, EditedProperties } from "./GraphExplorer";
|
||||
|
||||
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: {
|
||||
@@ -23,6 +24,7 @@ describe("<EditorNodePropertiesComponent />", () => {
|
||||
{ value: true, type: "boolean" },
|
||||
{ value: false, type: "boolean" },
|
||||
{ value: undefined, type: "null" },
|
||||
{ value: null, type: "null" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -39,13 +41,14 @@ describe("<EditorNodePropertiesComponent />", () => {
|
||||
{ value: true, type: "boolean" },
|
||||
{ value: false, type: "boolean" },
|
||||
{ value: undefined, type: "null" },
|
||||
{ value: null, type: "null" },
|
||||
],
|
||||
},
|
||||
],
|
||||
addedProperties: [],
|
||||
droppedKeys: [],
|
||||
},
|
||||
onUpdateProperties,
|
||||
onUpdateProperties: (editedProperties: EditedProperties): void => {},
|
||||
};
|
||||
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -78,7 +81,7 @@ describe("<EditorNodePropertiesComponent />", () => {
|
||||
addedProperties: [],
|
||||
droppedKeys: [],
|
||||
},
|
||||
onUpdateProperties,
|
||||
onUpdateProperties: (editedProperties: EditedProperties): void => {},
|
||||
};
|
||||
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import AddIcon from "../../../../images/Add-property.svg";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
import { EditedProperties } from "./GraphExplorer";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import AddIcon from "../../../../images/Add-property.svg";
|
||||
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
|
||||
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++) {
|
||||
const ip = editedProperties.existingProperties[i];
|
||||
let 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;
|
||||
const ap = editedProperties.addedProperties;
|
||||
let 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;
|
||||
const ap = editedProperties.addedProperties;
|
||||
let 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 = undefined;
|
||||
singleValue.value = null;
|
||||
}
|
||||
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={() => this.removeExistingProperty(key)}
|
||||
onActivated={(e) => 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={() => this.removeExistingProperty(nodeProp.key)}
|
||||
onActivated={(e) => 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 = undefined;
|
||||
firstValue.value = null;
|
||||
}
|
||||
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={() => this.removeAddedProperty(index)}
|
||||
onActivated={(e) => this.removeAddedProperty(index)}
|
||||
>
|
||||
<img src={DeleteIcon} alt="Delete" />
|
||||
</AccessibleElement>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
import * as React from "react";
|
||||
import CancelIcon from "../../../../images/cancel.svg";
|
||||
import CheckIcon from "../../../../images/check-1.svg";
|
||||
import CheckIcon from "../../../../images/check.svg";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import EditIcon from "../../../../images/edit-1.svg";
|
||||
import EditIcon from "../../../../images/edit.svg";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import CloseIcon from "../../../../images/close-black.svg";
|
||||
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
||||
import CloseIcon from "../../../../images/close-black.svg";
|
||||
|
||||
export interface QueryContainerComponentProps {
|
||||
initialQuery: string;
|
||||
@@ -82,7 +82,7 @@ export class QueryContainerComponent extends React.Component<
|
||||
<button
|
||||
type="button"
|
||||
className="filterbtnstyle queryButton"
|
||||
onClick={() => this.props.onExecuteClick(this.state.query)}
|
||||
onClick={(e) => this.props.onExecuteClick(this.state.query)}
|
||||
disabled={this.props.isLoading || !QueryContainerComponent.isQueryValid(this.state.query)}
|
||||
>
|
||||
Execute Gremlin Query
|
||||
|
||||
@@ -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={() => this.props.selectNode(_neighbor.id)}
|
||||
onActivated={(e) => this.props.selectNode(_neighbor.id)}
|
||||
title={GraphUtil.getNeighborTitle(_neighbor)}
|
||||
>
|
||||
{_neighbor.name}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { GraphHighlightedNodeData } from "./GraphExplorer";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
|
||||
export interface ReadOnlyNodePropertiesComponentProps {
|
||||
node: GraphHighlightedNodeData;
|
||||
|
||||
@@ -37,7 +37,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
</td>
|
||||
<td
|
||||
className="valueCol"
|
||||
title="efgh, 1234, true, false, undefined"
|
||||
title="efgh, 1234, true, false, undefined, null"
|
||||
>
|
||||
<div
|
||||
className="propertyValue"
|
||||
@@ -69,6 +69,12 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
>
|
||||
undefined
|
||||
</div>
|
||||
<div
|
||||
className="propertyValue isNull"
|
||||
key="null"
|
||||
>
|
||||
null
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
@@ -172,6 +178,12 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
>
|
||||
undefined
|
||||
</div>
|
||||
<div
|
||||
className="propertyValue isNull"
|
||||
key="null"
|
||||
>
|
||||
null
|
||||
</div>
|
||||
</td>
|
||||
<td />
|
||||
<td
|
||||
|
||||
@@ -8,7 +8,6 @@ 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";
|
||||
@@ -54,8 +53,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||
if (container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker", container.memoryUsageInfo));
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,8 +6,6 @@ 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";
|
||||
|
||||
@@ -29,6 +27,9 @@ 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", () => {
|
||||
@@ -69,19 +70,18 @@ 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", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
@@ -89,6 +89,8 @@ 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",
|
||||
});
|
||||
@@ -99,29 +101,27 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
//expect(enableNotebookBtn).toBeDefined();
|
||||
//expect(enableNotebookBtn.disabled).toBe(false);
|
||||
//expect(enableNotebookBtn.tooltipText).toBe("");
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(false);
|
||||
expect(enableNotebookBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
//expect(enableNotebookBtn).toBeDefined();
|
||||
//expect(enableNotebookBtn.disabled).toBe(true);
|
||||
//expect(enableNotebookBtn.tooltipText).toBe(
|
||||
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
||||
//);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(true);
|
||||
expect(enableNotebookBtn.tooltipText).toBe(
|
||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,25 +138,24 @@ 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",
|
||||
});
|
||||
useNotebook.getState().setIsShellEnabled(true);
|
||||
});
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
|
||||
afterEach(() => {
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
mockExplorer.isShellEnabled = ko.observable(true);
|
||||
});
|
||||
|
||||
it("Mongo Api not available - button should be hidden", () => {
|
||||
@@ -185,7 +184,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
@@ -193,36 +192,30 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openMongoShellBtn.disabled).toBe(true);
|
||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openMongoShellBtn.disabled).toBe(true);
|
||||
//expect(openMongoShellBtn.disabled).toBe(false);
|
||||
//expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
useNotebook.getState().setIsShellEnabled(false);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
mockExplorer.isShellEnabled = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
@@ -243,6 +236,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -253,11 +247,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useNotebook.getState().setIsNotebookEnabled(false);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(false);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
});
|
||||
|
||||
it("Cassandra Api not available - button should be hidden", () => {
|
||||
@@ -268,6 +259,7 @@ 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();
|
||||
@@ -290,7 +282,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
@@ -298,31 +290,24 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
|
||||
//TODO: modify once notebooks are available
|
||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
||||
//expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
//expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -341,17 +326,23 @@ 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", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
@@ -359,7 +350,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||
useNotebook.getState().setIsNotebookEnabled(true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
@@ -385,11 +376,10 @@ 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,
|
||||
|
||||
@@ -22,22 +22,15 @@ 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;
|
||||
@@ -67,50 +60,35 @@ export function createStaticCommandBarButtons(
|
||||
newCollectionBtn.children.push(newDatabaseBtn);
|
||||
}
|
||||
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
buttons.push(createDivider());
|
||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
||||
buttons.push(createDivider());
|
||||
|
||||
if (container.isNotebookEnabled()) {
|
||||
const newNotebookButton = createNewNotebookButton(container);
|
||||
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
|
||||
notebookButtons.push(newNotebookButton);
|
||||
buttons.push(newNotebookButton);
|
||||
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
buttons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
buttons.push(createOpenTerminalButton(container));
|
||||
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
buttons.push(createNotebookWorkspaceResetButton(container));
|
||||
if (
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
container.isShellEnabled() &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
notebookButtons.push(createDivider());
|
||||
buttons.push(createDivider());
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
notebookButtons.push(createOpenCassandraTerminalButton(container));
|
||||
buttons.push(createOpenCassandraTerminalButton(container));
|
||||
} else {
|
||||
notebookButtons.push(createOpenMongoTerminalButton(container));
|
||||
buttons.push(createOpenMongoTerminalButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
notebookButtons.forEach((btn) => {
|
||||
if (userContext.features.notebooksTemporarilyDown) {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
} else {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
|
||||
buttons.push(createDivider());
|
||||
if (!isRunningOnNationalCloud()) {
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
@@ -160,16 +138,14 @@ export function createContextCommandBarButtons(
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
|
||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
if (!userContext.features.notebooksTemporarilyDown) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
}
|
||||
if (container.isShellEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
@@ -177,13 +153,7 @@ export function createContextCommandBarButtons(
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
tooltipText:
|
||||
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
|
||||
? Constants.Notebook.mongoShellTemporarilyDownMsg
|
||||
: undefined,
|
||||
disabled:
|
||||
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
|
||||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
}
|
||||
@@ -299,7 +269,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useNotebook.getState().isSynapseLinkUpdating,
|
||||
disabled: container.isSynapseLinkUpdating(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
@@ -309,8 +279,9 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
return {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
|
||||
onCommandClick: () => {
|
||||
container.openAddDatabasePane();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
@@ -411,13 +382,6 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
|
||||
if (!buttonProps.isDivider) {
|
||||
buttonProps.disabled = true;
|
||||
buttonProps.tooltipText = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
@@ -449,8 +413,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
||||
return {
|
||||
iconSrc: BrowseQueriesIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||
onCommandClick: () => container.openBrowseQueriesPanel(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
@@ -483,18 +446,12 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
||||
),
|
||||
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||
disabled: !container.isNotebooksEnabledForAccount(),
|
||||
ariaLabel: label,
|
||||
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
|
||||
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -518,21 +475,15 @@ 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 =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
if (container.isNotebookEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -550,21 +501,15 @@ 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 =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
if (container.isNotebookEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -591,21 +536,10 @@ 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: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<GitHubReposPanel
|
||||
explorer={container}
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>
|
||||
),
|
||||
onCommandClick: () => container.openGitHubReposPanel(label),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
@@ -620,12 +554,12 @@ function createStaticCommandBarButtonsForResourceToken(
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||
const openQueryBtn = createOpenQueryButton(container);
|
||||
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||
const isResourceTokenCollectionNodeSelected: boolean =
|
||||
resourceTokenCollection?.id() === selectedNodeState.selectedNode?.id();
|
||||
container.resourceTokenCollection() &&
|
||||
container.resourceTokenCollection().id() === selectedNodeState.selectedNode?.id();
|
||||
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||
newSqlQueryBtn.onCommandClick = () => {
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
|
||||
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,14 +6,16 @@ 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 { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
|
||||
|
||||
/**
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
@@ -22,13 +24,6 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
|
||||
const getFilter = (isDisabled: boolean): string => {
|
||||
if (isDisabled) {
|
||||
return StyleConstants.GrayScale;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return btns
|
||||
.filter((btn) => btn)
|
||||
.map(
|
||||
@@ -44,7 +39,6 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined,
|
||||
filter: getFilter(btn.disabled),
|
||||
},
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName,
|
||||
@@ -131,12 +125,8 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||
width: 12,
|
||||
paddingLeft: 1,
|
||||
paddingTop: 6,
|
||||
filter: getFilter(btn.disabled),
|
||||
},
|
||||
imageProps: {
|
||||
src: ChevronDownIcon,
|
||||
alt: btn.iconAlt,
|
||||
},
|
||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,9 +185,12 @@ export const createDivider = (key: string): ICommandBarItemProps => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
||||
export const createMemoryTracker = (
|
||||
key: string,
|
||||
memoryUsageInfo: Observable<MemoryUsageInfo>
|
||||
): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <MemoryTracker />,
|
||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,29 +1,48 @@
|
||||
import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
|
||||
import { Observable, Subscription } from "knockout";
|
||||
import * as React from "react";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
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;
|
||||
|
||||
export const MemoryTracker: React.FC = (): JSX.Element => {
|
||||
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
||||
if (!memoryUsageInfo) {
|
||||
return (
|
||||
<Stack className="memoryTrackerContainer" horizontal>
|
||||
<span>Memory</span>
|
||||
<Spinner size={SpinnerSize.medium} />
|
||||
<ProgressIndicator
|
||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||
percentComplete={usedGB / totalGB}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Dropdown, IDropdownOption } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
|
||||
import ClearIcon from "../../../../images/Clear-1.svg";
|
||||
import ClearIcon from "../../../../images/Clear.svg";
|
||||
import ErrorBlackIcon from "../../../../images/error_black.svg";
|
||||
import ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
|
||||
@@ -277,10 +277,6 @@ export class NotebookComponentBootstrapper {
|
||||
return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>);
|
||||
}
|
||||
|
||||
public isNotebookUntrusted(): boolean {
|
||||
return NotebookUtil.isNotebookUntrusted(this.getStore().getState(), this.contentRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* For display purposes, only return non-killed kernels
|
||||
*/
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import * as React from "react";
|
||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import { connect } from "react-redux";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import * as NteractUtil from "../NTeractUtil";
|
||||
|
||||
interface VirtualCommandBarComponentProps {
|
||||
kernelSpecName: string;
|
||||
kernelStatus: string;
|
||||
currentCellType: string;
|
||||
isNotebookUntrusted: boolean;
|
||||
onRender: () => void;
|
||||
}
|
||||
|
||||
@@ -22,8 +20,7 @@ class VirtualCommandBarComponent extends React.Component<VirtualCommandBarCompon
|
||||
return (
|
||||
this.props.kernelStatus !== nextProps.kernelStatus ||
|
||||
this.props.kernelSpecName !== nextProps.kernelSpecName ||
|
||||
this.props.currentCellType !== nextProps.currentCellType ||
|
||||
this.props.isNotebookUntrusted !== nextProps.isNotebookUntrusted
|
||||
this.props.currentCellType !== nextProps.currentCellType
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +50,6 @@ const makeMapStateToProps = (
|
||||
kernelStatus,
|
||||
kernelSpecName,
|
||||
currentCellType,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
} as VirtualCommandBarComponentProps;
|
||||
}
|
||||
|
||||
@@ -73,7 +69,6 @@ const makeMapStateToProps = (
|
||||
kernelStatus,
|
||||
kernelSpecName,
|
||||
currentCellType,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
onRender: initialProps.onRender,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
|
||||
@@ -31,14 +32,14 @@ interface FileProps {
|
||||
|
||||
export class File extends React.PureComponent<FileProps> {
|
||||
getChoice = () => {
|
||||
let choice;
|
||||
let choice = null;
|
||||
|
||||
// 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 = undefined;
|
||||
} else if (this.props.mimetype === undefined || !TextFile.handles(this.props.mimetype)) {
|
||||
choice = null;
|
||||
} else if (this.props.mimetype == null || !TextFile.handles(this.props.mimetype)) {
|
||||
// This should not happen as we intercept mimetype upstream, but just in case
|
||||
choice = (
|
||||
<PaddedContainer>
|
||||
|
||||
@@ -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 undefined;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ function makeMapStateToTextFileProps(
|
||||
|
||||
return {
|
||||
contentRef,
|
||||
mimetype: content.mimetype !== undefined ? content.mimetype : "text/plain",
|
||||
mimetype: content.mimetype != null ? content.mimetype : "text/plain",
|
||||
text,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,11 +34,9 @@ import {
|
||||
import { webSocket } from "rxjs/webSocket";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useDialog } from "../../Controls/Dialog";
|
||||
import * as FileSystemUtil from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
@@ -687,8 +685,10 @@ const handleKernelConnectionLostEpic = (
|
||||
logConsoleError(msg);
|
||||
logFailureToTelemetry(state, "Kernel restart error", msg);
|
||||
|
||||
useDialog.getState().showOkModalDialog("kernel restarts", msg);
|
||||
|
||||
const explorer = window.dataExplorer;
|
||||
if (explorer) {
|
||||
explorer.showOkModalDialog("kernel restarts", msg);
|
||||
}
|
||||
return of(EMPTY);
|
||||
}
|
||||
|
||||
@@ -772,16 +772,15 @@ const closeUnsupportedMimetypesEpic = (
|
||||
ofType(actions.FETCH_CONTENT_FULFILLED),
|
||||
mergeMap((action) => {
|
||||
const mimetype = action.payload.model.mimetype;
|
||||
if (!TextFile.handles(mimetype)) {
|
||||
const explorer = window.dataExplorer;
|
||||
if (explorer && !TextFile.handles(mimetype)) {
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
explorer.tabsManager.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.`;
|
||||
useDialog.getState().showOkModalDialog("File cannot be rendered", msg);
|
||||
explorer.showOkModalDialog("File cannot be rendered", msg);
|
||||
logConsoleError(msg);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -801,16 +800,17 @@ const closeContentFailedToFetchEpic = (
|
||||
return action$.pipe(
|
||||
ofType(actions.FETCH_CONTENT_FAILED),
|
||||
mergeMap((action) => {
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
const explorer = window.dataExplorer;
|
||||
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)
|
||||
);
|
||||
const msg = `Failed to load file: ${filepath}.`;
|
||||
useDialog.getState().showOkModalDialog("Failure to load", msg);
|
||||
logConsoleError(msg);
|
||||
const msg = `Failed to load file: ${filepath}.`;
|
||||
explorer.showOkModalDialog("Failure to load", msg);
|
||||
logConsoleError(msg);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -8,26 +8,25 @@ 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 onConnectionLost: () => void) {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
constructor(
|
||||
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
||||
private onConnectionLost: () => void,
|
||||
private onMemoryUsageInfoUpdate: (update: DataModels.MemoryUsageInfo) => void
|
||||
) {
|
||||
if (notebookServerInfo() && notebookServerInfo().notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
} else {
|
||||
const unsub = useNotebook.subscribe(
|
||||
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
if (newServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
unsub();
|
||||
},
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
const subscription = notebookServerInfo.subscribe((newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
if (newServerInfo && newServerInfo.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
subscription.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,14 +36,13 @@ export class NotebookContainerClient {
|
||||
private scheduleHeartbeat(delayMs: number): void {
|
||||
setTimeout(() => {
|
||||
this.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
|
||||
.then((memoryUsageInfo) => this.onMemoryUsageInfoUpdate(memoryUsageInfo))
|
||||
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
|
||||
return Promise.reject(error);
|
||||
@@ -100,8 +98,7 @@ export class NotebookContainerClient {
|
||||
}
|
||||
|
||||
private async _resetWorkspace(): Promise<void> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
|
||||
return Promise.reject(error);
|
||||
@@ -120,11 +117,15 @@ export class NotebookContainerClient {
|
||||
}
|
||||
|
||||
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
|
||||
let authToken: string,
|
||||
notebookServerEndpoint = this.notebookServerInfo().notebookServerEndpoint,
|
||||
token = this.notebookServerInfo().authToken;
|
||||
if (token) {
|
||||
authToken = `Token ${token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
|
||||
notebookServerEndpoint,
|
||||
authToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
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 contentProvider: IContentProvider) {}
|
||||
constructor(
|
||||
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
||||
public notebookBasePath: ko.Observable<string>,
|
||||
private contentProvider: IContentProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* This updates the item and points all the children's parent to this item
|
||||
* @param item
|
||||
*/
|
||||
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> {
|
||||
public updateItemChildren(item: NotebookContentItem): Promise<void> {
|
||||
return this.fetchNotebookFiles(item.path).then((subItems) => {
|
||||
item.children = subItems;
|
||||
subItems.forEach((subItem) => (subItem.parent = item));
|
||||
@@ -36,7 +29,7 @@ export class NotebookContentClient {
|
||||
*
|
||||
* @param parent parent folder
|
||||
*/
|
||||
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> {
|
||||
public createNewNotebookFile(parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||
throw new Error(`Parent must be a directory: ${parent}`);
|
||||
}
|
||||
@@ -57,8 +50,6 @@ export class NotebookContentClient {
|
||||
const notebookFile = xhr.response;
|
||||
|
||||
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
|
||||
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
|
||||
// TODO: delete when ResourceTreeAdapter is removed
|
||||
if (parent.children) {
|
||||
item.parent = parent;
|
||||
parent.children.push(item);
|
||||
@@ -68,20 +59,18 @@ export class NotebookContentClient {
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||
const path = await this.deleteNotebookFile(item.path);
|
||||
useNotebook.getState().deleteNotebookItem(item, isGithubTree);
|
||||
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");
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (item.parent && item.parent.children) {
|
||||
// Remove deleted child
|
||||
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
||||
item.parent.children = newChildren;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +82,7 @@ export class NotebookContentClient {
|
||||
public async uploadFileAsync(
|
||||
name: string,
|
||||
content: string,
|
||||
parent: NotebookContentItem,
|
||||
isGithubTree?: boolean
|
||||
parent: NotebookContentItem
|
||||
): Promise<NotebookContentItem> {
|
||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||
throw new Error(`Parent must be a directory: ${parent}`);
|
||||
@@ -118,8 +106,6 @@ export class NotebookContentClient {
|
||||
.then((xhr: AjaxResponse) => {
|
||||
const notebookFile = xhr.response;
|
||||
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
|
||||
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
|
||||
// TODO: delete when ResourceTreeAdapter is removed
|
||||
if (parent.children) {
|
||||
item.parent = parent;
|
||||
parent.children.push(item);
|
||||
@@ -142,11 +128,7 @@ export class NotebookContentClient {
|
||||
* @param sourcePath
|
||||
* @param targetName is not prefixed with path
|
||||
*/
|
||||
public renameNotebook(
|
||||
item: NotebookContentItem,
|
||||
targetName: string,
|
||||
isGithubTree?: boolean
|
||||
): Promise<NotebookContentItem> {
|
||||
public renameNotebook(item: NotebookContentItem, targetName: string): Promise<NotebookContentItem> {
|
||||
const sourcePath = item.path;
|
||||
// Match extension
|
||||
if (sourcePath.indexOf(".") !== -1) {
|
||||
@@ -172,9 +154,6 @@ export class NotebookContentClient {
|
||||
item.name = notebookFile.name;
|
||||
item.path = notebookFile.path;
|
||||
item.timestamp = NotebookUtil.getCurrentTimestamp();
|
||||
|
||||
useNotebook.getState().updateNotebookItem(item, isGithubTree);
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
@@ -184,11 +163,7 @@ export class NotebookContentClient {
|
||||
* @param parent
|
||||
* @param newDirectoryName basename of the new directory
|
||||
*/
|
||||
public async createDirectory(
|
||||
parent: NotebookContentItem,
|
||||
newDirectoryName: string,
|
||||
isGithubTree?: boolean
|
||||
): Promise<NotebookContentItem> {
|
||||
public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise<NotebookContentItem> {
|
||||
if (parent.type !== NotebookContentItemType.Directory) {
|
||||
throw new Error(`Parent is not a directory: ${parent.path}`);
|
||||
}
|
||||
@@ -215,11 +190,8 @@ export class NotebookContentClient {
|
||||
|
||||
const dir = xhr.response;
|
||||
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
|
||||
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
|
||||
// TODO: delete when ResourceTreeAdapter is removed
|
||||
item.parent = parent;
|
||||
parent.children?.push(item);
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
@@ -299,10 +271,9 @@ export class NotebookContentClient {
|
||||
}
|
||||
|
||||
private getServerConfig(): ServerConfig {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
return {
|
||||
endpoint: notebookServerInfo.notebookServerEndpoint,
|
||||
token: notebookServerInfo.authToken,
|
||||
endpoint: this.notebookServerInfo().notebookServerEndpoint,
|
||||
token: this.notebookServerInfo().authToken,
|
||||
crossDomain: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
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";
|
||||
@@ -18,10 +20,8 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import Explorer from "../Explorer";
|
||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
|
||||
@@ -30,7 +30,6 @@ 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;
|
||||
|
||||
@@ -38,6 +37,7 @@ export type { NotebookPaneContent };
|
||||
|
||||
export interface NotebookManagerOptions {
|
||||
container: Explorer;
|
||||
notebookBasePath: ko.Observable<string>;
|
||||
resourceTree: ResourceTreeAdapter;
|
||||
refreshCommandBarButtons: () => void;
|
||||
refreshNotebookList: () => void;
|
||||
@@ -81,28 +81,23 @@ export default class NotebookManager {
|
||||
contents.JupyterContentProvider
|
||||
);
|
||||
|
||||
this.notebookClient = new NotebookContainerClient(() =>
|
||||
this.params.container.initNotebooks(userContext?.databaseAccount)
|
||||
this.notebookClient = new NotebookContainerClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
() => this.params.container.initNotebooks(userContext?.databaseAccount),
|
||||
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
|
||||
);
|
||||
|
||||
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
|
||||
this.notebookContentClient = new NotebookContentClient(
|
||||
this.params.container.notebookServerInfo,
|
||||
this.params.notebookBasePath,
|
||||
this.notebookContentProvider
|
||||
);
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
||||
this.gitHubClient.setToken(token?.access_token);
|
||||
if (this?.gitHubOAuthService.isLoggedIn()) {
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
setTimeout(() => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Manage GitHub settings",
|
||||
<GitHubReposPanel
|
||||
explorer={this.params.container}
|
||||
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
|
||||
junoClientProp={this.junoClient}
|
||||
/>
|
||||
);
|
||||
}, 200);
|
||||
this.params.container.openGitHubReposPanel("Manager GitHub settings", this.junoClient);
|
||||
}
|
||||
|
||||
this.params.refreshCommandBarButtons();
|
||||
@@ -112,7 +107,6 @@ export default class NotebookManager {
|
||||
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
|
||||
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
|
||||
this.params.resourceTree.triggerRender();
|
||||
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
|
||||
});
|
||||
this.refreshPinnedRepos();
|
||||
}
|
||||
@@ -144,7 +138,6 @@ export default class NotebookManager {
|
||||
notebookContentRef={notebookContentRef}
|
||||
onTakeSnapshot={onTakeSnapshot}
|
||||
/>,
|
||||
"440px",
|
||||
onClosePanel
|
||||
);
|
||||
}
|
||||
@@ -173,33 +166,21 @@ export default class NotebookManager {
|
||||
if (error.status === HttpStatusCodes.Unauthorized) {
|
||||
this.gitHubOAuthService.resetToken();
|
||||
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
() =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Connect to GitHub",
|
||||
<GitHubReposPanel
|
||||
explorer={this.params.container}
|
||||
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
|
||||
junoClientProp={this.junoClient}
|
||||
/>
|
||||
),
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
() => this.params.container.openGitHubReposPanel("Connect to GitHub"),
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
|
||||
@@ -6,7 +6,7 @@ import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { userContext } from "../../../UserContext";
|
||||
@@ -14,7 +14,6 @@ import * as cdbActions from "../NotebookComponent/actions";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
|
||||
import { AzureTheme } from "./AzureTheme";
|
||||
import "./base.css";
|
||||
import CellCreator from "./decorators/CellCreator";
|
||||
@@ -108,7 +107,6 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
return (
|
||||
<>
|
||||
<div className="NotebookRendererContainer">
|
||||
<SecurityWarningBar contentRef={this.props.contentRef} />
|
||||
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<KeyboardShortcuts contentRef={this.props.contentRef}>
|
||||
|
||||
@@ -19,12 +19,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.disabledRunCellButton {
|
||||
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
|
||||
color: @BaseMediumHigh;
|
||||
}
|
||||
}
|
||||
|
||||
.greyStopButton {
|
||||
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
|
||||
color: @BaseMediumHigh;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Dispatch } from "redux";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { CdbAppState } from "../NotebookComponent/types";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
|
||||
export interface PassedPromptProps {
|
||||
id: string;
|
||||
@@ -13,7 +12,6 @@ export interface PassedPromptProps {
|
||||
status?: string;
|
||||
executionCount?: number;
|
||||
isHovered?: boolean;
|
||||
isRunDisabled?: boolean;
|
||||
runCell?: () => void;
|
||||
stopCell?: () => void;
|
||||
}
|
||||
@@ -22,7 +20,6 @@ interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
isHovered?: boolean;
|
||||
isNotebookUntrusted?: boolean;
|
||||
children: (props: PassedPromptProps) => React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -50,7 +47,6 @@ export class PromptPure extends React.Component<Props> {
|
||||
runCell: this.props.executeCell,
|
||||
stopCell: this.props.stopExecution,
|
||||
isHovered: this.props.isHovered,
|
||||
isRunDisabled: this.props.isNotebookUntrusted,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -79,7 +75,6 @@ const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((s
|
||||
status,
|
||||
executionCount,
|
||||
isHovered,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { PassedPromptProps } from "./Prompt";
|
||||
import { promptContent } from "./PromptContent";
|
||||
|
||||
describe("PromptContent", () => {
|
||||
it("renders for busy status", () => {
|
||||
const props: PassedPromptProps = {
|
||||
id: "id",
|
||||
contentRef: "contentRef",
|
||||
status: "busy",
|
||||
};
|
||||
const wrapper = shallow(promptContent(props));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders when hovered", () => {
|
||||
const props: PassedPromptProps = {
|
||||
id: "id",
|
||||
contentRef: "contentRef",
|
||||
isHovered: true,
|
||||
};
|
||||
const wrapper = shallow(promptContent(props));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
import { IconButton, Spinner, SpinnerSize } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import { PassedPromptProps } from "./Prompt";
|
||||
import "./Prompt.less";
|
||||
|
||||
export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
||||
if (props.status === "busy") {
|
||||
const stopButtonText = "Stop cell execution";
|
||||
const stopButtonText: string = "Stop cell execution";
|
||||
return (
|
||||
<div
|
||||
style={{ position: "sticky", width: "100%", maxHeight: "100%", left: 0, top: 0, zIndex: 300 }}
|
||||
@@ -24,18 +23,15 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
||||
</div>
|
||||
);
|
||||
} else if (props.isHovered) {
|
||||
const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell";
|
||||
const playButtonText: string = "Run cell";
|
||||
return (
|
||||
<div className={props.isRunDisabled ? "disabledRunCellButton" : ""}>
|
||||
<IconButton
|
||||
className="runCellButton"
|
||||
iconProps={{ iconName: "MSNVideosSolid" }}
|
||||
title={playButtonText}
|
||||
ariaLabel={playButtonText}
|
||||
disabled={props.isRunDisabled}
|
||||
onClick={props.runCell}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
className="runCellButton"
|
||||
iconProps={{ iconName: "MSNVideosSolid" }}
|
||||
title={playButtonText}
|
||||
ariaLabel={playButtonText}
|
||||
onClick={props.runCell}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { StatusBar } from "./StatusBar";
|
||||
|
||||
describe("StatusBar", () => {
|
||||
@@ -27,8 +28,8 @@ describe("StatusBar", () => {
|
||||
kernelSpecDisplayName: "javascript",
|
||||
kernelStatus: "kernelStatus",
|
||||
},
|
||||
undefined,
|
||||
undefined
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(shouldUpdate).toBe(true);
|
||||
});
|
||||
@@ -46,8 +47,8 @@ describe("StatusBar", () => {
|
||||
kernelSpecDisplayName: "python3",
|
||||
kernelStatus: "kernelStatus",
|
||||
},
|
||||
undefined,
|
||||
undefined
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(shouldUpdate).toBe(true);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 {
|
||||
@@ -13,6 +12,8 @@ interface Props {
|
||||
|
||||
const NOT_CONNECTED = "not connected";
|
||||
|
||||
import styled from "styled-components";
|
||||
|
||||
export const LeftStatus = styled.div`
|
||||
float: left;
|
||||
display: block;
|
||||
@@ -79,7 +80,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) => {
|
||||
@@ -89,26 +90,26 @@ const makeMapStateToProps = (_initialState: AppState, initialProps: InitialProps
|
||||
return {
|
||||
kernelStatus: NOT_CONNECTED,
|
||||
kernelSpecDisplayName: "no kernel",
|
||||
lastSaved: undefined,
|
||||
lastSaved: null,
|
||||
};
|
||||
}
|
||||
|
||||
const kernelRef = content.model.kernelRef;
|
||||
let kernel;
|
||||
let kernel = null;
|
||||
if (kernelRef) {
|
||||
kernel = selectors.kernel(state, { kernelRef });
|
||||
}
|
||||
|
||||
const lastSaved = content && content.lastSaved ? content.lastSaved : undefined;
|
||||
const lastSaved = content && content.lastSaved ? content.lastSaved : null;
|
||||
|
||||
const kernelStatus = kernel?.status || NOT_CONNECTED;
|
||||
const kernelStatus = kernel != null && kernel.status != null ? 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?.kernelSpecName) {
|
||||
} else if (kernel != null && kernel.kernelSpecName != null) {
|
||||
kernelSpecDisplayName = kernel.kernelSpecName;
|
||||
} else if (content && content.type === "notebook") {
|
||||
kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " ";
|
||||
|
||||
@@ -27,7 +27,7 @@ interface DispatchProps {
|
||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
||||
clearOutputs: () => void;
|
||||
deleteCell: () => void;
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) => void;
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
|
||||
takeNotebookSnapshot: (payload: SnapshotRequest) => void;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ interface StateProps {
|
||||
cellIdAbove: CellId;
|
||||
cellIdBelow: CellId;
|
||||
hasCodeOutput: boolean;
|
||||
isNotebookUntrusted: boolean;
|
||||
}
|
||||
|
||||
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
|
||||
@@ -44,16 +43,12 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
|
||||
render(): JSX.Element {
|
||||
let items: IContextualMenuItem[] = [];
|
||||
const isNotebookUntrusted = this.props.isNotebookUntrusted;
|
||||
const runTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
|
||||
|
||||
if (this.props.cellType === "code") {
|
||||
items = items.concat([
|
||||
{
|
||||
key: "Run",
|
||||
text: "Run",
|
||||
title: runTooltip,
|
||||
disabled: isNotebookUntrusted,
|
||||
onClick: () => {
|
||||
this.props.executeCell();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
|
||||
@@ -208,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?: string) =>
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
|
||||
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data })),
|
||||
takeNotebookSnapshot: (request: SnapshotRequest) => dispatch(cdbActions.takeNotebookSnapshot(request)),
|
||||
});
|
||||
@@ -228,7 +223,6 @@ const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state
|
||||
cellIdAbove,
|
||||
cellIdBelow,
|
||||
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, ownProps.contentRef),
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PromptContent renders for busy status 1`] = `
|
||||
<div
|
||||
className="greyStopButton"
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"maxHeight": "100%",
|
||||
"position": "sticky",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
"zIndex": 300,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Stop cell execution"
|
||||
className="runCellButton"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "CircleStopSolid",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
title="Stop cell execution"
|
||||
/>
|
||||
<StyledSpinnerBase
|
||||
size={3}
|
||||
style={
|
||||
Object {
|
||||
"paddingTop": 5,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PromptContent renders when hovered 1`] = `
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Run cell"
|
||||
className="runCellButton"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "MSNVideosSolid",
|
||||
}
|
||||
}
|
||||
title="Run cell"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,7 +1,8 @@
|
||||
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 {
|
||||
@@ -28,7 +29,10 @@ class HoverableCell extends React.Component<ComponentProps & DispatchProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch, { id }: { id: string }): DispatchProps => ({
|
||||
const mapDispatchToProps = (
|
||||
dispatch: Dispatch,
|
||||
{ id, contentRef }: { id: string; contentRef: ContentRef }
|
||||
): DispatchProps => ({
|
||||
hover: () => dispatch(actions.setHoveredCell({ cellId: id })),
|
||||
unHover: () => dispatch(actions.setHoveredCell({ cellId: undefined })),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DropTargetConnector,
|
||||
DropTargetMonitor,
|
||||
} from "react-dnd";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import styled, { StyledComponent } from "styled-components";
|
||||
@@ -122,10 +123,9 @@ export const cellTarget = {
|
||||
drop(props: Props, monitor: DropTargetMonitor, component: any): void {
|
||||
if (monitor) {
|
||||
const hoverUpperHalf = isDragUpper(props, monitor, component.el);
|
||||
const item: Props = monitor.getItem();
|
||||
// DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation.
|
||||
props.moveCell({
|
||||
id: item.id,
|
||||
id: monitor.getItem().id,
|
||||
destinationId: props.id,
|
||||
above: hoverUpperHalf,
|
||||
contentRef: props.contentRef,
|
||||
|
||||
@@ -4,7 +4,6 @@ import Immutable from "immutable";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { NotebookUtil } from "../../../NotebookUtil";
|
||||
|
||||
interface ComponentProps {
|
||||
contentRef: ContentRef;
|
||||
@@ -15,7 +14,6 @@ interface StateProps {
|
||||
cellMap: Immutable.Map<string, any>;
|
||||
cellOrder: Immutable.List<string>;
|
||||
focusedCell?: string | null;
|
||||
isNotebookUntrusted: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
@@ -61,13 +59,8 @@ export class KeyboardShortcuts extends React.Component<Props> {
|
||||
cellOrder,
|
||||
focusedCell,
|
||||
cellMap,
|
||||
isNotebookUntrusted,
|
||||
} = this.props;
|
||||
|
||||
if (isNotebookUntrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ctrlKeyPressed = e.ctrlKey;
|
||||
// Allow cmd + enter (macOS) to operate like ctrl + enter
|
||||
if (process.platform === "darwin") {
|
||||
@@ -132,7 +125,6 @@ export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps)
|
||||
cellOrder,
|
||||
cellMap,
|
||||
focusedCell,
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable";
|
||||
import { AppState, selectors } from "@nteract/core";
|
||||
import domtoimage from "dom-to-image";
|
||||
import Html2Canvas from "html2canvas";
|
||||
import path from "path";
|
||||
@@ -12,8 +11,6 @@ import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentI
|
||||
export type FileType = "directory" | "file" | "notebook";
|
||||
// Utilities for notebooks
|
||||
export class NotebookUtil {
|
||||
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
|
||||
|
||||
/**
|
||||
* It's a notebook file if the filename ends with .ipynb.
|
||||
*/
|
||||
@@ -156,16 +153,6 @@ export class NotebookUtil {
|
||||
);
|
||||
}
|
||||
|
||||
public static isNotebookUntrusted(state: AppState, contentRef: string): boolean {
|
||||
const content = selectors.content(state, { contentRef });
|
||||
if (content?.type === "notebook") {
|
||||
const metadata = selectors.notebook.metadata(content.model);
|
||||
return metadata.getIn(["untrusted"]) as boolean;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find code cells with display
|
||||
* @param notebookObject
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { SecurityWarningBar } from "./SecurityWarningBar";
|
||||
|
||||
describe("SecurityWarningBar", () => {
|
||||
it("renders if notebook is untrusted", () => {
|
||||
const wrapper = shallow(
|
||||
<SecurityWarningBar
|
||||
contentRef={"contentRef"}
|
||||
isNotebookUntrusted={true}
|
||||
markNotebookAsTrusted={undefined}
|
||||
saveNotebook={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders if notebook is trusted", () => {
|
||||
const wrapper = shallow(
|
||||
<SecurityWarningBar
|
||||
contentRef={"contentRef"}
|
||||
isNotebookUntrusted={false}
|
||||
markNotebookAsTrusted={undefined}
|
||||
saveNotebook={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||
import { actions, AppState } from "@nteract/core";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
|
||||
export interface SecurityWarningBarPureProps {
|
||||
contentRef: string;
|
||||
}
|
||||
|
||||
interface SecurityWarningBarDispatchProps {
|
||||
markNotebookAsTrusted: (contentRef: string) => void;
|
||||
saveNotebook: (contentRef: string) => void;
|
||||
}
|
||||
|
||||
type SecurityWarningBarProps = SecurityWarningBarPureProps & StateProps & SecurityWarningBarDispatchProps;
|
||||
|
||||
interface SecurityWarningBarState {
|
||||
isBarDismissed: boolean;
|
||||
}
|
||||
|
||||
export class SecurityWarningBar extends React.Component<SecurityWarningBarProps, SecurityWarningBarState> {
|
||||
constructor(props: SecurityWarningBarProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isBarDismissed: false,
|
||||
};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return this.props.isNotebookUntrusted && !this.state.isBarDismissed ? (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
isMultiline={false}
|
||||
onDismiss={() => this.setState({ isBarDismissed: true })}
|
||||
dismissButtonAriaLabel="Close"
|
||||
actions={
|
||||
<MessageBarButton
|
||||
onClick={() => {
|
||||
this.props.markNotebookAsTrusted(this.props.contentRef);
|
||||
this.props.saveNotebook(this.props.contentRef);
|
||||
}}
|
||||
>
|
||||
Trust Notebook
|
||||
</MessageBarButton>
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone
|
||||
else may involve security risks.
|
||||
</MessageBar>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
isNotebookUntrusted: boolean;
|
||||
}
|
||||
|
||||
interface InitialProps {
|
||||
contentRef: string;
|
||||
}
|
||||
|
||||
// Redux
|
||||
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
||||
const mapStateToProps = (state: AppState): StateProps => ({
|
||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, initialProps.contentRef),
|
||||
});
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const makeMapDispatchToProps = () => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch): SecurityWarningBarDispatchProps => {
|
||||
return {
|
||||
markNotebookAsTrusted: (contentRef: string) => {
|
||||
return dispatch(
|
||||
actions.deleteMetadataField({
|
||||
contentRef,
|
||||
field: "untrusted",
|
||||
})
|
||||
);
|
||||
},
|
||||
saveNotebook: (contentRef: string) => dispatch(actions.save({ contentRef })),
|
||||
};
|
||||
};
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SecurityWarningBar);
|
||||
@@ -1,22 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SecurityWarningBar renders if notebook is trusted 1`] = `<Fragment />`;
|
||||
|
||||
exports[`SecurityWarningBar renders if notebook is untrusted 1`] = `
|
||||
<StyledMessageBar
|
||||
actions={
|
||||
<CustomizedMessageBarButton
|
||||
onClick={[Function]}
|
||||
>
|
||||
Trust Notebook
|
||||
</CustomizedMessageBarButton>
|
||||
}
|
||||
dismissButtonAriaLabel="Close"
|
||||
isMultiline={false}
|
||||
messageBarType={5}
|
||||
onDismiss={[Function]}
|
||||
>
|
||||
|
||||
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone else may involve security risks.
|
||||
</StyledMessageBar>
|
||||
`;
|
||||
@@ -1,249 +0,0 @@
|
||||
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;
|
||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => 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;
|
||||
},
|
||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, parent);
|
||||
item.parent = parentItem;
|
||||
if (parentItem.children) {
|
||||
parentItem.children.push(item);
|
||||
} else {
|
||||
parentItem.children = [item];
|
||||
}
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : 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;
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, item.parent);
|
||||
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : 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: [],
|
||||
parent: gitHubNotebooksContentRoot,
|
||||
};
|
||||
|
||||
pinnedRepo.branches.forEach((branch) => {
|
||||
repoTreeItem.children.push({
|
||||
name: branch.name,
|
||||
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
|
||||
type: NotebookContentItemType.Directory,
|
||||
parent: repoTreeItem,
|
||||
});
|
||||
});
|
||||
|
||||
gitHubNotebooksContentRoot.children.push(repoTreeItem);
|
||||
});
|
||||
|
||||
set({ gitHubNotebooksContentRoot });
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -113,7 +113,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
collectionId: "",
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: this.getPartitionKey(),
|
||||
partitionKey: "",
|
||||
enableDedicatedThroughput: false,
|
||||
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
|
||||
useHashV2: false,
|
||||
@@ -413,10 +413,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Text variant="small" aria-label="pkDescription">
|
||||
{this.getPartitionKeySubtext()}
|
||||
</Text>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="addCollection-partitionKeyValue"
|
||||
@@ -811,30 +807,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return tooltipText;
|
||||
}
|
||||
|
||||
private getPartitionKey(): string {
|
||||
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
||||
return "";
|
||||
}
|
||||
if (userContext.features.partitionKeyDefault) {
|
||||
return userContext.apiType === "SQL" ? "/id" : "_id";
|
||||
}
|
||||
if (userContext.features.partitionKeyDefault2) {
|
||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
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">
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -37,7 +36,7 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
||||
selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query);
|
||||
}
|
||||
|
||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab);
|
||||
queryTab.tabTitle(savedQuery.queryName);
|
||||
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ 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";
|
||||
@@ -98,31 +97,28 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
|
||||
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
|
||||
let parent: NotebookContentItem;
|
||||
let isGithubTree: boolean;
|
||||
switch (location.type) {
|
||||
case "MyNotebooks":
|
||||
parent = {
|
||||
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
path: container.getNotebookBasePath(),
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
isGithubTree = false;
|
||||
break;
|
||||
|
||||
case "GitHub":
|
||||
parent = {
|
||||
name: selectedLocation.branch,
|
||||
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
isGithubTree = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported location type ${location.type}`);
|
||||
}
|
||||
|
||||
return container.uploadFile(name, content, parent, isGithubTree);
|
||||
return container.uploadFile(name, content, parent);
|
||||
};
|
||||
|
||||
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
@@ -52,7 +53,10 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
const fakeExplorer = new Explorer();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
|
||||
const database = { id: ko.observable("testDB") } as Database;
|
||||
@@ -61,11 +65,11 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
database.isDatabaseShared = ko.computed(() => false);
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
useSelectedNode.getState().setSelectedNode(database);
|
||||
wrapper.setProps({});
|
||||
wrapper.setProps({ explorer: fakeExplorer });
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
|
||||
database.isDatabaseShared = ko.computed(() => true);
|
||||
wrapper.setProps({});
|
||||
wrapper.setProps({ explorer: fakeExplorer });
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -73,6 +77,8 @@ 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),
|
||||
@@ -109,7 +115,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
it("should call delete collection", () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
@@ -126,7 +132,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmCollectionId")
|
||||
|
||||
@@ -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 {
|
||||
refreshDatabases: () => Promise<void>;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
||||
refreshDatabases,
|
||||
explorer,
|
||||
}: DeleteCollectionConfirmationPaneProps) => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||
@@ -31,7 +31,8 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
|
||||
const shouldRecordFeedback = (): boolean =>
|
||||
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
useDatabases.getState().isLastCollection() &&
|
||||
!useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
@@ -62,12 +63,10 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
|
||||
setIsExecuting(false);
|
||||
useSelectedNode.getState().setSelectedNode(collection.database);
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
refreshDatabases();
|
||||
explorer.tabsManager?.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
explorer.refreshAllDatabases();
|
||||
|
||||
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
|
||||
<DeleteCollectionConfirmationPane
|
||||
refreshDatabases={[Function]}
|
||||
explorer={
|
||||
Object {
|
||||
"refreshAllDatabases": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<RightPaneForm
|
||||
formError=""
|
||||
|
||||
@@ -10,12 +10,15 @@ 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(() => {
|
||||
@@ -34,6 +37,10 @@ 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);
|
||||
@@ -49,17 +56,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 refreshDatabases={() => undefined} />);
|
||||
const wrapper = shallow(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
|
||||
|
||||
useDatabases.getState().addDatabases([database]);
|
||||
wrapper.setProps({});
|
||||
wrapper.setProps({ explorer: fakeExplorer });
|
||||
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
|
||||
useDatabases.getState().clearDatabases();
|
||||
});
|
||||
|
||||
it("Should call delete database", () => {
|
||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
|
||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
||||
|
||||
@@ -74,7 +81,7 @@ describe("Delete Database Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
|
||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
|
||||
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmDatabaseId")
|
||||
|
||||
@@ -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 {
|
||||
refreshDatabases: () => Promise<void>;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
|
||||
refreshDatabases,
|
||||
explorer,
|
||||
}: 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 = useDatabases.getState().findSelectedDatabase();
|
||||
const selectedDatabase: Database = useSelectedNode.getState().findSelectedDatabase();
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
|
||||
@@ -52,18 +52,15 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
try {
|
||||
await deleteDatabase(selectedDatabase.id());
|
||||
closeSidePanel();
|
||||
refreshDatabases();
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
|
||||
explorer.refreshAllDatabases();
|
||||
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
selectedDatabase
|
||||
.collections()
|
||||
.forEach((collection: Collection) =>
|
||||
useTabs
|
||||
.getState()
|
||||
.closeTabsByComparator(
|
||||
(tab) =>
|
||||
tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
)
|
||||
explorer.tabsManager.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
)
|
||||
);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteDatabase,
|
||||
|
||||
@@ -120,7 +120,6 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
||||
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
||||
}
|
||||
}
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
}
|
||||
|
||||
public resetData(): void {
|
||||
@@ -145,18 +144,11 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
||||
|
||||
private setup(forceShowConnectToGitHub = false): void {
|
||||
forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn()
|
||||
? this.setupForConnectToGitHub(forceShowConnectToGitHub)
|
||||
? this.setupForConnectToGitHub()
|
||||
: this.setupForManageRepos();
|
||||
}
|
||||
|
||||
private setupForConnectToGitHub(forceShowConnectToGitHub: boolean): void {
|
||||
if (forceShowConnectToGitHub) {
|
||||
const newState = { ...this.state.gitHubReposState };
|
||||
newState.showAuthorizeAccess = forceShowConnectToGitHub;
|
||||
this.setState({
|
||||
gitHubReposState: newState,
|
||||
});
|
||||
}
|
||||
private setupForConnectToGitHub(): void {
|
||||
this.setState({
|
||||
isExecuting: false,
|
||||
});
|
||||
@@ -376,28 +368,46 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,17 @@ 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],
|
||||
@@ -28,11 +37,21 @@ 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],
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
padding: 0 34px;
|
||||
margin: 20px 0;
|
||||
overflow: auto;
|
||||
min-height: 30px;
|
||||
|
||||
& > :not(.collapsibleSection) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
@@ -150,6 +151,9 @@
|
||||
.backImageIcon {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entityValueTextField {
|
||||
margin: 24px;
|
||||
}
|
||||
.addEntityDatePicker {
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
@@ -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?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
|
||||
if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||
private onDissmiss = (ev?: React.SyntheticEvent<HTMLElement>): void => {
|
||||
if ((ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||
ev.preventDefault();
|
||||
} else {
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
@@ -85,12 +85,11 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
|
||||
export const SidePanel: React.FC = () => {
|
||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||
const { isOpen, panelContent, 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
|
||||
@@ -101,7 +100,6 @@ export const SidePanel: React.FC = () => {
|
||||
panelContent={panelContent}
|
||||
headerText={headerText}
|
||||
isConsoleExpanded={isConsoleExpanded}
|
||||
panelWidth={panelWidth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
|
||||
notebookName,
|
||||
notebookDescription,
|
||||
notebookTags?.split(","),
|
||||
author,
|
||||
imageSrc,
|
||||
content
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ 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";
|
||||
@@ -35,7 +34,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||
logConsoleError("Failed to save query: account not setup to save queries");
|
||||
}
|
||||
|
||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||
const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab);
|
||||
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
|
||||
|
||||
if (!queryName || queryName.length === 0) {
|
||||
|
||||
@@ -63,7 +63,7 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
explorer.refreshExplorer();
|
||||
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks
|
||||
|
||||
closeSidePanel();
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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;
|
||||
@@ -22,6 +23,7 @@ export interface StringInputPanelProps {
|
||||
}
|
||||
|
||||
export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
|
||||
explorer: container,
|
||||
closePanel,
|
||||
errorMessage,
|
||||
inProgressMessage,
|
||||
@@ -53,12 +55,10 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
|
||||
logConsoleInfo(`${successMessage}: ${stringInput}`);
|
||||
const originalPath = notebookFile.path;
|
||||
|
||||
const notebookTabs = useTabs
|
||||
.getState()
|
||||
.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
|
||||
);
|
||||
const notebookTabs = container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
|
||||
);
|
||||
notebookTabs.forEach((tab) => {
|
||||
tab.tabTitle(newNotebookFile.name);
|
||||
tab.tabPath(newNotebookFile.path);
|
||||
|
||||
@@ -9,8 +9,17 @@ 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],
|
||||
@@ -18,11 +27,21 @@ 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 "
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { IDropdownOption, Image, IPanelProps, IRenderFunction, 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 { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import { PanelContainerComponent } from "../PanelContainerComponent";
|
||||
import {
|
||||
attributeNameLabel,
|
||||
attributeValueLabel,
|
||||
@@ -30,7 +30,9 @@ import {
|
||||
getCassandraDefaultEntities,
|
||||
getDefaultEntities,
|
||||
getEntityValuePlaceholder,
|
||||
getPanelTitle,
|
||||
imageProps,
|
||||
isValidEntities,
|
||||
options,
|
||||
} from "./Validators/EntityTableHelper";
|
||||
|
||||
@@ -59,6 +61,7 @@ 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>("");
|
||||
@@ -67,8 +70,6 @@ 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(() => {
|
||||
@@ -97,36 +98,19 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
};
|
||||
|
||||
/* Add new entity attribute */
|
||||
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;
|
||||
}
|
||||
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
|
||||
if (!isValidEntities(entities)) {
|
||||
return undefined;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
setIsExecuting(true);
|
||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||
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);
|
||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||
tableEntityListViewModel.redrawTableThrottled();
|
||||
}
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
||||
@@ -216,80 +200,110 @@ 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 (
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting,
|
||||
submitButtonText: getButtonLabel(userContext.apiType),
|
||||
onSubmit,
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<PanelContainerComponent
|
||||
headerText={getPanelTitle(userContext.apiType)}
|
||||
panelWidth="700px"
|
||||
isOpen={true}
|
||||
panelContent={renderPanelContent()}
|
||||
isConsoleExpanded={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { IDropdownOption, Image, IPanelProps, IRenderFunction, 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 { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import { PanelContainerComponent } from "../PanelContainerComponent";
|
||||
import {
|
||||
attributeNameLabel,
|
||||
attributeValueLabel,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
getEntityValuePlaceholder,
|
||||
getFormattedTime,
|
||||
imageProps,
|
||||
isValidEntities,
|
||||
options,
|
||||
} from "./Validators/EntityTableHelper";
|
||||
|
||||
@@ -58,13 +59,12 @@ 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,44 +190,26 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
return displayValue;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
|
||||
if (!isValidEntities(entities)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setIsExecuting(true);
|
||||
event.preventDefault();
|
||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||
const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient;
|
||||
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
|
||||
|
||||
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);
|
||||
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);
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
||||
@@ -317,81 +299,109 @@ 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 (
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting,
|
||||
submitButtonText: "Update",
|
||||
onSubmit,
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<PanelContainerComponent
|
||||
headerText="Edit Table Entity"
|
||||
panelWidth="700px"
|
||||
isOpen={true}
|
||||
panelContent={renderPanelContent()}
|
||||
isConsoleExpanded={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2
|
||||
|
||||
export const columnProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 10 },
|
||||
styles: { root: { marginBottom: 8 } },
|
||||
styles: { root: { width: 680 } },
|
||||
};
|
||||
|
||||
// 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, type } = entities[i];
|
||||
if (property === "" || property === undefined || !type) {
|
||||
const { property } = entities[i];
|
||||
if (property === "" || property === undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,13 @@ 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";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user