From 4be53284b560c059fa186bf5c0402b3410713e19 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Wed, 20 Jan 2021 09:15:01 -0600 Subject: [PATCH] Prettier 2.0 (#393) --- .eslintrc.js | 24 +- babel.config.js | 2 +- jest-puppeteer.config.js | 4 +- jest.config.e2e.js | 2 +- jest.config.js | 10 +- package-lock.json | 6 +- package.json | 2 +- src/AuthType.ts | 14 +- src/Bindings/BindingHandlersRegisterer.ts | 44 +- src/Bindings/ReactBindingHandler.ts | 2 +- src/Common/ArrayHashMap.ts | 2 +- src/Common/Constants.ts | 13 +- src/Common/CosmosClient.test.ts | 24 +- src/Common/CosmosClient.ts | 10 +- src/Common/EditableUtility.ts | 188 +- src/Common/EnvironmentUtility.ts | 12 +- src/Common/ErrorHandlingUtils.ts | 2 +- src/Common/HeadersUtility.test.ts | 50 +- src/Common/HeadersUtility.ts | 56 +- src/Common/IteratorUtilities.test.ts | 4 +- src/Common/IteratorUtilities.ts | 4 +- src/Common/Logger.test.ts | 52 +- src/Common/Logger.ts | 4 +- src/Common/MessageHandler.test.ts | 56 +- src/Common/MessageHandler.ts | 4 +- src/Common/MongoProxyClient.test.ts | 38 +- src/Common/MongoProxyClient.ts | 66 +- src/Common/MongoUtility.ts | 336 +- src/Common/OfferUtility.test.ts | 22 +- src/Common/OfferUtility.ts | 4 +- src/Common/PortalNotifications.ts | 2 +- src/Common/QueriesClient.ts | 438 +- src/Common/Splitter.ts | 216 +- src/Common/UrlUtility.ts | 4 +- .../dataAccess/createCollection.test.ts | 24 +- src/Common/dataAccess/createCollection.ts | 48 +- src/Common/dataAccess/createDatabase.ts | 38 +- .../dataAccess/createStoredProcedure.ts | 8 +- src/Common/dataAccess/createTrigger.ts | 11 +- .../dataAccess/createUserDefinedFunction.ts | 8 +- .../dataAccess/deleteCollection.test.ts | 10 +- src/Common/dataAccess/deleteCollection.ts | 5 +- src/Common/dataAccess/deleteConflict.ts | 2 +- src/Common/dataAccess/deleteDatabase.test.ts | 8 +- src/Common/dataAccess/deleteDatabase.ts | 4 +- .../dataAccess/deleteStoredProcedure.ts | 6 +- src/Common/dataAccess/deleteTrigger.ts | 6 +- .../dataAccess/deleteUserDefinedFunction.ts | 6 +- .../dataAccess/executeStoredProcedure.ts | 2 +- .../dataAccess/getCollectionDataUsageSize.ts | 4 +- .../getIndexTransformationProgress.ts | 5 +- src/Common/dataAccess/queryConflicts.ts | 5 +- src/Common/dataAccess/queryDocuments.ts | 5 +- src/Common/dataAccess/readCollection.test.ts | 10 +- src/Common/dataAccess/readCollection.ts | 5 +- src/Common/dataAccess/readCollectionOffer.ts | 4 +- src/Common/dataAccess/readCollections.test.ts | 12 +- src/Common/dataAccess/readCollections.ts | 7 +- src/Common/dataAccess/readDatabaseOffer.ts | 4 +- src/Common/dataAccess/readDatabases.test.ts | 10 +- src/Common/dataAccess/readDatabases.ts | 6 +- src/Common/dataAccess/readOfferWithSDK.ts | 10 +- src/Common/dataAccess/readOffers.ts | 4 +- src/Common/dataAccess/readStoredProcedures.ts | 2 +- src/Common/dataAccess/readTriggers.ts | 8 +- .../dataAccess/readUserDefinedFunctions.ts | 2 +- src/Common/dataAccess/updateCollection.ts | 12 +- src/Common/dataAccess/updateOffer.ts | 30 +- .../dataAccess/updateStoredProcedure.ts | 8 +- src/Common/dataAccess/updateTrigger.ts | 6 +- .../dataAccess/updateUserDefinedFunction.ts | 8 +- src/ConfigContext.ts | 10 +- src/Contracts/ActionContracts.ts | 6 +- src/Contracts/DataModels.ts | 1178 ++-- src/Contracts/Diagnostics.ts | 2 +- src/Contracts/ExplorerContracts.ts | 78 +- src/Contracts/SubscriptionType.ts | 2 +- src/Contracts/Versions.ts | 8 +- src/Contracts/ViewModels.ts | 880 +-- src/Controls/Heatmap/Heatmap.test.ts | 26 +- src/Controls/Heatmap/Heatmap.ts | 26 +- src/Controls/Heatmap/HeatmapDatatypes.ts | 2 +- src/DefaultAccountExperienceType.ts | 2 +- src/Definitions/datatables.d.ts | 3908 +++++------ src/Definitions/jquery-typescript.d.ts | 68 +- src/Definitions/jquery-ui.d.ts | 3542 +++++----- src/Definitions/jquery.d.ts | 3780 +++++----- src/Explorer/ComponentRegisterer.test.ts | 244 +- src/Explorer/ComponentRegisterer.ts | 154 +- src/Explorer/ContextMenuButtonFactory.ts | 32 +- .../AccessibleElement/AccessibleElement.tsx | 2 +- .../Controls/Accordion/AccordionComponent.tsx | 4 +- .../Controls/Arcadia/ArcadiaMenuPicker.tsx | 34 +- .../CollapsiblePanelComponent.ts | 112 +- .../CollapsibleSectionComponent.test.tsx | 2 +- .../CollapsibleSectionComponent.tsx | 2 +- .../collapsible-panel-component.html | 88 +- .../CommandButton/CommandButtonComponent.tsx | 6 +- .../DialogReactComponent/DialogComponent.tsx | 202 +- .../DialogComponentAdapter.tsx | 32 +- .../DiffEditor/DiffEditorComponent.ts | 8 +- ...DefaultDirectoryDropdownComponent.test.tsx | 24 +- .../DefaultDirectoryDropdownComponent.tsx | 10 +- .../Directory/DirectoryListComponent.test.tsx | 4 +- .../Directory/DirectoryListComponent.tsx | 24 +- .../Controls/DynamicList/DynamicList.test.ts | 2 +- .../DynamicList/DynamicListComponent.ts | 2 +- .../Controls/Editor/EditorComponent.ts | 126 +- src/Explorer/Controls/Editor/EditorReact.tsx | 2 +- .../ErrorDisplayComponent.ts | 54 +- .../error-display-component.html | 12 +- .../FeaturePanel/FeaturePanelComponent.tsx | 64 +- .../FeaturePanel/FeaturePanelLauncher.tsx | 16 +- .../Controls/GitHub/AddRepoComponent.tsx | 264 +- .../GitHub/AuthorizeAccessComponent.tsx | 182 +- .../Controls/GitHub/GitHubReposComponent.tsx | 174 +- .../GitHub/GitHubReposComponentAdapter.tsx | 40 +- .../Controls/GitHub/GitHubStyleConstants.ts | 116 +- .../Controls/GitHub/ReposListComponent.tsx | 608 +- .../Header/GalleryHeaderComponent.tsx | 162 +- .../Controls/InputTypeahead/InputTypeahead.ts | 372 +- .../InputTypeaheadComponent.test.tsx | 8 +- .../InputTypeaheadComponent.tsx | 18 +- .../InputTypeahead/input-typeahead.html | 38 +- .../JsonEditor/JsonEditorComponent.ts | 346 +- .../JsonEditor/json-editor-component.html | 2 +- .../NotebookTerminalComponent.test.tsx | 316 +- .../Cards/GalleryCardComponent.test.tsx | 4 +- .../Cards/GalleryCardComponent.tsx | 18 +- .../CodeOfConductComponent.test.tsx | 9 +- .../CodeOfConductComponent.tsx | 8 +- .../GalleryAndNotebookViewerComponent.tsx | 228 +- ...lleryAndNotebookViewerComponentAdapter.tsx | 46 +- .../GalleryViewerComponent.test.tsx | 2 +- .../GalleryViewerComponent.tsx | 76 +- .../NotebookMetadataComponent.test.tsx | 8 +- .../NotebookMetadataComponent.tsx | 4 +- .../NotebookViewerComponent.tsx | 23 +- .../QueriesGridComponent.tsx | 584 +- .../QueriesGridComponentAdapter.tsx | 66 +- .../RadioSwitchComponent.tsx | 2 +- .../Settings/SettingsComponent.test.tsx | 34 +- .../Controls/Settings/SettingsComponent.tsx | 86 +- .../Settings/SettingsRenderUtils.test.tsx | 10 +- .../Controls/Settings/SettingsRenderUtils.tsx | 90 +- .../ConflictResolutionComponent.test.tsx | 2 +- .../ConflictResolutionComponent.tsx | 6 +- .../IndexingPolicyComponent.test.tsx | 4 +- .../IndexingPolicyComponent.tsx | 6 +- .../IndexingPolicyRefreshComponent.test.tsx | 2 +- .../IndexingPolicyRefreshComponent.tsx | 4 +- .../AddMongoIndexComponent.test.tsx | 2 +- .../AddMongoIndexComponent.tsx | 8 +- .../MongoIndexingPolicyComponent.test.tsx | 12 +- .../MongoIndexingPolicyComponent.tsx | 20 +- .../ScaleComponent.test.tsx | 16 +- .../SettingsSubComponents/ScaleComponent.tsx | 2 +- .../SubSettingsComponent.test.tsx | 2 +- .../SubSettingsComponent.tsx | 12 +- ...roughputInputAutoPilotV3Component.test.tsx | 4 +- .../ThroughputInputAutoPilotV3Component.tsx | 40 +- .../ToolTipLabelComponent.test.tsx | 2 +- .../Controls/Settings/SettingsUtils.test.tsx | 6 +- .../Controls/Settings/SettingsUtils.tsx | 18 +- src/Explorer/Controls/Settings/TestUtils.tsx | 8 +- .../SmartUi/SmartUiComponent.test.tsx | 38 +- .../Controls/SmartUi/SmartUiComponent.tsx | 48 +- src/Explorer/Controls/Tabs/TabComponent.tsx | 2 +- .../ThroughputInputComponentAutoPilotV3.ts | 6 +- .../ThroughputInputComponentAutoscaleV3.html | 2 +- .../TreeComponent/TreeComponent.test.tsx | 48 +- .../Controls/TreeComponent/TreeComponent.tsx | 22 +- .../ContainerSampleGenerator.test.ts | 26 +- .../DataSamples/ContainerSampleGenerator.ts | 8 +- .../DataSamples/DataSamplesUtil.test.ts | 2 +- src/Explorer/DataSamples/DataSamplesUtil.ts | 6 +- src/Explorer/Explorer.ts | 6143 ++++++++--------- .../D3ForceGraph.test.ts | 314 +- .../GraphExplorerComponent/D3ForceGraph.ts | 2677 ++++--- .../EditorNeighborsComponent.tsx | 6 +- .../EditorNodePropertiesComponent.test.tsx | 36 +- .../EditorNodePropertiesComponent.tsx | 22 +- .../GraphExplorerComponent/GraphData.test.ts | 12 +- .../Graph/GraphExplorerComponent/GraphData.ts | 4 +- .../GraphExplorer.test.tsx | 70 +- .../GraphExplorerComponent/GraphExplorer.tsx | 94 +- .../GraphExplorerComponent/GraphUtil.test.ts | 20 +- .../Graph/GraphExplorerComponent/GraphUtil.ts | 11 +- .../GremlinClient.test.ts | 50 +- .../GraphExplorerComponent/GremlinClient.ts | 6 +- .../GremlinSimpleClient.test.ts | 58 +- .../GremlinSimpleClient.ts | 14 +- .../LeftPaneComponent.tsx | 2 +- .../NodePropertiesComponent.test.tsx | 12 +- .../NodePropertiesComponent.tsx | 39 +- .../QueryContainerComponent.tsx | 4 +- .../ReadOnlyNeighborsComponent.tsx | 2 +- .../ReadOnlyNodePropertiesComponent.test.tsx | 14 +- .../ReadOnlyNodePropertiesComponent.tsx | 6 +- .../GraphStyleComponent/GraphStyle.test.ts | 102 +- .../GraphStyleComponent.ts | 206 +- .../graph-style-component.html | 148 +- .../NewVertexComponent/NewVertex.test.ts | 150 +- .../NewVertexComponent/NewVertexComponent.ts | 198 +- .../CommandBar/CommandBarComponentAdapter.tsx | 4 +- .../CommandBarComponentButtonFactory.test.ts | 44 +- .../CommandBarComponentButtonFactory.ts | 58 +- .../Menus/CommandBar/CommandBarUtil.test.tsx | 2 +- .../Menus/CommandBar/CommandBarUtil.tsx | 50 +- .../Menus/NavBar/ControlBarComponent.tsx | 2 +- .../NotificationConsoleComponent.test.tsx | 24 +- .../NotificationConsoleComponent.tsx | 6 +- .../MostRecentActivity/MostRecentActivity.ts | 6 +- src/Explorer/Notebook/NotebookClientV2.ts | 42 +- .../NotebookComponentAdapter.tsx | 6 +- .../NotebookComponentBootstrapper.tsx | 42 +- .../NotebookContentProvider.ts | 138 +- .../VirtualCommandBarComponent.tsx | 4 +- .../NotebookComponent/__mocks__/rx-jupyter.ts | 4 +- .../Notebook/NotebookComponent/actions.ts | 12 +- .../NotebookComponent/contents/file/index.tsx | 2 +- .../contents/file/text-file.tsx | 10 +- .../NotebookComponent/contents/index.tsx | 18 +- .../Notebook/NotebookComponent/epics.test.ts | 174 +- .../Notebook/NotebookComponent/epics.ts | 122 +- .../NotebookComponent/loadTransform.ts | 10 +- .../Notebook/NotebookComponent/reducers.ts | 2 +- .../Notebook/NotebookComponent/store.ts | 10 +- .../Notebook/NotebookComponent/types.ts | 2 +- .../Notebook/NotebookContainerClient.ts | 12 +- .../Notebook/NotebookContentClient.ts | 20 +- src/Explorer/Notebook/NotebookContentItem.ts | 2 +- src/Explorer/Notebook/NotebookManager.ts | 388 +- .../NotebookReadOnlyRenderer.tsx | 16 +- .../NotebookRenderer/NotebookRenderer.tsx | 22 +- .../Notebook/NotebookRenderer/Prompt.tsx | 8 +- .../NotebookRenderer/StatusBar.test.tsx | 4 +- .../Notebook/NotebookRenderer/StatusBar.tsx | 4 +- .../Notebook/NotebookRenderer/Toolbar.tsx | 40 +- .../decorators/CellCreator.tsx | 6 +- .../decorators/CellLabeler.tsx | 2 +- .../decorators/HoverableCell.tsx | 2 +- .../decorators/draggable/index.tsx | 32 +- .../decorators/hijack-scroll/index.tsx | 6 +- .../decorators/kbd-shortcuts/index.tsx | 8 +- src/Explorer/Notebook/NotebookUtil.test.ts | 274 +- src/Explorer/Notebook/NotebookUtil.ts | 6 +- .../Notebook/notebookClientV2.test.ts | 12 +- src/Explorer/OpenActions.test.ts | 74 +- src/Explorer/OpenActions.ts | 374 +- src/Explorer/Panes/AddCollectionPane.html | 795 ++- src/Explorer/Panes/AddCollectionPane.test.ts | 8 +- src/Explorer/Panes/AddCollectionPane.ts | 48 +- src/Explorer/Panes/AddDatabasePane.html | 167 +- src/Explorer/Panes/AddDatabasePane.test.ts | 8 +- src/Explorer/Panes/AddDatabasePane.ts | 28 +- src/Explorer/Panes/BrowseQueriesPane.html | 66 +- src/Explorer/Panes/BrowseQueriesPane.ts | 216 +- .../Panes/CassandraAddCollectionPane.html | 10 +- .../Panes/CassandraAddCollectionPane.ts | 32 +- src/Explorer/Panes/ContextualPaneBase.ts | 272 +- src/Explorer/Panes/CopyNotebookPane.tsx | 388 +- .../Panes/CopyNotebookPaneComponent.tsx | 22 +- .../DeleteCollectionConfirmationPane.html | 216 +- .../DeleteCollectionConfirmationPane.test.ts | 12 +- .../Panes/DeleteCollectionConfirmationPane.ts | 294 +- .../Panes/DeleteDatabaseConfirmationPane.html | 218 +- .../DeleteDatabaseConfirmationPane.test.ts | 10 +- .../Panes/DeleteDatabaseConfirmationPane.ts | 318 +- .../Panes/ExecuteSprocParamsPane.html | 340 +- src/Explorer/Panes/ExecuteSprocParamsPane.ts | 344 +- .../Panes/GenericRightPaneComponent.tsx | 2 +- src/Explorer/Panes/GitHubReposPane.html | 28 +- src/Explorer/Panes/GitHubReposPane.ts | 706 +- src/Explorer/Panes/GraphNewVertexPane.html | 120 +- src/Explorer/Panes/GraphStylingPane.html | 118 +- src/Explorer/Panes/GraphStylingPane.ts | 136 +- src/Explorer/Panes/LoadQueryPane.html | 176 +- src/Explorer/Panes/LoadQueryPane.ts | 298 +- src/Explorer/Panes/NewVertexPane.ts | 130 +- src/Explorer/Panes/PaneComponents.ts | 452 +- .../Panes/PublishNotebookPaneAdapter.tsx | 424 +- .../PublishNotebookPaneComponent.test.tsx | 2 +- .../Panes/PublishNotebookPaneComponent.tsx | 28 +- src/Explorer/Panes/RenewAdHocAccessPane.html | 180 +- src/Explorer/Panes/RenewAdHocAccessPane.ts | 202 +- src/Explorer/Panes/SaveQueryPane.html | 126 +- src/Explorer/Panes/SaveQueryPane.ts | 330 +- src/Explorer/Panes/SettingsPane.html | 2 +- src/Explorer/Panes/SettingsPane.test.ts | 76 +- src/Explorer/Panes/SetupNotebooksPane.html | 90 +- src/Explorer/Panes/SetupNotebooksPane.ts | 226 +- src/Explorer/Panes/StringInputPane.html | 2 +- src/Explorer/Panes/StringInputPane.ts | 2 +- src/Explorer/Panes/SwitchDirectoryPane.ts | 4 +- .../Panes/Tables/AddTableEntityPane.ts | 302 +- .../Panes/Tables/EditTableEntityPane.ts | 458 +- .../Panes/Tables/EntityPropertyViewModel.ts | 328 +- src/Explorer/Panes/Tables/QuerySelectPane.ts | 348 +- .../Panes/Tables/TableAddEntityPane.html | 381 +- .../Panes/Tables/TableColumnOptionsPane.html | 156 +- .../Panes/Tables/TableColumnOptionsPane.ts | 390 +- .../Panes/Tables/TableEditEntityPane.html | 375 +- src/Explorer/Panes/Tables/TableEntityPane.ts | 562 +- .../Panes/Tables/TableQuerySelectPane.html | 158 +- .../Validators/EntityPropertyNameValidator.ts | 612 +- .../EntityPropertyValidationCommon.ts | 46 +- .../EntityPropertyValueValidator.ts | 682 +- src/Explorer/Panes/UploadFilePane.html | 2 +- src/Explorer/Panes/UploadFilePane.ts | 6 +- src/Explorer/Panes/UploadItemsPane.html | 260 +- src/Explorer/Panes/UploadItemsPane.ts | 290 +- src/Explorer/Panes/UploadItemsPaneAdapter.tsx | 6 +- .../SplashScreenComponentAdapter.test.ts | 4 +- .../SplashScreenComponentApdapter.tsx | 34 +- src/Explorer/Tables/Constants.ts | 342 +- src/Explorer/Tables/DataTable/CacheBase.ts | 52 +- .../DataTable/DataTableBindingManager.ts | 807 ++- .../Tables/DataTable/DataTableBuilder.ts | 104 +- .../DataTable/DataTableOperationManager.ts | 600 +- .../Tables/DataTable/DataTableOperations.ts | 384 +- .../Tables/DataTable/DataTableUtilities.ts | 296 +- .../Tables/DataTable/DataTableViewModel.ts | 540 +- .../Tables/DataTable/TableCommands.ts | 312 +- .../Tables/DataTable/TableEntityCache.ts | 54 +- .../DataTable/TableEntityListViewModel.ts | 1204 ++-- src/Explorer/Tables/Entities.ts | 76 +- .../Tables/QueryBuilder/ClauseGroup.ts | 654 +- .../QueryBuilder/ClauseGroupViewModel.ts | 98 +- .../QueryBuilder/CustomTimestampHelper.ts | 754 +- .../Tables/QueryBuilder/DateTimeUtilities.ts | 134 +- .../QueryBuilder/QueryBuilderViewModel.ts | 1594 ++--- .../QueryBuilder/QueryClauseViewModel.ts | 570 +- .../Tables/QueryBuilder/QueryViewModel.ts | 474 +- src/Explorer/Tables/TableDataClient.ts | 1264 ++-- src/Explorer/Tables/TableEntityProcessor.ts | 388 +- src/Explorer/Tables/Utilities.ts | 558 +- src/Explorer/Tabs/ConflictsTab.ts | 49 +- src/Explorer/Tabs/DatabaseSettingsTab.html | 6 +- src/Explorer/Tabs/DatabaseSettingsTab.ts | 16 +- src/Explorer/Tabs/DocumentsTab.html | 447 +- src/Explorer/Tabs/DocumentsTab.test.ts | 336 +- src/Explorer/Tabs/DocumentsTab.ts | 1921 +++--- src/Explorer/Tabs/GalleryTab.tsx | 2 +- src/Explorer/Tabs/GraphTab.ts | 482 +- src/Explorer/Tabs/MongoDocumentsTab.html | 843 +-- src/Explorer/Tabs/MongoDocumentsTab.ts | 28 +- src/Explorer/Tabs/MongoQueryTab.html | 240 +- src/Explorer/Tabs/MongoShellTab.html | 30 +- src/Explorer/Tabs/MongoShellTab.ts | 430 +- src/Explorer/Tabs/NotebookV2Tab.ts | 76 +- src/Explorer/Tabs/NotebookViewerTab.tsx | 4 +- src/Explorer/Tabs/QueryTab.html | 684 +- src/Explorer/Tabs/QueryTab.test.ts | 172 +- src/Explorer/Tabs/QueryTab.ts | 1212 ++-- src/Explorer/Tabs/QueryTablesTab.html | 527 +- src/Explorer/Tabs/QueryTablesTab.ts | 556 +- src/Explorer/Tabs/ScriptTabBase.ts | 32 +- src/Explorer/Tabs/SettingsTabV2.tsx | 6 +- src/Explorer/Tabs/SparkMasterTab.html | 14 +- src/Explorer/Tabs/SparkMasterTab.ts | 70 +- src/Explorer/Tabs/StoredProcedureTab.html | 178 +- src/Explorer/Tabs/StoredProcedureTab.ts | 592 +- src/Explorer/Tabs/TabComponents.ts | 392 +- src/Explorer/Tabs/TabsBase.ts | 420 +- src/Explorer/Tabs/TabsManager.test.ts | 14 +- src/Explorer/Tabs/TabsManager.ts | 2 +- src/Explorer/Tabs/TerminalTab.tsx | 2 +- src/Explorer/Tabs/TriggerTab.html | 78 +- src/Explorer/Tabs/TriggerTab.ts | 22 +- src/Explorer/Tabs/UserDefinedFunctionTab.html | 60 +- src/Explorer/Tabs/UserDefinedFunctionTab.ts | 18 +- src/Explorer/Tree/AccessibleVerticalList.ts | 246 +- src/Explorer/Tree/Collection.test.ts | 224 +- src/Explorer/Tree/Collection.ts | 169 +- src/Explorer/Tree/ConflictId.ts | 2 +- src/Explorer/Tree/Database.test.ts | 8 +- src/Explorer/Tree/Database.ts | 714 +- src/Explorer/Tree/DocumentId.ts | 142 +- src/Explorer/Tree/ObjectId.ts | 28 +- src/Explorer/Tree/ResourceTokenCollection.ts | 14 +- src/Explorer/Tree/ResourceTreeAdapter.test.ts | 26 +- .../Tree/ResourceTreeAdapter.test.tsx | 78 +- src/Explorer/Tree/ResourceTreeAdapter.tsx | 123 +- ...sourceTreeAdapterForResourceToken.test.tsx | 2 +- .../ResourceTreeAdapterForResourceToken.tsx | 14 +- src/Explorer/Tree/StoredProcedure.ts | 350 +- src/Explorer/Tree/Trigger.ts | 246 +- src/Explorer/Tree/UserDefinedFunction.ts | 232 +- src/Explorer/WaitsForTemplateViewModel.ts | 74 +- src/GalleryViewer/GalleryViewer.tsx | 4 +- src/GalleryViewer/galleryViewer.html | 2 +- src/GitHub/GitHubClient.ts | 84 +- src/GitHub/GitHubConnector.ts | 60 +- src/GitHub/GitHubContentProvider.test.ts | 604 +- src/GitHub/GitHubContentProvider.ts | 862 +-- src/GitHub/GitHubOAuthService.test.ts | 332 +- src/GitHub/GitHubOAuthService.ts | 250 +- src/HostedExplorer.tsx | 286 +- src/Index.ts | 46 +- src/Juno/JunoClient.test.ts | 772 +-- src/Juno/JunoClient.ts | 1056 +-- src/Main.tsx | 1102 +-- src/NotebookViewer/NotebookViewer.tsx | 4 +- .../NotebookWorkspaceManager.ts | 196 +- ...ookWorkspaceResourceProviderMockClients.ts | 100 +- src/Platform/Emulator/emulatorAccount.tsx | 6 +- src/Platform/Hosted/Authorization.ts | 4 +- .../Components/AccountSwitcher.test.tsx | 2 +- .../Hosted/Components/AccountSwitcher.tsx | 26 +- .../Hosted/Components/ConnectExplorer.tsx | 6 +- .../Components/DirectoryPickerPanel.test.tsx | 2 +- .../Components/DirectoryPickerPanel.tsx | 4 +- src/Platform/Hosted/Components/MeControl.tsx | 14 +- .../Hosted/Components/SignInButton.tsx | 2 +- .../Hosted/Components/SwitchAccount.tsx | 8 +- .../Hosted/Components/SwitchSubscription.tsx | 8 +- .../Helpers/ConnectionStringParser.test.ts | 146 +- .../Hosted/Helpers/ConnectionStringParser.ts | 96 +- .../Hosted/Helpers/ResourceTokenUtils.test.ts | 4 +- .../Hosted/Helpers/ResourceTokenUtils.ts | 2 +- src/Platform/Hosted/HostedUtils.test.ts | 8 +- src/Platform/Hosted/HostedUtils.ts | 8 +- src/Platform/Hosted/extractFeatures.test.ts | 2 +- src/ReactDevTools.ts | 6 +- .../ResourceProviderClient.ts | 434 +- .../ResourceProviderClientFactory.ts | 44 +- src/RouteHandlers/RouteHandler.ts | 70 +- src/RouteHandlers/TabRouteHandler.test.ts | 204 +- src/RouteHandlers/TabRouteHandler.ts | 836 +-- src/SelfServe/ClassDecorators.tsx | 4 +- src/SelfServe/Example/SelfServeExample.tsx | 16 +- src/SelfServe/SelfServeComponent.test.tsx | 32 +- src/SelfServe/SelfServeComponent.tsx | 4 +- src/SelfServe/SelfServeUtils.test.tsx | 80 +- src/SelfServe/SelfServeUtils.tsx | 10 +- src/Shared/AddCollectionUtility.test.ts | 14 +- src/Shared/AddCollectionUtility.ts | 46 +- src/Shared/Constants.ts | 478 +- src/Shared/DefaultExperienceUtility.test.ts | 18 +- src/Shared/DefaultExperienceUtility.ts | 2 +- src/Shared/PriceEstimateCalculator.ts | 86 +- src/Shared/StorageUtility.ts | 2 +- src/Shared/Telemetry/TelemetryConstants.ts | 272 +- src/Shared/Telemetry/TelemetryProcessor.ts | 30 +- src/Shared/appInsights.ts | 4 +- .../ArcadiaResourceManager.ts | 156 +- src/Terminal/JupyterLabAppFactory.ts | 2 +- src/Terminal/NotebookAppContracts.d.ts | 6 +- src/Terminal/index.ts | 10 +- src/TokenProviders/PortalTokenProvider.ts | 26 +- src/TokenProviders/TokenProviderFactory.ts | 40 +- src/Utils/AuthorizationUtils.test.ts | 210 +- src/Utils/AuthorizationUtils.ts | 112 +- src/Utils/Base64Utils.test.ts | 22 +- src/Utils/Base64Utils.ts | 14 +- src/Utils/BlobUtils.ts | 34 +- src/Utils/GalleryUtils.test.ts | 222 +- src/Utils/GalleryUtils.ts | 688 +- src/Utils/GitHubUtils.test.ts | 64 +- src/Utils/GitHubUtils.ts | 124 +- src/Utils/JunoUtils.test.ts | 90 +- src/Utils/JunoUtils.ts | 4 +- src/Utils/NotebookConfigurationUtils.ts | 184 +- src/Utils/NotificationConsoleUtils.ts | 164 +- src/Utils/PricingUtils.test.ts | 28 +- src/Utils/PricingUtils.ts | 13 +- src/Utils/QueryUtils.test.ts | 374 +- src/Utils/QueryUtils.ts | 236 +- src/Utils/StringUtils.test.ts | 60 +- src/Utils/StringUtils.ts | 42 +- src/Utils/UserUtils.ts | 16 +- src/Utils/WindowUtils.test.ts | 78 +- src/Utils/WindowUtils.ts | 36 +- src/Utils/arm/request.test.ts | 12 +- src/Utils/arm/request.ts | 10 +- src/connectToGitHub.html | 28 +- src/explorer.html | 24 +- src/global.d.ts | 26 +- src/hooks/useAADAuth.ts | 18 +- src/hooks/useDirectories.tsx | 2 +- src/hooks/useGraphPhoto.tsx | 6 +- src/hooks/usePortalAccessToken.tsx | 10 +- src/hooks/useSubscriptions.tsx | 2 +- src/hostedExplorer.html | 24 +- src/index.html | 120 +- src/koComment.tsx | 40 +- src/quickstart.html | 706 +- src/quickstart.ts | 10 +- src/workers/upload/index.ts | 16 +- test/cassandra/container.spec.ts | 12 +- test/mongo/container.spec.ts | 10 +- test/notebooks/notebookTestUtils.ts | 2 +- test/notebooks/uploadAndOpenNotebook.spec.ts | 2 +- test/sql/container.spec.ts | 12 +- test/sql/resourceToken.spec.ts | 4 +- test/tables/container.spec.ts | 8 +- test/testExplorer/TestExplorer.ts | 8 +- test/testExplorer/TestExplorerParams.ts | 2 +- webpack.config.js | 108 +- 500 files changed, 41927 insertions(+), 41838 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ae05c511a..c1b67a931 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,36 +1,36 @@ module.exports = { env: { browser: true, - es6: true + es6: true, }, plugins: ["@typescript-eslint", "no-null", "prefer-arrow"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], globals: { Atomics: "readonly", - SharedArrayBuffer: "readonly" + SharedArrayBuffer: "readonly", }, parser: "@typescript-eslint/parser", parserOptions: { ecmaFeatures: { - jsx: true + jsx: true, }, ecmaVersion: 2018, - sourceType: "module" + sourceType: "module", }, overrides: [ { files: ["**/*.tsx"], extends: ["plugin:react/recommended"], // TODO: Add react-hooks - plugins: ["react"] + plugins: ["react"], }, { files: ["**/*.{test,spec}.{ts,tsx}"], env: { - jest: true + jest: true, }, extends: ["plugin:jest/recommended"], - plugins: ["jest"] - } + plugins: ["jest"], + }, ], rules: { "no-console": ["error", { allow: ["error", "warn", "dir"] }], @@ -46,8 +46,8 @@ module.exports = { "error", { selector: "CallExpression[callee.object.name='JSON'][callee.property.name='stringify'] Identifier[name=/$err/]", - message: "Do not use JSON.stringify(error). It will print '{}'" - } - ] - } + message: "Do not use JSON.stringify(error). It will print '{}'", + }, + ], + }, }; diff --git a/babel.config.js b/babel.config.js index 54700a3aa..52141ce97 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,4 @@ module.exports = { presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"], - plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]] + plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]], }; diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index b0db3f8ea..bc0020308 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -6,6 +6,6 @@ module.exports = { slowMo: 55, defaultViewport: null, ignoreHTTPSErrors: true, - args: ["--disable-web-security"] - } + args: ["--disable-web-security"], + }, }; diff --git a/jest.config.e2e.js b/jest.config.e2e.js index eadd56114..38970f58e 100644 --- a/jest.config.e2e.js +++ b/jest.config.e2e.js @@ -1,5 +1,5 @@ module.exports = { preset: "jest-puppeteer", testMatch: ["/test/**/*.spec.[jt]s?(x)"], - setupFiles: ["dotenv/config"] + setupFiles: ["dotenv/config"], }; diff --git a/jest.config.js b/jest.config.js index 3cceb40a3..5151e71c7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -42,8 +42,8 @@ module.exports = { branches: 22, functions: 28, lines: 33, - statements: 31 - } + statements: 31, + }, }, // Make calling deprecated APIs throw helpful error messages @@ -76,7 +76,7 @@ module.exports = { "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes "^dnd-core$": "dnd-core/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs", - "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs" + "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader @@ -164,11 +164,11 @@ module.exports = { // A map from regular expressions to paths to transformers transform: { "^.+\\.html?$": "html-loader-jest", - "^.+\\.[t|j]sx?$": "babel-jest" + "^.+\\.[t|j]sx?$": "babel-jest", }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - transformIgnorePatterns: ["/node_modules/", "/externals/"] + transformIgnorePatterns: ["/node_modules/", "/externals/"], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/package-lock.json b/package-lock.json index 275820720..443080bf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17647,9 +17647,9 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", "dev": true }, "pretty-error": { diff --git a/package.json b/package.json index d9f1efa93..109966b99 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ "mini-css-extract-plugin": "0.4.3", "monaco-editor-webpack-plugin": "1.7.0", "node-fetch": "2.6.1", - "prettier": "1.19.1", + "prettier": "2.2.1", "puppeteer": "4.0.0", "raw-loader": "0.5.1", "rimraf": "3.0.0", diff --git a/src/AuthType.ts b/src/AuthType.ts index 9cc33f789..a8a968f34 100644 --- a/src/AuthType.ts +++ b/src/AuthType.ts @@ -1,7 +1,7 @@ -export enum AuthType { - AAD = "aad", - EncryptedToken = "encryptedtoken", - MasterKey = "masterkey", - ResourceToken = "resourcetoken", - ConnectionString = "connectionstring" -} +export enum AuthType { + AAD = "aad", + EncryptedToken = "encryptedtoken", + MasterKey = "masterkey", + ResourceToken = "resourcetoken", + ConnectionString = "connectionstring", +} diff --git a/src/Bindings/BindingHandlersRegisterer.ts b/src/Bindings/BindingHandlersRegisterer.ts index 55f329bec..45afc6732 100644 --- a/src/Bindings/BindingHandlersRegisterer.ts +++ b/src/Bindings/BindingHandlersRegisterer.ts @@ -1,22 +1,22 @@ -import * as ko from "knockout"; -import * as ReactBindingHandler from "./ReactBindingHandler"; -import "../Explorer/Tables/DataTable/DataTableBindingManager"; - -export class BindingHandlersRegisterer { - public static registerBindingHandlers() { - ko.bindingHandlers.setTemplateReady = { - init( - element: any, - wrappedValueAccessor: () => any, - allBindings?: ko.AllBindings, - viewModel?: any, - bindingContext?: ko.BindingContext - ) { - const value = ko.unwrap(wrappedValueAccessor()); - bindingContext?.$data.isTemplateReady(value); - } - } as ko.BindingHandler; - - ReactBindingHandler.Registerer.register(); - } -} +import * as ko from "knockout"; +import * as ReactBindingHandler from "./ReactBindingHandler"; +import "../Explorer/Tables/DataTable/DataTableBindingManager"; + +export class BindingHandlersRegisterer { + public static registerBindingHandlers() { + ko.bindingHandlers.setTemplateReady = { + init( + element: any, + wrappedValueAccessor: () => any, + allBindings?: ko.AllBindings, + viewModel?: any, + bindingContext?: ko.BindingContext + ) { + const value = ko.unwrap(wrappedValueAccessor()); + bindingContext?.$data.isTemplateReady(value); + }, + } as ko.BindingHandler; + + ReactBindingHandler.Registerer.register(); + } +} diff --git a/src/Bindings/ReactBindingHandler.ts b/src/Bindings/ReactBindingHandler.ts index 8ecb51eee..bbd6c5b76 100644 --- a/src/Bindings/ReactBindingHandler.ts +++ b/src/Bindings/ReactBindingHandler.ts @@ -42,7 +42,7 @@ export class Registerer { // Initial rendering at mount point ReactDOM.render(adapter.renderComponent(), element); - } + }, } as ko.BindingHandler; } } diff --git a/src/Common/ArrayHashMap.ts b/src/Common/ArrayHashMap.ts index e19735939..eef791373 100644 --- a/src/Common/ArrayHashMap.ts +++ b/src/Common/ArrayHashMap.ts @@ -40,7 +40,7 @@ export class ArrayHashMap { public forEach(key: string, iteratorFct: (value: T) => void) { const values = this.store.get(key); if (values) { - values.forEach(value => iteratorFct(value)); + values.forEach((value) => iteratorFct(value)); } } diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 30d693903..9f7214d76 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -7,7 +7,7 @@ export class CodeOfConductEndpoints { export class EndpointsRegex { public static readonly cassandra = [ "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com", - "HostName=(.*).cassandra.cosmos.azure.com" + "HostName=(.*).cassandra.cosmos.azure.com", ]; public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com"; public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com"; @@ -147,7 +147,7 @@ export class MongoDBAccounts { export enum MongoBackendEndpointType { local, - remote + remote, } // TODO: 435619 Add default endpoints per cloud and use regional only when available @@ -274,7 +274,7 @@ export class HttpStatusCodes { HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list HttpStatusCodes.BadGateway, HttpStatusCodes.ServiceUnavailable, - HttpStatusCodes.GatewayTimeout + HttpStatusCodes.GatewayTimeout, ]; } @@ -330,10 +330,7 @@ export class HashRoutePrefixes { public static docsWithIds(databaseId: string, collectionId: string, docId: string) { const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId); - return transformedDatabasePrefix - .replace("{coll_id}", collectionId) - .replace("{doc_id}", docId) - .replace("/", ""); // strip the first slash since hasher adds it + return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("{doc_id}", docId).replace("/", ""); // strip the first slash since hasher adds it } } @@ -379,7 +376,7 @@ export class OfferVersions { export enum ConflictOperationType { Replace = "replace", Create = "create", - Delete = "delete" + Delete = "delete", } export const EmulatorMasterKey = diff --git a/src/Common/CosmosClient.test.ts b/src/Common/CosmosClient.test.ts index 08aaee386..ea91efa53 100644 --- a/src/Common/CosmosClient.test.ts +++ b/src/Common/CosmosClient.test.ts @@ -10,17 +10,17 @@ describe("tokenProvider", () => { resourceId: "", resourceType: "dbs" as ResourceType, headers: {}, - getAuthorizationTokenUsingMasterKey: () => "" + getAuthorizationTokenUsingMasterKey: () => "", }; beforeEach(() => { updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); window.fetch = jest.fn().mockImplementation(() => { return { json: () => "{}", - headers: new Map() + headers: new Map(), }; }); }); @@ -36,7 +36,7 @@ describe("tokenProvider", () => { it("does not call the auth service if a master key is set", async () => { updateUserContext({ - masterKey: "foo" + masterKey: "foo", }); await tokenProvider(options); expect((window.fetch as any).mock.calls.length).toBe(0); @@ -50,7 +50,7 @@ describe("getTokenFromAuthService", () => { window.fetch = jest.fn().mockImplementation(() => { return { json: () => "{}", - headers: new Map() + headers: new Map(), }; }); }); @@ -61,7 +61,7 @@ describe("getTokenFromAuthService", () => { it("builds the correct URL in production", () => { updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); getTokenFromAuthService("GET", "dbs", "foo"); expect(window.fetch).toHaveBeenCalledWith( @@ -72,7 +72,7 @@ describe("getTokenFromAuthService", () => { it("builds the correct URL in dev", () => { updateConfigContext({ - BACKEND_ENDPOINT: "https://localhost:1234" + BACKEND_ENDPOINT: "https://localhost:1234", }); getTokenFromAuthService("GET", "dbs", "foo"); expect(window.fetch).toHaveBeenCalledWith( @@ -96,15 +96,15 @@ describe("endpoint", () => { documentEndpoint: "bar", gremlinEndpoint: "foo", tableEndpoint: "foo", - cassandraEndpoint: "foo" - } - } + cassandraEndpoint: "foo", + }, + }, }); expect(endpoint()).toEqual("bar"); }); it("uses _endpoint if set", () => { updateUserContext({ - endpoint: "baz" + endpoint: "baz", }); expect(endpoint()).toEqual("baz"); }); @@ -121,7 +121,7 @@ describe("requestPlugin", () => { updateConfigContext({ platform: Platform.Hosted, BACKEND_ENDPOINT: "https://localhost:1234", - PROXY_PATH: "/proxy" + PROXY_PATH: "/proxy", }); const headers = {}; const endpoint = "https://docs.azure.com"; diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 95eaf552d..7c23b3388 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -58,13 +58,13 @@ export async function getTokenFromAuthService(verb: string, resourceType: string method: "POST", headers: { "content-type": "application/json", - "x-ms-encrypted-auth-token": userContext.accessToken + "x-ms-encrypted-auth-token": userContext.accessToken, }, body: JSON.stringify({ verb, resourceType, - resourceId - }) + resourceId, + }), }); //TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json() const result = JSON.parse(await response.json()); @@ -81,9 +81,9 @@ export function client(): Cosmos.CosmosClient { key: userContext.masterKey, tokenProvider, connectionPolicy: { - enableEndpointDiscovery: false + enableEndpointDiscovery: false, }, - userAgentSuffix: "Azure Portal" + userAgentSuffix: "Azure Portal", }; if (configContext.PROXY_PATH !== undefined) { diff --git a/src/Common/EditableUtility.ts b/src/Common/EditableUtility.ts index 5956060de..fc908dca4 100644 --- a/src/Common/EditableUtility.ts +++ b/src/Common/EditableUtility.ts @@ -1,94 +1,94 @@ -import * as ko from "knockout"; -import * as ViewModels from "../Contracts/ViewModels"; - -export default class EditableUtility { - public static observable(initialValue?: T): ViewModels.Editable { - var observable: ViewModels.Editable = >ko.observable(initialValue); - - observable.edits = ko.observableArray([initialValue]); - observable.validations = ko.observableArray<(value: T) => boolean>([]); - - observable.setBaseline = (baseline: T) => { - observable(baseline); - observable.edits([baseline]); - }; - - observable.getEditableCurrentValue = ko.computed(() => { - const edits = (observable.edits && observable.edits()) || []; - if (edits.length === 0) { - return undefined; - } - - return edits[edits.length - 1]; - }); - - observable.getEditableOriginalValue = ko.computed(() => { - const edits = (observable.edits && observable.edits()) || []; - if (edits.length === 0) { - return undefined; - } - - return edits[0]; - }); - - observable.editableIsDirty = ko.computed(() => { - const edits = (observable.edits && observable.edits()) || []; - if (edits.length <= 1) { - return false; - } - - let current: any = observable.getEditableCurrentValue(); - let original: any = observable.getEditableOriginalValue(); - - switch (typeof current) { - case "string": - case "undefined": - case "number": - case "boolean": - current = current && current.toString(); - break; - - default: - current = JSON.stringify(current); - break; - } - - switch (typeof original) { - case "string": - case "undefined": - case "number": - case "boolean": - original = original && original.toString(); - break; - - default: - original = JSON.stringify(original); - break; - } - - if (current !== original) { - return true; - } - - return false; - }); - - observable.subscribe(edit => { - var edits = observable.edits && observable.edits(); - if (!edits) { - return; - } - edits.push(edit); - observable.edits(edits); - }); - - observable.editableIsValid = ko.observable(true); - observable.subscribe(value => { - const validations: ((value: T) => boolean)[] = (observable.validations && observable.validations()) || []; - const isValid = validations.every(validate => validate(value)); - observable.editableIsValid(isValid); - }); - - return observable; - } -} +import * as ko from "knockout"; +import * as ViewModels from "../Contracts/ViewModels"; + +export default class EditableUtility { + public static observable(initialValue?: T): ViewModels.Editable { + var observable: ViewModels.Editable = >ko.observable(initialValue); + + observable.edits = ko.observableArray([initialValue]); + observable.validations = ko.observableArray<(value: T) => boolean>([]); + + observable.setBaseline = (baseline: T) => { + observable(baseline); + observable.edits([baseline]); + }; + + observable.getEditableCurrentValue = ko.computed(() => { + const edits = (observable.edits && observable.edits()) || []; + if (edits.length === 0) { + return undefined; + } + + return edits[edits.length - 1]; + }); + + observable.getEditableOriginalValue = ko.computed(() => { + const edits = (observable.edits && observable.edits()) || []; + if (edits.length === 0) { + return undefined; + } + + return edits[0]; + }); + + observable.editableIsDirty = ko.computed(() => { + const edits = (observable.edits && observable.edits()) || []; + if (edits.length <= 1) { + return false; + } + + let current: any = observable.getEditableCurrentValue(); + let original: any = observable.getEditableOriginalValue(); + + switch (typeof current) { + case "string": + case "undefined": + case "number": + case "boolean": + current = current && current.toString(); + break; + + default: + current = JSON.stringify(current); + break; + } + + switch (typeof original) { + case "string": + case "undefined": + case "number": + case "boolean": + original = original && original.toString(); + break; + + default: + original = JSON.stringify(original); + break; + } + + if (current !== original) { + return true; + } + + return false; + }); + + observable.subscribe((edit) => { + var edits = observable.edits && observable.edits(); + if (!edits) { + return; + } + edits.push(edit); + observable.edits(edits); + }); + + observable.editableIsValid = ko.observable(true); + observable.subscribe((value) => { + const validations: ((value: T) => boolean)[] = (observable.validations && observable.validations()) || []; + const isValid = validations.every((validate) => validate(value)); + observable.editableIsValid(isValid); + }); + + return observable; + } +} diff --git a/src/Common/EnvironmentUtility.ts b/src/Common/EnvironmentUtility.ts index 0a4516514..b133cd8b3 100644 --- a/src/Common/EnvironmentUtility.ts +++ b/src/Common/EnvironmentUtility.ts @@ -1,6 +1,6 @@ -export function normalizeArmEndpoint(uri: string): string { - if (uri && uri.slice(-1) !== "/") { - return `${uri}/`; - } - return uri; -} +export function normalizeArmEndpoint(uri: string): string { + if (uri && uri.slice(-1) !== "/") { + return `${uri}/`; + } + return uri; +} diff --git a/src/Common/ErrorHandlingUtils.ts b/src/Common/ErrorHandlingUtils.ts index 440b07b25..c303e1f74 100644 --- a/src/Common/ErrorHandlingUtils.ts +++ b/src/Common/ErrorHandlingUtils.ts @@ -37,7 +37,7 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri } sendMessage({ type: MessageTypes.ForbiddenError, - reason: errorMessage + reason: errorMessage, }); } }; diff --git a/src/Common/HeadersUtility.test.ts b/src/Common/HeadersUtility.test.ts index aa83b36c4..5f46c420f 100644 --- a/src/Common/HeadersUtility.test.ts +++ b/src/Common/HeadersUtility.test.ts @@ -1,25 +1,25 @@ -import * as HeadersUtility from "./HeadersUtility"; -import { ExplorerSettings } from "../Shared/ExplorerSettings"; -import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; - -describe("Headers Utility", () => { - describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => { - beforeEach(() => { - ExplorerSettings.createDefaultSettings(); - }); - - it("should return true by default", () => { - expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true); - }); - - it("should return false if the enable cross partition key feed option is false", () => { - LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "false"); - expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(false); - }); - - it("should return true if the enable cross partition key feed option is true", () => { - LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); - expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true); - }); - }); -}); +import * as HeadersUtility from "./HeadersUtility"; +import { ExplorerSettings } from "../Shared/ExplorerSettings"; +import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; + +describe("Headers Utility", () => { + describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => { + beforeEach(() => { + ExplorerSettings.createDefaultSettings(); + }); + + it("should return true by default", () => { + expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true); + }); + + it("should return false if the enable cross partition key feed option is false", () => { + LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "false"); + expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(false); + }); + + it("should return true if the enable cross partition key feed option is true", () => { + LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); + expect(HeadersUtility.shouldEnableCrossPartitionKey()).toBe(true); + }); + }); +}); diff --git a/src/Common/HeadersUtility.ts b/src/Common/HeadersUtility.ts index 8280b28e3..0579c1776 100644 --- a/src/Common/HeadersUtility.ts +++ b/src/Common/HeadersUtility.ts @@ -1,28 +1,28 @@ -import * as Constants from "./Constants"; - -import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; - -// x-ms-resource-quota: databases = 100; collections = 5000; users = 500000; permissions = 2000000; -export function getQuota(responseHeaders: any): any { - return responseHeaders && responseHeaders[Constants.HttpHeaders.resourceQuota] - ? parseStringIntoObject(responseHeaders[Constants.HttpHeaders.resourceQuota]) - : null; -} - -export function shouldEnableCrossPartitionKey(): boolean { - return LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"; -} - -function parseStringIntoObject(resourceString: string) { - var entityObject: any = {}; - - if (resourceString) { - var entitiesArray: string[] = resourceString.split(";"); - for (var i: any = 0; i < entitiesArray.length; i++) { - var entity: string[] = entitiesArray[i].split("="); - entityObject[entity[0]] = entity[1]; - } - } - - return entityObject; -} +import * as Constants from "./Constants"; + +import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; + +// x-ms-resource-quota: databases = 100; collections = 5000; users = 500000; permissions = 2000000; +export function getQuota(responseHeaders: any): any { + return responseHeaders && responseHeaders[Constants.HttpHeaders.resourceQuota] + ? parseStringIntoObject(responseHeaders[Constants.HttpHeaders.resourceQuota]) + : null; +} + +export function shouldEnableCrossPartitionKey(): boolean { + return LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"; +} + +function parseStringIntoObject(resourceString: string) { + var entityObject: any = {}; + + if (resourceString) { + var entitiesArray: string[] = resourceString.split(";"); + for (var i: any = 0; i < entitiesArray.length; i++) { + var entity: string[] = entitiesArray[i].split("="); + entityObject[entity[0]] = entity[1]; + } + } + + return entityObject; +} diff --git a/src/Common/IteratorUtilities.test.ts b/src/Common/IteratorUtilities.test.ts index e6cc23a12..e036a9d70 100644 --- a/src/Common/IteratorUtilities.test.ts +++ b/src/Common/IteratorUtilities.test.ts @@ -11,8 +11,8 @@ describe("nextPage", () => { queryMetrics: {}, requestCharge: 1, headers: {}, - activityId: "foo" - }) + activityId: "foo", + }), }; expect(await nextPage(fakeIterator, 10)).toMatchSnapshot(); diff --git a/src/Common/IteratorUtilities.ts b/src/Common/IteratorUtilities.ts index 0318c1ae3..62249c1ed 100644 --- a/src/Common/IteratorUtilities.ts +++ b/src/Common/IteratorUtilities.ts @@ -14,7 +14,7 @@ export interface MinimalQueryIterator { // Pick, "fetchNext">; export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise { - return documentsIterator.fetchNext().then(response => { + return documentsIterator.fetchNext().then((response) => { const documents = response.resources; const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const itemCount = (documents && documents.length) || 0; @@ -26,7 +26,7 @@ export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex lastItemIndex: Number(firstItemIndex) + Number(itemCount), headers, activityId: response.activityId, - requestCharge: response.requestCharge + requestCharge: response.requestCharge, }; }); } diff --git a/src/Common/Logger.test.ts b/src/Common/Logger.test.ts index 44a8c79fb..d2d873c9b 100644 --- a/src/Common/Logger.test.ts +++ b/src/Common/Logger.test.ts @@ -1,26 +1,26 @@ -jest.mock("./MessageHandler"); -import { LogEntryLevel } from "../Contracts/Diagnostics"; -import * as Logger from "./Logger"; -import { MessageTypes } from "../Contracts/ExplorerContracts"; -import { sendMessage } from "./MessageHandler"; - -describe("Logger", () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - it("should log info messages", () => { - Logger.logInfo("Test info", "DocDB"); - expect(sendMessage).toBeCalled(); - }); - - it("should log error messages", () => { - Logger.logError("Test error", "DocDB"); - expect(sendMessage).toBeCalled(); - }); - - it("should log warnings", () => { - Logger.logWarning("Test warning", "DocDB"); - expect(sendMessage).toBeCalled(); - }); -}); +jest.mock("./MessageHandler"); +import { LogEntryLevel } from "../Contracts/Diagnostics"; +import * as Logger from "./Logger"; +import { MessageTypes } from "../Contracts/ExplorerContracts"; +import { sendMessage } from "./MessageHandler"; + +describe("Logger", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it("should log info messages", () => { + Logger.logInfo("Test info", "DocDB"); + expect(sendMessage).toBeCalled(); + }); + + it("should log error messages", () => { + Logger.logError("Test error", "DocDB"); + expect(sendMessage).toBeCalled(); + }); + + it("should log warnings", () => { + Logger.logWarning("Test warning", "DocDB"); + expect(sendMessage).toBeCalled(); + }); +}); diff --git a/src/Common/Logger.ts b/src/Common/Logger.ts index 58186d21e..1e34609df 100644 --- a/src/Common/Logger.ts +++ b/src/Common/Logger.ts @@ -29,7 +29,7 @@ export function logError(errorMessage: string, area: string, code?: number | str function _logEntry(entry: Diagnostics.LogEntry): void { sendMessage({ type: MessageTypes.LogInfo, - data: JSON.stringify(entry) + data: JSON.stringify(entry), }); const severityLevel = ((level: Diagnostics.LogEntryLevel): SeverityLevel => { @@ -60,6 +60,6 @@ function _generateLogEntry( level, message, area, - code + code, }; } diff --git a/src/Common/MessageHandler.test.ts b/src/Common/MessageHandler.test.ts index 505226243..2fb596d26 100644 --- a/src/Common/MessageHandler.test.ts +++ b/src/Common/MessageHandler.test.ts @@ -1,28 +1,28 @@ -import Q from "q"; -import * as MessageHandler from "./MessageHandler"; - -describe("Message Handler", () => { - it("should handle cached message", async () => { - let mockPromise = { - id: "123", - startTime: new Date(), - deferred: Q.defer() - }; - let mockMessage = { message: { id: "123", data: "{}" } }; - MessageHandler.RequestMap[mockPromise.id] = mockPromise; - MessageHandler.handleCachedDataMessage(mockMessage); - expect(mockPromise.deferred.promise.isFulfilled()).toBe(true); - }); - - it("should delete fulfilled promises on running the garbage collector", async () => { - let message = { - id: "123", - startTime: new Date(), - deferred: Q.defer() - }; - - MessageHandler.handleCachedDataMessage(message); - MessageHandler.runGarbageCollector(); - expect(MessageHandler.RequestMap["123"]).toBeUndefined(); - }); -}); +import Q from "q"; +import * as MessageHandler from "./MessageHandler"; + +describe("Message Handler", () => { + it("should handle cached message", async () => { + let mockPromise = { + id: "123", + startTime: new Date(), + deferred: Q.defer(), + }; + let mockMessage = { message: { id: "123", data: "{}" } }; + MessageHandler.RequestMap[mockPromise.id] = mockPromise; + MessageHandler.handleCachedDataMessage(mockMessage); + expect(mockPromise.deferred.promise.isFulfilled()).toBe(true); + }); + + it("should delete fulfilled promises on running the garbage collector", async () => { + let message = { + id: "123", + startTime: new Date(), + deferred: Q.defer(), + }; + + MessageHandler.handleCachedDataMessage(message); + MessageHandler.runGarbageCollector(); + expect(MessageHandler.RequestMap["123"]).toBeUndefined(); + }); +}); diff --git a/src/Common/MessageHandler.ts b/src/Common/MessageHandler.ts index 1932739e9..e5bee34ed 100644 --- a/src/Common/MessageHandler.ts +++ b/src/Common/MessageHandler.ts @@ -35,7 +35,7 @@ export function sendCachedDataMessage( let cachedDataPromise: CachedDataPromise = { deferred: Q.defer(), startTime: new Date(), - id: _.uniqueId() + id: _.uniqueId(), }; RequestMap[cachedDataPromise.id] = cachedDataPromise; sendMessage({ type: messageType, params: params, id: cachedDataPromise.id }); @@ -54,7 +54,7 @@ export function sendMessage(data: any): void { portalChildWindow.parent.postMessage( { signature: "pcIframe", - data: data + data: data, }, portalChildWindow.document.referrer ); diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 53d5f0e76..2b636a468 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -14,7 +14,7 @@ const fetchMock = () => { ok: true, text: () => "{}", json: () => "{}", - headers: new Map() + headers: new Map(), }); }; @@ -27,8 +27,8 @@ const collection = { partitionKey: { paths: ["/pk"], kind: "Hash", - version: 1 - } + version: 1, + }, } as Collection; const documentId = ({ @@ -38,8 +38,8 @@ const documentId = ({ partitionKey: { paths: ["/pk"], kind: "Hash", - version: 1 - } + version: 1, + }, } as unknown) as DocumentId; const databaseAccount = { @@ -52,8 +52,8 @@ const databaseAccount = { documentEndpoint: "bar", gremlinEndpoint: "foo", tableEndpoint: "foo", - cassandraEndpoint: "foo" - } + cassandraEndpoint: "foo", + }, } as DatabaseAccount; describe("MongoProxyClient", () => { @@ -61,10 +61,10 @@ describe("MongoProxyClient", () => { beforeEach(() => { resetConfigContext(); updateUserContext({ - databaseAccount + databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -93,10 +93,10 @@ describe("MongoProxyClient", () => { beforeEach(() => { resetConfigContext(); updateUserContext({ - databaseAccount + databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -125,10 +125,10 @@ describe("MongoProxyClient", () => { beforeEach(() => { resetConfigContext(); updateUserContext({ - databaseAccount + databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -157,10 +157,10 @@ describe("MongoProxyClient", () => { beforeEach(() => { resetConfigContext(); updateUserContext({ - databaseAccount + databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -189,10 +189,10 @@ describe("MongoProxyClient", () => { beforeEach(() => { resetConfigContext(); updateUserContext({ - databaseAccount + databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -222,10 +222,10 @@ describe("MongoProxyClient", () => { resetConfigContext(); delete window.authType; updateUserContext({ - databaseAccount + databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }); }); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index c489ef910..9f51d6188 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -16,7 +16,7 @@ import { sendMessage } from "./MessageHandler"; const defaultHeaders = { [HttpHeaders.apiType]: ApiType.MongoDB.toString(), [CosmosSDKConstants.HttpHeaders.MaxEntityCount]: "100", - [CosmosSDKConstants.HttpHeaders.Version]: "2017-11-15" + [CosmosSDKConstants.HttpHeaders.Version]: "2017-11-15", }; function authHeaders() { @@ -31,7 +31,7 @@ export function queryIterator(databaseId: string, collection: Collection, query: let continuationToken: string; return { fetchNext: () => { - return queryDocuments(databaseId, collection, false, query).then(response => { + return queryDocuments(databaseId, collection, false, query).then((response) => { continuationToken = response.continuationToken; const headers: { [key: string]: string | number } = {}; response.headers.forEach((value, key) => { @@ -42,10 +42,10 @@ export function queryIterator(databaseId: string, collection: Collection, query: headers, requestCharge: Number(headers[CosmosSDKConstants.HttpHeaders.RequestCharge]), activityId: String(headers[CosmosSDKConstants.HttpHeaders.ActivityId]), - hasMoreResults: !!continuationToken + hasMoreResults: !!continuationToken, }; }); - } + }, }; } @@ -74,7 +74,9 @@ export function queryDocuments( rg: userContext.resourceGroup, dba: databaseAccount.name, pk: - collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : "" + collection && collection.partitionKey && !collection.partitionKey.systemKey + ? collection.partitionKeyProperty + : "", }; const endpoint = getEndpoint() || ""; @@ -87,7 +89,7 @@ export function queryDocuments( [CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true", [CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true", [CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true", - [HttpHeaders.contentType]: "application/query+json" + [HttpHeaders.contentType]: "application/query+json", }; if (continuationToken) { @@ -100,14 +102,14 @@ export function queryDocuments( .fetch(`${endpoint}${path}?${queryString.stringify(params)}`, { method: "POST", body: JSON.stringify({ query }), - headers + headers, }) - .then(async response => { + .then(async (response) => { if (response.ok) { return { continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation), documents: (await response.json()).Documents as DataModels.DocumentId[], - headers: response.headers + headers: response.headers, }; } errorHandling(response, "querying documents", params); @@ -135,7 +137,9 @@ export function readDocument( rg: userContext.resourceGroup, dba: databaseAccount.name, pk: - documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperty + : "", }; const endpoint = getEndpoint(); @@ -147,10 +151,10 @@ export function readDocument( ...authHeaders(), [CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent( JSON.stringify(documentId.partitionKeyHeader()) - ) - } + ), + }, }) - .then(response => { + .then((response) => { if (response.ok) { return response.json(); } @@ -175,7 +179,7 @@ export function createDocument( sid: userContext.subscriptionId, rg: userContext.resourceGroup, dba: databaseAccount.name, - pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "" + pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", }; const endpoint = getEndpoint(); @@ -186,10 +190,10 @@ export function createDocument( body: JSON.stringify(documentContent), headers: { ...defaultHeaders, - ...authHeaders() - } + ...authHeaders(), + }, }) - .then(response => { + .then((response) => { if (response.ok) { return response.json(); } @@ -218,7 +222,9 @@ export function updateDocument( rg: userContext.resourceGroup, dba: databaseAccount.name, pk: - documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperty + : "", }; const endpoint = getEndpoint(); @@ -230,10 +236,10 @@ export function updateDocument( ...defaultHeaders, ...authHeaders(), [HttpHeaders.contentType]: "application/json", - [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()) - } + [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), + }, }) - .then(response => { + .then((response) => { if (response.ok) { return response.json(); } @@ -257,7 +263,9 @@ export function deleteDocument(databaseId: string, collection: Collection, docum rg: userContext.resourceGroup, dba: databaseAccount.name, pk: - documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperty + : "", }; const endpoint = getEndpoint(); @@ -268,10 +276,10 @@ export function deleteDocument(databaseId: string, collection: Collection, docum ...defaultHeaders, ...authHeaders(), [HttpHeaders.contentType]: "application/json", - [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()) - } + [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), + }, }) - .then(response => { + .then((response) => { if (response.ok) { return undefined; } @@ -299,7 +307,7 @@ export function createMongoCollectionWithProxy( rg: userContext.resourceGroup, dba: databaseAccount.name, isAutoPilot: !!params.autoPilotMaxThroughput, - autoPilotThroughput: params.autoPilotMaxThroughput?.toString() + autoPilotThroughput: params.autoPilotMaxThroughput?.toString(), }; const endpoint = getEndpoint(); @@ -314,11 +322,11 @@ export function createMongoCollectionWithProxy( headers: { ...defaultHeaders, ...authHeaders(), - [HttpHeaders.contentType]: "application/json" - } + [HttpHeaders.contentType]: "application/json", + }, } ) - .then(response => { + .then((response) => { if (response.ok) { return response.json(); } diff --git a/src/Common/MongoUtility.ts b/src/Common/MongoUtility.ts index e7e866b1a..829d17c4a 100644 --- a/src/Common/MongoUtility.ts +++ b/src/Common/MongoUtility.ts @@ -1,168 +1,168 @@ -/* Copyright 2013 10gen Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default class MongoUtility { - public static tojson = function(x: any, indent: string, nolint: boolean) { - if (x === null || x === undefined) { - return String(x); - } - indent = indent || ""; - - switch (typeof x) { - case "string": - var out = new Array(x.length + 1); - out[0] = '"'; - for (var i = 0; i < x.length; i++) { - if (x[i] === '"') { - out[out.length] = '\\"'; - } else if (x[i] === "\\") { - out[out.length] = "\\\\"; - } else if (x[i] === "\b") { - out[out.length] = "\\b"; - } else if (x[i] === "\f") { - out[out.length] = "\\f"; - } else if (x[i] === "\n") { - out[out.length] = "\\n"; - } else if (x[i] === "\r") { - out[out.length] = "\\r"; - } else if (x[i] === "\t") { - out[out.length] = "\\t"; - } else { - var code = x.charCodeAt(i); - if (code < 0x20) { - out[out.length] = (code < 0x10 ? "\\u000" : "\\u00") + code.toString(16); - } else { - out[out.length] = x[i]; - } - } - } - return out.join("") + '"'; - case "number": - /* falls through */ - case "boolean": - return "" + x; - case "object": - var func = $.isArray(x) ? MongoUtility.tojsonArray : MongoUtility.tojsonObject; - var s = func(x, indent, nolint); - if ( - (nolint === null || nolint === undefined || nolint === true) && - s.length < 80 && - (indent === null || indent.length === 0) - ) { - s = s.replace(/[\t\r\n]+/gm, " "); - } - return s; - case "function": - return x.toString(); - default: - throw new Error("tojson can't handle type " + typeof x); - } - }; - - private static tojsonObject = function(x: any, indent: string, nolint: boolean) { - var lineEnding = nolint ? " " : "\n"; - var tabSpace = nolint ? "" : "\t"; - indent = indent || ""; - - if (typeof x.tojson === "function" && x.tojson !== MongoUtility.tojson) { - return x.tojson(indent, nolint); - } - - if (x.constructor && typeof x.constructor.tojson === "function" && x.constructor.tojson !== MongoUtility.tojson) { - return x.constructor.tojson(x, indent, nolint); - } - - if (MongoUtility.hasDefinedProperty(x, "toString") && !$.isArray(x)) { - return x.toString(); - } - - if (x instanceof Error) { - return x.toString(); - } - - if (MongoUtility.isObjectId(x)) { - return 'ObjectId("' + x.$oid + '")'; - } - - // push one level of indent - indent += tabSpace; - var s = "{"; - - var pairs = []; - for (var k in x) { - if (x.hasOwnProperty(k)) { - var val = x[k]; - var pair = '"' + k + '" : ' + MongoUtility.tojson(val, indent, nolint); - - if (k === "_id") { - pairs.unshift(pair); - } else { - pairs.push(pair); - } - } - } - // Add proper line endings, indents, and commas to each line - s += $.map(pairs, function(pair) { - return lineEnding + indent + pair; - }).join(","); - s += lineEnding; - - // pop one level of indent - indent = indent.substring(1); - return s + indent + "}"; - }; - - private static tojsonArray = function(a: any, indent: string, nolint: boolean) { - if (a.length === 0) { - return "[ ]"; - } - - var lineEnding = nolint ? " " : "\n"; - if (!indent || nolint) { - indent = ""; - } - - var s = "[" + lineEnding; - indent += "\t"; - for (var i = 0; i < a.length; i++) { - s += indent + MongoUtility.tojson(a[i], indent, nolint); - if (i < a.length - 1) { - s += "," + lineEnding; - } - } - if (a.length === 0) { - s += indent; - } - - indent = indent.substring(1); - s += lineEnding + indent + "]"; - return s; - }; - - private static hasDefinedProperty = function(obj: any, prop: string): boolean { - if (Object.getPrototypeOf === undefined || Object.getPrototypeOf(obj) === null) { - return false; - } else if (obj.hasOwnProperty(prop)) { - return true; - } else { - return MongoUtility.hasDefinedProperty(Object.getPrototypeOf(obj), prop); - } - }; - - private static isObjectId(obj: any): boolean { - var keys = Object.keys(obj); - return keys.length === 1 && keys[0] === "$oid" && typeof obj.$oid === "string" && /^[0-9a-f]{24}$/.test(obj.$oid); - } -} +/* Copyright 2013 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class MongoUtility { + public static tojson = function (x: any, indent: string, nolint: boolean) { + if (x === null || x === undefined) { + return String(x); + } + indent = indent || ""; + + switch (typeof x) { + case "string": + var out = new Array(x.length + 1); + out[0] = '"'; + for (var i = 0; i < x.length; i++) { + if (x[i] === '"') { + out[out.length] = '\\"'; + } else if (x[i] === "\\") { + out[out.length] = "\\\\"; + } else if (x[i] === "\b") { + out[out.length] = "\\b"; + } else if (x[i] === "\f") { + out[out.length] = "\\f"; + } else if (x[i] === "\n") { + out[out.length] = "\\n"; + } else if (x[i] === "\r") { + out[out.length] = "\\r"; + } else if (x[i] === "\t") { + out[out.length] = "\\t"; + } else { + var code = x.charCodeAt(i); + if (code < 0x20) { + out[out.length] = (code < 0x10 ? "\\u000" : "\\u00") + code.toString(16); + } else { + out[out.length] = x[i]; + } + } + } + return out.join("") + '"'; + case "number": + /* falls through */ + case "boolean": + return "" + x; + case "object": + var func = $.isArray(x) ? MongoUtility.tojsonArray : MongoUtility.tojsonObject; + var s = func(x, indent, nolint); + if ( + (nolint === null || nolint === undefined || nolint === true) && + s.length < 80 && + (indent === null || indent.length === 0) + ) { + s = s.replace(/[\t\r\n]+/gm, " "); + } + return s; + case "function": + return x.toString(); + default: + throw new Error("tojson can't handle type " + typeof x); + } + }; + + private static tojsonObject = function (x: any, indent: string, nolint: boolean) { + var lineEnding = nolint ? " " : "\n"; + var tabSpace = nolint ? "" : "\t"; + indent = indent || ""; + + if (typeof x.tojson === "function" && x.tojson !== MongoUtility.tojson) { + return x.tojson(indent, nolint); + } + + if (x.constructor && typeof x.constructor.tojson === "function" && x.constructor.tojson !== MongoUtility.tojson) { + return x.constructor.tojson(x, indent, nolint); + } + + if (MongoUtility.hasDefinedProperty(x, "toString") && !$.isArray(x)) { + return x.toString(); + } + + if (x instanceof Error) { + return x.toString(); + } + + if (MongoUtility.isObjectId(x)) { + return 'ObjectId("' + x.$oid + '")'; + } + + // push one level of indent + indent += tabSpace; + var s = "{"; + + var pairs = []; + for (var k in x) { + if (x.hasOwnProperty(k)) { + var val = x[k]; + var pair = '"' + k + '" : ' + MongoUtility.tojson(val, indent, nolint); + + if (k === "_id") { + pairs.unshift(pair); + } else { + pairs.push(pair); + } + } + } + // Add proper line endings, indents, and commas to each line + s += $.map(pairs, function (pair) { + return lineEnding + indent + pair; + }).join(","); + s += lineEnding; + + // pop one level of indent + indent = indent.substring(1); + return s + indent + "}"; + }; + + private static tojsonArray = function (a: any, indent: string, nolint: boolean) { + if (a.length === 0) { + return "[ ]"; + } + + var lineEnding = nolint ? " " : "\n"; + if (!indent || nolint) { + indent = ""; + } + + var s = "[" + lineEnding; + indent += "\t"; + for (var i = 0; i < a.length; i++) { + s += indent + MongoUtility.tojson(a[i], indent, nolint); + if (i < a.length - 1) { + s += "," + lineEnding; + } + } + if (a.length === 0) { + s += indent; + } + + indent = indent.substring(1); + s += lineEnding + indent + "]"; + return s; + }; + + private static hasDefinedProperty = function (obj: any, prop: string): boolean { + if (Object.getPrototypeOf === undefined || Object.getPrototypeOf(obj) === null) { + return false; + } else if (obj.hasOwnProperty(prop)) { + return true; + } else { + return MongoUtility.hasDefinedProperty(Object.getPrototypeOf(obj), prop); + } + }; + + private static isObjectId(obj: any): boolean { + var keys = Object.keys(obj); + return keys.length === 1 && keys[0] === "$oid" && typeof obj.$oid === "string" && /^[0-9a-f]{24}$/.test(obj.$oid); + } +} diff --git a/src/Common/OfferUtility.test.ts b/src/Common/OfferUtility.test.ts index d24310758..ce86c0d8a 100644 --- a/src/Common/OfferUtility.test.ts +++ b/src/Common/OfferUtility.test.ts @@ -9,14 +9,14 @@ describe("parseSDKOfferResponse", () => { offerThroughput: 500, collectionThroughputInfo: { minimumRUForCollection: 400, - numPhysicalPartitions: 1 - } + numPhysicalPartitions: 1, + }, }, - id: "test" + id: "test", } as SDKOfferDefinition; const mockResponse = { - resource: mockOfferDefinition + resource: mockOfferDefinition, } as OfferResponse; const expectedResult: Offer = { @@ -25,7 +25,7 @@ describe("parseSDKOfferResponse", () => { minimumThroughput: 400, id: "test", offerDefinition: mockOfferDefinition, - offerReplacePending: false + offerReplacePending: false, }; expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); @@ -37,17 +37,17 @@ describe("parseSDKOfferResponse", () => { offerThroughput: 400, collectionThroughputInfo: { minimumRUForCollection: 400, - numPhysicalPartitions: 1 + numPhysicalPartitions: 1, }, offerAutopilotSettings: { - maxThroughput: 5000 - } + maxThroughput: 5000, + }, }, - id: "test" + id: "test", } as SDKOfferDefinition; const mockResponse = { - resource: mockOfferDefinition + resource: mockOfferDefinition, } as OfferResponse; const expectedResult: Offer = { @@ -56,7 +56,7 @@ describe("parseSDKOfferResponse", () => { minimumThroughput: 400, id: "test", offerDefinition: mockOfferDefinition, - offerReplacePending: false + offerReplacePending: false, }; expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); diff --git a/src/Common/OfferUtility.ts b/src/Common/OfferUtility.ts index 6d7f6bf7f..83eb5fccb 100644 --- a/src/Common/OfferUtility.ts +++ b/src/Common/OfferUtility.ts @@ -22,7 +22,7 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | und manualThroughput: undefined, minimumThroughput, offerDefinition, - offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" + offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true", }; } @@ -32,6 +32,6 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | und manualThroughput: offerContent.offerThroughput, minimumThroughput, offerDefinition, - offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" + offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true", }; }; diff --git a/src/Common/PortalNotifications.ts b/src/Common/PortalNotifications.ts index 34afd40e0..4298b0b8f 100644 --- a/src/Common/PortalNotifications.ts +++ b/src/Common/PortalNotifications.ts @@ -30,7 +30,7 @@ export const fetchPortalNotifications = async (): Promise { - const queriesCollection: ViewModels.Collection = this.findQueriesCollection(); - if (queriesCollection) { - return Promise.resolve(queriesCollection.rawDataModel); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); - return createCollection({ - collectionId: SavedQueries.CollectionName, - createNewDatabase: true, - databaseId: SavedQueries.DatabaseName, - partitionKey: QueriesClient.PartitionKey, - offerThroughput: SavedQueries.OfferThroughput, - databaseLevelThroughput: false - }) - .then( - (collection: DataModels.Collection) => { - NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); - return Promise.resolve(collection); - }, - (error: any) => { - handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries"); - return Promise.reject(error); - } - ) - .finally(() => clearMessage()); - } - - public async saveQuery(query: DataModels.Query): Promise { - const queriesCollection = this.findQueriesCollection(); - if (!queriesCollection) { - const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); - return Promise.reject(errorMessage); - } - - try { - this.validateQuery(query); - } catch (error) { - const errorMessage: string = "Invalid query specified"; - NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); - return Promise.reject(errorMessage); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); - query.id = query.queryName; - return createDocument(queriesCollection, query) - .then( - (savedQuery: DataModels.Query) => { - NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); - return Promise.resolve(); - }, - (error: any) => { - if (error.code === HttpStatusCodes.Conflict.toString()) { - error = `Query ${query.queryName} already exists`; - } - handleError(error, "saveQuery", `Failed to save query ${query.queryName}`); - return Promise.reject(error); - } - ) - .finally(() => clearMessage()); - } - - public async getQueries(): Promise { - const queriesCollection = this.findQueriesCollection(); - if (!queriesCollection) { - const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); - return Promise.reject(errorMessage); - } - - const options: any = { enableCrossPartitionQuery: true }; - const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); - const queryIterator: QueryIterator = queryDocuments( - SavedQueries.DatabaseName, - SavedQueries.CollectionName, - this.fetchQueriesQuery(), - options - ); - const fetchQueries = async (firstItemIndex: number): Promise => - await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex); - return QueryUtils.queryAllPages(fetchQueries) - .then( - (results: ViewModels.QueryResults) => { - let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { - if (!document) { - return undefined; - } - const { id, resourceId, query, queryName } = document; - const parsedQuery: DataModels.Query = { - resourceId: resourceId, - queryName: queryName, - query: query, - id: id - }; - try { - this.validateQuery(parsedQuery); - return parsedQuery; - } catch (error) { - return undefined; - } - }); - queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); - NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); - return Promise.resolve(queries); - }, - (error: any) => { - handleError(error, "getSavedQueries", "Failed to fetch saved queries"); - return Promise.reject(error); - } - ) - .finally(() => clearMessage()); - } - - public async deleteQuery(query: DataModels.Query): Promise { - const queriesCollection = this.findQueriesCollection(); - if (!queriesCollection) { - const errorMessage: string = "Account not set up to perform saved query operations"; - NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); - return Promise.reject(errorMessage); - } - - try { - this.validateQuery(query); - } catch (error) { - const errorMessage: string = "Invalid query specified"; - NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); - query.id = query.queryName; - const documentId = new DocumentId( - { - partitionKey: QueriesClient.PartitionKey, - partitionKeyProperty: "id" - } as DocumentsTab, - query, - query.queryName - ); // TODO: Remove DocumentId's dependency on DocumentsTab - const options: any = { partitionKey: query.resourceId }; - return deleteDocument(queriesCollection, documentId) - .then( - () => { - NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); - return Promise.resolve(); - }, - (error: any) => { - handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`); - return Promise.reject(error); - } - ) - .finally(() => clearMessage()); - } - - public getResourceId(): string { - const databaseAccount = userContext.databaseAccount; - const databaseAccountName = (databaseAccount && databaseAccount.name) || ""; - const subscriptionId = userContext.subscriptionId || ""; - const resourceGroup = userContext.resourceGroup || ""; - - return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`; - } - - private findQueriesCollection(): ViewModels.Collection { - const queriesDatabase: ViewModels.Database = _.find( - this.container.databases(), - (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName - ); - if (!queriesDatabase) { - return undefined; - } - return _.find( - queriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === SavedQueries.CollectionName - ); - } - - private validateQuery(query: DataModels.Query): void { - if (!query || query.queryName == null || query.query == null || query.resourceId == null) { - throw new Error("Invalid query specified"); - } - } - - private fetchQueriesQuery(): string { - if (this.container.isPreferredApiMongoDB()) { - return QueriesClient.FetchMongoQuery; - } - return QueriesClient.FetchQuery; - } -} +import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import * as _ from "underscore"; +import * as DataModels from "../Contracts/DataModels"; +import * as ViewModels from "../Contracts/ViewModels"; +import Explorer from "../Explorer/Explorer"; +import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; +import DocumentId from "../Explorer/Tree/DocumentId"; +import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; +import { QueryUtils } from "../Utils/QueryUtils"; +import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; +import { userContext } from "../UserContext"; +import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; +import { createCollection } from "./dataAccess/createCollection"; +import { handleError } from "./ErrorHandlingUtils"; +import { createDocument } from "./dataAccess/createDocument"; +import { deleteDocument } from "./dataAccess/deleteDocument"; +import { queryDocuments } from "./dataAccess/queryDocuments"; + +export class QueriesClient { + private static readonly PartitionKey: DataModels.PartitionKey = { + paths: [`/${SavedQueries.PartitionKeyProperty}`], + kind: BackendDefaults.partitionKeyKind, + version: BackendDefaults.partitionKeyVersion, + }; + private static readonly FetchQuery: string = "SELECT * FROM c"; + private static readonly FetchMongoQuery: string = "{}"; + + public constructor(private container: Explorer) {} + + public async setupQueriesCollection(): Promise { + const queriesCollection: ViewModels.Collection = this.findQueriesCollection(); + if (queriesCollection) { + return Promise.resolve(queriesCollection.rawDataModel); + } + + const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); + return createCollection({ + collectionId: SavedQueries.CollectionName, + createNewDatabase: true, + databaseId: SavedQueries.DatabaseName, + partitionKey: QueriesClient.PartitionKey, + offerThroughput: SavedQueries.OfferThroughput, + databaseLevelThroughput: false, + }) + .then( + (collection: DataModels.Collection) => { + NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); + return Promise.resolve(collection); + }, + (error: any) => { + handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries"); + return Promise.reject(error); + } + ) + .finally(() => clearMessage()); + } + + public async saveQuery(query: DataModels.Query): Promise { + const queriesCollection = this.findQueriesCollection(); + if (!queriesCollection) { + const errorMessage: string = "Account not set up to perform saved query operations"; + NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); + return Promise.reject(errorMessage); + } + + try { + this.validateQuery(query); + } catch (error) { + const errorMessage: string = "Invalid query specified"; + NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); + return Promise.reject(errorMessage); + } + + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); + query.id = query.queryName; + return createDocument(queriesCollection, query) + .then( + (savedQuery: DataModels.Query) => { + NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); + return Promise.resolve(); + }, + (error: any) => { + if (error.code === HttpStatusCodes.Conflict.toString()) { + error = `Query ${query.queryName} already exists`; + } + handleError(error, "saveQuery", `Failed to save query ${query.queryName}`); + return Promise.reject(error); + } + ) + .finally(() => clearMessage()); + } + + public async getQueries(): Promise { + const queriesCollection = this.findQueriesCollection(); + if (!queriesCollection) { + const errorMessage: string = "Account not set up to perform saved query operations"; + NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); + return Promise.reject(errorMessage); + } + + const options: any = { enableCrossPartitionQuery: true }; + const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); + const queryIterator: QueryIterator = queryDocuments( + SavedQueries.DatabaseName, + SavedQueries.CollectionName, + this.fetchQueriesQuery(), + options + ); + const fetchQueries = async (firstItemIndex: number): Promise => + await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex); + return QueryUtils.queryAllPages(fetchQueries) + .then( + (results: ViewModels.QueryResults) => { + let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { + if (!document) { + return undefined; + } + const { id, resourceId, query, queryName } = document; + const parsedQuery: DataModels.Query = { + resourceId: resourceId, + queryName: queryName, + query: query, + id: id, + }; + try { + this.validateQuery(parsedQuery); + return parsedQuery; + } catch (error) { + return undefined; + } + }); + queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); + NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); + return Promise.resolve(queries); + }, + (error: any) => { + handleError(error, "getSavedQueries", "Failed to fetch saved queries"); + return Promise.reject(error); + } + ) + .finally(() => clearMessage()); + } + + public async deleteQuery(query: DataModels.Query): Promise { + const queriesCollection = this.findQueriesCollection(); + if (!queriesCollection) { + const errorMessage: string = "Account not set up to perform saved query operations"; + NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); + return Promise.reject(errorMessage); + } + + try { + this.validateQuery(query); + } catch (error) { + const errorMessage: string = "Invalid query specified"; + NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); + } + + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); + query.id = query.queryName; + const documentId = new DocumentId( + { + partitionKey: QueriesClient.PartitionKey, + partitionKeyProperty: "id", + } as DocumentsTab, + query, + query.queryName + ); // TODO: Remove DocumentId's dependency on DocumentsTab + const options: any = { partitionKey: query.resourceId }; + return deleteDocument(queriesCollection, documentId) + .then( + () => { + NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); + return Promise.resolve(); + }, + (error: any) => { + handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`); + return Promise.reject(error); + } + ) + .finally(() => clearMessage()); + } + + public getResourceId(): string { + const databaseAccount = userContext.databaseAccount; + const databaseAccountName = (databaseAccount && databaseAccount.name) || ""; + const subscriptionId = userContext.subscriptionId || ""; + const resourceGroup = userContext.resourceGroup || ""; + + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`; + } + + private findQueriesCollection(): ViewModels.Collection { + const queriesDatabase: ViewModels.Database = _.find( + this.container.databases(), + (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName + ); + if (!queriesDatabase) { + return undefined; + } + return _.find( + queriesDatabase.collections(), + (collection: ViewModels.Collection) => collection.id() === SavedQueries.CollectionName + ); + } + + private validateQuery(query: DataModels.Query): void { + if (!query || query.queryName == null || query.query == null || query.resourceId == null) { + throw new Error("Invalid query specified"); + } + } + + private fetchQueriesQuery(): string { + if (this.container.isPreferredApiMongoDB()) { + return QueriesClient.FetchMongoQuery; + } + return QueriesClient.FetchQuery; + } +} diff --git a/src/Common/Splitter.ts b/src/Common/Splitter.ts index 5699247f1..5785aa8ec 100644 --- a/src/Common/Splitter.ts +++ b/src/Common/Splitter.ts @@ -1,109 +1,107 @@ -import * as ko from "knockout"; - -import { SplitterMetrics } from "./Constants"; - -export enum SplitterDirection { - Horizontal = "horizontal", - Vertical = "vertical" -} - -export interface SplitterBounds { - max: number; - min: number; -} - -export interface SplitterOptions { - splitterId: string; - leftId: string; - bounds: SplitterBounds; - direction: SplitterDirection; -} - -export class Splitter { - public splitterId: string; - public leftSideId: string; - - public splitter!: HTMLElement; - public leftSide!: HTMLElement; - public lastX!: number; - public lastWidth!: number; - - private isCollapsed: ko.Observable; - private bounds: SplitterBounds; - private direction: SplitterDirection; - - constructor(options: SplitterOptions) { - this.splitterId = options.splitterId; - this.leftSideId = options.leftId; - this.isCollapsed = ko.observable(false); - this.bounds = options.bounds; - this.direction = options.direction; - this.initialize(); - } - - public initialize() { - if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) { - this.splitter = document.getElementById(this.splitterId); - this.leftSide = document.getElementById(this.leftSideId); - } - const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical; - const splitterOptions: JQueryUI.ResizableOptions = { - animate: true, - animateDuration: "fast", - start: this.onResizeStart, - stop: this.onResizeStop - }; - - if (isVerticalSplitter) { - $(this.leftSide).css("width", this.bounds.min); - $(this.splitter).css("height", "100%"); - - splitterOptions.maxWidth = this.bounds.max; - splitterOptions.minWidth = this.bounds.min; - splitterOptions.handles = { e: "#" + this.splitterId }; - } else { - $(this.leftSide).css("height", this.bounds.min); - $(this.splitter).css("width", "100%"); - - splitterOptions.maxHeight = this.bounds.max; - splitterOptions.minHeight = this.bounds.min; - splitterOptions.handles = { s: "#" + this.splitterId }; - } - - $(this.leftSide).resizable(splitterOptions); - } - - private onResizeStart: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { - if (this.direction === SplitterDirection.Vertical) { - $(".ui-resizable-helper").height("100%"); - } else { - $(".ui-resizable-helper").width("100%"); - } - $("iframe").css("pointer-events", "none"); - }; - - private onResizeStop: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { - $("iframe").css("pointer-events", "auto"); - }; - - public collapseLeft() { - this.lastX = $(this.splitter).position().left; - this.lastWidth = $(this.leftSide).width(); - $(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft); - $(this.leftSide).css("width", ""); - $(this.leftSide) - .resizable("option", "disabled", true) - .removeClass("ui-resizable-disabled"); // remove class so splitter is visible - $(this.splitter).removeClass("ui-resizable-e"); - this.isCollapsed(true); - } - - public expandLeft() { - $(this.splitter).addClass("ui-resizable-e"); - $(this.leftSide).css("width", this.lastWidth); - $(this.splitter).css("left", this.lastX); - $(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing - $(this.leftSide).resizable("enable"); - this.isCollapsed(false); - } -} +import * as ko from "knockout"; + +import { SplitterMetrics } from "./Constants"; + +export enum SplitterDirection { + Horizontal = "horizontal", + Vertical = "vertical", +} + +export interface SplitterBounds { + max: number; + min: number; +} + +export interface SplitterOptions { + splitterId: string; + leftId: string; + bounds: SplitterBounds; + direction: SplitterDirection; +} + +export class Splitter { + public splitterId: string; + public leftSideId: string; + + public splitter!: HTMLElement; + public leftSide!: HTMLElement; + public lastX!: number; + public lastWidth!: number; + + private isCollapsed: ko.Observable; + private bounds: SplitterBounds; + private direction: SplitterDirection; + + constructor(options: SplitterOptions) { + this.splitterId = options.splitterId; + this.leftSideId = options.leftId; + this.isCollapsed = ko.observable(false); + this.bounds = options.bounds; + this.direction = options.direction; + this.initialize(); + } + + public initialize() { + if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) { + this.splitter = document.getElementById(this.splitterId); + this.leftSide = document.getElementById(this.leftSideId); + } + const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical; + const splitterOptions: JQueryUI.ResizableOptions = { + animate: true, + animateDuration: "fast", + start: this.onResizeStart, + stop: this.onResizeStop, + }; + + if (isVerticalSplitter) { + $(this.leftSide).css("width", this.bounds.min); + $(this.splitter).css("height", "100%"); + + splitterOptions.maxWidth = this.bounds.max; + splitterOptions.minWidth = this.bounds.min; + splitterOptions.handles = { e: "#" + this.splitterId }; + } else { + $(this.leftSide).css("height", this.bounds.min); + $(this.splitter).css("width", "100%"); + + splitterOptions.maxHeight = this.bounds.max; + splitterOptions.minHeight = this.bounds.min; + splitterOptions.handles = { s: "#" + this.splitterId }; + } + + $(this.leftSide).resizable(splitterOptions); + } + + private onResizeStart: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { + if (this.direction === SplitterDirection.Vertical) { + $(".ui-resizable-helper").height("100%"); + } else { + $(".ui-resizable-helper").width("100%"); + } + $("iframe").css("pointer-events", "none"); + }; + + private onResizeStop: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { + $("iframe").css("pointer-events", "auto"); + }; + + public collapseLeft() { + this.lastX = $(this.splitter).position().left; + this.lastWidth = $(this.leftSide).width(); + $(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft); + $(this.leftSide).css("width", ""); + $(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible + $(this.splitter).removeClass("ui-resizable-e"); + this.isCollapsed(true); + } + + public expandLeft() { + $(this.splitter).addClass("ui-resizable-e"); + $(this.leftSide).css("width", this.lastWidth); + $(this.splitter).css("left", this.lastX); + $(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing + $(this.leftSide).resizable("enable"); + this.isCollapsed(false); + } +} diff --git a/src/Common/UrlUtility.ts b/src/Common/UrlUtility.ts index 460372e30..4d6727956 100644 --- a/src/Common/UrlUtility.ts +++ b/src/Common/UrlUtility.ts @@ -32,8 +32,8 @@ export default class UrlUtility { type: type, objectBody: { id: id, - self: resourcePath - } + self: resourcePath, + }, }; return result; diff --git a/src/Common/dataAccess/createCollection.test.ts b/src/Common/dataAccess/createCollection.test.ts index 7d4076f35..9f7ed9783 100644 --- a/src/Common/dataAccess/createCollection.test.ts +++ b/src/Common/dataAccess/createCollection.test.ts @@ -14,15 +14,15 @@ describe("createCollection", () => { collectionId: "testContainer", databaseId: "testDatabase", databaseLevelThroughput: true, - offerThroughput: 400 + offerThroughput: 400, }; beforeAll(() => { updateUserContext({ databaseAccount: { - name: "test" + name: "test", } as DatabaseAccount, - defaultExperience: DefaultAccountExperienceType.DocumentDB + defaultExperience: DefaultAccountExperienceType.DocumentDB, }); }); @@ -40,12 +40,12 @@ describe("createCollection", () => { return { database: { containers: { - create: () => ({}) - } - } + create: () => ({}), + }, + }, }; - } - } + }, + }, }); await createCollection(createCollectionParams); expect(client).toHaveBeenCalled(); @@ -59,7 +59,7 @@ describe("createCollection", () => { collectionId: "testContainer", databaseId: "testDatabase", databaseLevelThroughput: false, - offerThroughput: 400 + offerThroughput: 400, }; expect(constructRpOptions(manualThroughputParams)).toEqual({ throughput: 400 }); @@ -69,12 +69,12 @@ describe("createCollection", () => { databaseId: "testDatabase", databaseLevelThroughput: false, offerThroughput: 400, - autoPilotMaxThroughput: 4000 + autoPilotMaxThroughput: 4000, }; expect(constructRpOptions(autoPilotThroughputParams)).toEqual({ autoscaleSettings: { - maxThroughput: 4000 - } + maxThroughput: 4000, + }, }); }); }); diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 97a0c2a62..b4700ce60 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -11,15 +11,15 @@ import { createMongoCollectionWithProxy } from "../MongoProxyClient"; import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { createUpdateCassandraTable, - getCassandraTable + getCassandraTable, } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { createUpdateMongoDBCollection, - getMongoDBCollection + getMongoDBCollection, } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { createUpdateGremlinGraph, - getGremlinGraph + getGremlinGraph, } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; @@ -41,7 +41,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams autoPilotMaxThroughput: params.autoPilotMaxThroughput, databaseId: params.databaseId, databaseLevelThroughput: params.databaseLevelThroughput, - offerThroughput: params.offerThroughput + offerThroughput: params.offerThroughput, }; await createDatabase(createDatabaseParams); } @@ -100,7 +100,7 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.SqlContainerResource = { - id: params.collectionId + id: params.collectionId, }; if (params.analyticalStorageTtl) { resource.analyticalStorageTtl = params.analyticalStorageTtl; @@ -118,8 +118,8 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = { properties: { resource, - options - } + options, + }, }; const createResponse = await createUpdateSqlContainer( @@ -154,7 +154,7 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams): const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.MongoDBCollectionResource = { - id: params.collectionId + id: params.collectionId, }; if (params.analyticalStorageTtl) { resource.analyticalStorageTtl = params.analyticalStorageTtl; @@ -170,8 +170,8 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams): const rpPayload: ARMTypes.MongoDBCollectionCreateUpdateParameters = { properties: { resource, - options - } + options, + }, }; const createResponse = await createUpdateMongoDBCollection( @@ -185,7 +185,7 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams): if (params.createMongoWildcardIndex) { TelemetryProcessor.trace(Action.CreateMongoCollectionWithWildcardIndex, ActionModifiers.Mark, { - message: "Mongo Collection created with wildcard index on all fields." + message: "Mongo Collection created with wildcard index on all fields.", }); } @@ -212,7 +212,7 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams): const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.CassandraTableResource = { - id: params.collectionId + id: params.collectionId, }; if (params.analyticalStorageTtl) { resource.analyticalStorageTtl = params.analyticalStorageTtl; @@ -221,8 +221,8 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams): const rpPayload: ARMTypes.CassandraTableCreateUpdateParameters = { properties: { resource, - options - } + options, + }, }; const createResponse = await createUpdateCassandraTable( @@ -256,7 +256,7 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise { beforeAll(() => { updateUserContext({ databaseAccount: { - name: "test" + name: "test", } as DatabaseAccount, - defaultExperience: DefaultAccountExperienceType.DocumentDB + defaultExperience: DefaultAccountExperienceType.DocumentDB, }); }); @@ -32,11 +32,11 @@ describe("deleteCollection", () => { return { container: () => { return { - delete: (): unknown => undefined + delete: (): unknown => undefined, }; - } + }, }; - } + }, }); await deleteCollection("database", "collection"); expect(client).toHaveBeenCalled(); diff --git a/src/Common/dataAccess/deleteCollection.ts b/src/Common/dataAccess/deleteCollection.ts index 5a5c4fd7d..3672fcce9 100644 --- a/src/Common/dataAccess/deleteCollection.ts +++ b/src/Common/dataAccess/deleteCollection.ts @@ -16,10 +16,7 @@ export async function deleteCollection(databaseId: string, collectionId: string) if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { await deleteCollectionWithARM(databaseId, collectionId); } else { - await client() - .database(databaseId) - .container(collectionId) - .delete(); + await client().database(databaseId).container(collectionId).delete(); } logConsoleInfo(`Successfully deleted container ${collectionId}`); } catch (error) { diff --git a/src/Common/dataAccess/deleteConflict.ts b/src/Common/dataAccess/deleteConflict.ts index 746d3577b..5c778f474 100644 --- a/src/Common/dataAccess/deleteConflict.ts +++ b/src/Common/dataAccess/deleteConflict.ts @@ -10,7 +10,7 @@ export const deleteConflict = async (collection: CollectionBase, conflictId: Con try { const options = { - partitionKey: getPartitionKeyHeaderForConflict(conflictId) + partitionKey: getPartitionKeyHeaderForConflict(conflictId), }; await client() diff --git a/src/Common/dataAccess/deleteDatabase.test.ts b/src/Common/dataAccess/deleteDatabase.test.ts index 19d702ba3..226e5322f 100644 --- a/src/Common/dataAccess/deleteDatabase.test.ts +++ b/src/Common/dataAccess/deleteDatabase.test.ts @@ -13,9 +13,9 @@ describe("deleteDatabase", () => { beforeAll(() => { updateUserContext({ databaseAccount: { - name: "test" + name: "test", } as DatabaseAccount, - defaultExperience: DefaultAccountExperienceType.DocumentDB + defaultExperience: DefaultAccountExperienceType.DocumentDB, }); }); @@ -30,9 +30,9 @@ describe("deleteDatabase", () => { (client as jest.Mock).mockReturnValue({ database: () => { return { - delete: (): unknown => undefined + delete: (): unknown => undefined, }; - } + }, }); await deleteDatabase("database"); expect(client).toHaveBeenCalled(); diff --git a/src/Common/dataAccess/deleteDatabase.ts b/src/Common/dataAccess/deleteDatabase.ts index 40029c0d2..93c618ac9 100644 --- a/src/Common/dataAccess/deleteDatabase.ts +++ b/src/Common/dataAccess/deleteDatabase.ts @@ -19,9 +19,7 @@ export async function deleteDatabase(databaseId: string): Promise { if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { await deleteDatabaseWithARM(databaseId); } else { - await client() - .database(databaseId) - .delete(); + await client().database(databaseId).delete(); } logConsoleInfo(`Successfully deleted database ${databaseId}`); } catch (error) { diff --git a/src/Common/dataAccess/deleteStoredProcedure.ts b/src/Common/dataAccess/deleteStoredProcedure.ts index fac47de33..6d57df1fc 100644 --- a/src/Common/dataAccess/deleteStoredProcedure.ts +++ b/src/Common/dataAccess/deleteStoredProcedure.ts @@ -27,11 +27,7 @@ export async function deleteStoredProcedure( storedProcedureId ); } else { - await client() - .database(databaseId) - .container(collectionId) - .scripts.storedProcedure(storedProcedureId) - .delete(); + await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete(); } } catch (error) { handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`); diff --git a/src/Common/dataAccess/deleteTrigger.ts b/src/Common/dataAccess/deleteTrigger.ts index f8a5713db..c4552083d 100644 --- a/src/Common/dataAccess/deleteTrigger.ts +++ b/src/Common/dataAccess/deleteTrigger.ts @@ -23,11 +23,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr triggerId ); } else { - await client() - .database(databaseId) - .container(collectionId) - .scripts.trigger(triggerId) - .delete(); + await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete(); } } catch (error) { handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`); diff --git a/src/Common/dataAccess/deleteUserDefinedFunction.ts b/src/Common/dataAccess/deleteUserDefinedFunction.ts index 6160ac52c..5bc64552e 100644 --- a/src/Common/dataAccess/deleteUserDefinedFunction.ts +++ b/src/Common/dataAccess/deleteUserDefinedFunction.ts @@ -23,11 +23,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId id ); } else { - await client() - .database(databaseId) - .container(collectionId) - .scripts.userDefinedFunction(id) - .delete(); + await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete(); } } catch (error) { handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`); diff --git a/src/Common/dataAccess/executeStoredProcedure.ts b/src/Common/dataAccess/executeStoredProcedure.ts index 7459c3a02..e0d99f3dd 100644 --- a/src/Common/dataAccess/executeStoredProcedure.ts +++ b/src/Common/dataAccess/executeStoredProcedure.ts @@ -33,7 +33,7 @@ export const executeStoredProcedure = async ( ); return { result: response.resource, - scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string + scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string, }; } catch (error) { handleError( diff --git a/src/Common/dataAccess/getCollectionDataUsageSize.ts b/src/Common/dataAccess/getCollectionDataUsageSize.ts index bdbca9e04..94718d630 100644 --- a/src/Common/dataAccess/getCollectionDataUsageSize.ts +++ b/src/Common/dataAccess/getCollectionDataUsageSize.ts @@ -60,8 +60,8 @@ export const getCollectionUsageSizeInKB = async (databaseName: string, container apiVersion: "2018-01-01", queryParams: { filter, - metricNames - } + metricNames, + }, }); if (metricsResponse?.value?.length !== 2) { diff --git a/src/Common/dataAccess/getIndexTransformationProgress.ts b/src/Common/dataAccess/getIndexTransformationProgress.ts index fa0298fbc..be58097b3 100644 --- a/src/Common/dataAccess/getIndexTransformationProgress.ts +++ b/src/Common/dataAccess/getIndexTransformationProgress.ts @@ -11,10 +11,7 @@ export async function getIndexTransformationProgress(databaseId: string, collect let indexTransformationPercentage: number; const clearMessage = logConsoleProgress(`Reading container ${collectionId}`); try { - const response = await client() - .database(databaseId) - .container(collectionId) - .read({ populateQuotaInfo: true }); + const response = await client().database(databaseId).container(collectionId).read({ populateQuotaInfo: true }); indexTransformationPercentage = parseInt( response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string diff --git a/src/Common/dataAccess/queryConflicts.ts b/src/Common/dataAccess/queryConflicts.ts index ed3ecaa26..16735dd26 100644 --- a/src/Common/dataAccess/queryConflicts.ts +++ b/src/Common/dataAccess/queryConflicts.ts @@ -7,8 +7,5 @@ export const queryConflicts = ( query: string, options: FeedOptions ): QueryIterator => { - return client() - .database(databaseId) - .container(containerId) - .conflicts.query(query, options); + return client().database(databaseId).container(containerId).conflicts.query(query, options); }; diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 0436b756c..16b2fb39e 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -10,10 +10,7 @@ export const queryDocuments = ( options: FeedOptions ): QueryIterator => { options = getCommonQueryOptions(options); - return client() - .database(databaseId) - .container(containerId) - .items.query(query, options); + return client().database(databaseId).container(containerId).items.query(query, options); }; export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { diff --git a/src/Common/dataAccess/readCollection.test.ts b/src/Common/dataAccess/readCollection.test.ts index a1fd80fcb..143d69db7 100644 --- a/src/Common/dataAccess/readCollection.test.ts +++ b/src/Common/dataAccess/readCollection.test.ts @@ -10,9 +10,9 @@ describe("readCollection", () => { beforeAll(() => { updateUserContext({ databaseAccount: { - name: "test" + name: "test", } as DatabaseAccount, - defaultExperience: DefaultAccountExperienceType.DocumentDB + defaultExperience: DefaultAccountExperienceType.DocumentDB, }); }); @@ -23,11 +23,11 @@ describe("readCollection", () => { return { container: () => { return { - read: (): unknown => ({}) + read: (): unknown => ({}), }; - } + }, }; - } + }, }); await readCollection("database", "collection"); expect(client).toHaveBeenCalled(); diff --git a/src/Common/dataAccess/readCollection.ts b/src/Common/dataAccess/readCollection.ts index ce886328d..6df44a932 100644 --- a/src/Common/dataAccess/readCollection.ts +++ b/src/Common/dataAccess/readCollection.ts @@ -7,10 +7,7 @@ export async function readCollection(databaseId: string, collectionId: string): let collection: DataModels.Collection; const clearMessage = logConsoleProgress(`Querying container ${collectionId}`); try { - const response = await client() - .database(databaseId) - .container(collectionId) - .read(); + const response = await client().database(databaseId).container(collectionId).read(); collection = response.resource as DataModels.Collection; } catch (error) { handleError(error, "ReadCollection", `Error while querying container ${collectionId}`); diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index 9d0ed5fc3..aa1edbcae 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -106,7 +106,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri autoscaleMaxThroughput: autoscaleSettings.maxThroughput, manualThroughput: undefined, minimumThroughput, - offerReplacePending: resource.offerReplacePending === "true" + offerReplacePending: resource.offerReplacePending === "true", }; } @@ -115,7 +115,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri autoscaleMaxThroughput: undefined, manualThroughput: resource.throughput, minimumThroughput, - offerReplacePending: resource.offerReplacePending === "true" + offerReplacePending: resource.offerReplacePending === "true", }; } diff --git a/src/Common/dataAccess/readCollections.test.ts b/src/Common/dataAccess/readCollections.test.ts index a45e6bd5d..ad55ca7ca 100644 --- a/src/Common/dataAccess/readCollections.test.ts +++ b/src/Common/dataAccess/readCollections.test.ts @@ -12,9 +12,9 @@ describe("readCollections", () => { beforeAll(() => { updateUserContext({ databaseAccount: { - name: "test" + name: "test", } as DatabaseAccount, - defaultExperience: DefaultAccountExperienceType.DocumentDB + defaultExperience: DefaultAccountExperienceType.DocumentDB, }); }); @@ -32,12 +32,12 @@ describe("readCollections", () => { containers: { readAll: () => { return { - fetchAll: (): unknown => [] + fetchAll: (): unknown => [], }; - } - } + }, + }, }; - } + }, }); await readCollections("database"); expect(client).toHaveBeenCalled(); diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts index a6b741d91..8817006ac 100644 --- a/src/Common/dataAccess/readCollections.ts +++ b/src/Common/dataAccess/readCollections.ts @@ -23,10 +23,7 @@ export async function readCollections(databaseId: string): Promise collection.properties?.resource as DataModels.Collection); + return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection); } diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index 9e99745c7..93fc46e3c 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -78,7 +78,7 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise => { autoscaleMaxThroughput: autoscaleSettings.maxThroughput, manualThroughput: undefined, minimumThroughput, - offerReplacePending: resource.offerReplacePending === "true" + offerReplacePending: resource.offerReplacePending === "true", }; } @@ -87,7 +87,7 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise => { autoscaleMaxThroughput: undefined, manualThroughput: resource.throughput, minimumThroughput, - offerReplacePending: resource.offerReplacePending === "true" + offerReplacePending: resource.offerReplacePending === "true", }; } diff --git a/src/Common/dataAccess/readDatabases.test.ts b/src/Common/dataAccess/readDatabases.test.ts index be84727af..2b03db3a1 100644 --- a/src/Common/dataAccess/readDatabases.test.ts +++ b/src/Common/dataAccess/readDatabases.test.ts @@ -12,9 +12,9 @@ describe("readDatabases", () => { beforeAll(() => { updateUserContext({ databaseAccount: { - name: "test" + name: "test", } as DatabaseAccount, - defaultExperience: DefaultAccountExperienceType.DocumentDB + defaultExperience: DefaultAccountExperienceType.DocumentDB, }); }); @@ -30,10 +30,10 @@ describe("readDatabases", () => { databases: { readAll: () => { return { - fetchAll: (): unknown => [] + fetchAll: (): unknown => [], }; - } - } + }, + }, }); await readDatabases(); expect(client).toHaveBeenCalled(); diff --git a/src/Common/dataAccess/readDatabases.ts b/src/Common/dataAccess/readDatabases.ts index 85a6c4632..07d82768f 100644 --- a/src/Common/dataAccess/readDatabases.ts +++ b/src/Common/dataAccess/readDatabases.ts @@ -21,9 +21,7 @@ export async function readDatabases(): Promise { ) { databases = await readDatabasesWithARM(); } else { - const sdkResponse = await client() - .databases.readAll() - .fetchAll(); + const sdkResponse = await client().databases.readAll().fetchAll(); databases = sdkResponse.resources as DataModels.Database[]; } } catch (error) { @@ -58,5 +56,5 @@ async function readDatabasesWithARM(): Promise { throw new Error(`Unsupported default experience type: ${defaultExperience}`); } - return rpResponse?.value?.map(database => database.properties?.resource as DataModels.Database); + return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database); } diff --git a/src/Common/dataAccess/readOfferWithSDK.ts b/src/Common/dataAccess/readOfferWithSDK.ts index ce60a3c0f..761d9b282 100644 --- a/src/Common/dataAccess/readOfferWithSDK.ts +++ b/src/Common/dataAccess/readOfferWithSDK.ts @@ -8,7 +8,7 @@ import { readOffers } from "./readOffers"; export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise => { if (!offerId) { const offers = await readOffers(); - const offer = offers.find(offer => offer.resource === resourceId); + const offer = offers.find((offer) => offer.resource === resourceId); if (!offer) { return undefined; @@ -18,12 +18,10 @@ export const readOfferWithSDK = async (offerId: string, resourceId: string): Pro const options: RequestOptions = { initialHeaders: { - [HttpHeaders.populateCollectionThroughputInfo]: true - } + [HttpHeaders.populateCollectionThroughputInfo]: true, + }, }; - const response = await client() - .offer(offerId) - .read(options); + const response = await client().offer(offerId).read(options); return parseSDKOfferResponse(response); }; diff --git a/src/Common/dataAccess/readOffers.ts b/src/Common/dataAccess/readOffers.ts index 4f5bc350b..7205e4ada 100644 --- a/src/Common/dataAccess/readOffers.ts +++ b/src/Common/dataAccess/readOffers.ts @@ -7,9 +7,7 @@ export const readOffers = async (): Promise => { const clearMessage = logConsoleProgress(`Querying offers`); try { - const response = await client() - .offers.readAll() - .fetchAll(); + const response = await client().offers.readAll().fetchAll(); return response?.resources; } catch (error) { // This should be removed when we can correctly identify if an account is serverless when connected using connection string too. diff --git a/src/Common/dataAccess/readStoredProcedures.ts b/src/Common/dataAccess/readStoredProcedures.ts index 75fb3111b..551c141c5 100644 --- a/src/Common/dataAccess/readStoredProcedures.ts +++ b/src/Common/dataAccess/readStoredProcedures.ts @@ -25,7 +25,7 @@ export async function readStoredProcedures( databaseId, collectionId ); - return rpResponse?.value?.map(sproc => sproc.properties?.resource as StoredProcedureDefinition & Resource); + return rpResponse?.value?.map((sproc) => sproc.properties?.resource as StoredProcedureDefinition & Resource); } const response = await client() diff --git a/src/Common/dataAccess/readTriggers.ts b/src/Common/dataAccess/readTriggers.ts index 85a96331f..959db2e6a 100644 --- a/src/Common/dataAccess/readTriggers.ts +++ b/src/Common/dataAccess/readTriggers.ts @@ -25,14 +25,10 @@ export async function readTriggers( databaseId, collectionId ); - return rpResponse?.value?.map(trigger => trigger.properties?.resource as TriggerDefinition & Resource); + return rpResponse?.value?.map((trigger) => trigger.properties?.resource as TriggerDefinition & Resource); } - const response = await client() - .database(databaseId) - .container(collectionId) - .scripts.triggers.readAll() - .fetchAll(); + const response = await client().database(databaseId).container(collectionId).scripts.triggers.readAll().fetchAll(); return response?.resources; } catch (error) { handleError(error, "ReadTriggers", `Failed to query triggers for container ${collectionId}`); diff --git a/src/Common/dataAccess/readUserDefinedFunctions.ts b/src/Common/dataAccess/readUserDefinedFunctions.ts index f6990db7c..032b51034 100644 --- a/src/Common/dataAccess/readUserDefinedFunctions.ts +++ b/src/Common/dataAccess/readUserDefinedFunctions.ts @@ -25,7 +25,7 @@ export async function readUserDefinedFunctions( databaseId, collectionId ); - return rpResponse?.value?.map(udf => udf.properties?.resource as UserDefinedFunctionDefinition & Resource); + return rpResponse?.value?.map((udf) => udf.properties?.resource as UserDefinedFunctionDefinition & Resource); } const response = await client() diff --git a/src/Common/dataAccess/updateCollection.ts b/src/Common/dataAccess/updateCollection.ts index 3a45af694..6c4821951 100644 --- a/src/Common/dataAccess/updateCollection.ts +++ b/src/Common/dataAccess/updateCollection.ts @@ -8,22 +8,22 @@ import { MongoDBCollectionCreateUpdateParameters, MongoDBCollectionResource, SqlContainerCreateUpdateParameters, - SqlContainerResource + SqlContainerResource, } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { RequestOptions } from "@azure/cosmos/dist-esm"; import { client } from "../CosmosClient"; import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { createUpdateCassandraTable, - getCassandraTable + getCassandraTable, } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { createUpdateMongoDBCollection, - getMongoDBCollection + getMongoDBCollection, } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { createUpdateGremlinGraph, - getGremlinGraph + getGremlinGraph, } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { handleError } from "../ErrorHandlingUtils"; @@ -130,8 +130,8 @@ export async function updateMongoDBCollectionThroughRP( const updateParams: MongoDBCollectionCreateUpdateParameters = { properties: { resource: newCollection, - options: updateOptions - } + options: updateOptions, + }, }; const updateResponse = await createUpdateMongoDBCollection( diff --git a/src/Common/dataAccess/updateOffer.ts b/src/Common/dataAccess/updateOffer.ts index 5489ec12d..1351d93a9 100644 --- a/src/Common/dataAccess/updateOffer.ts +++ b/src/Common/dataAccess/updateOffer.ts @@ -17,7 +17,7 @@ import { migrateSqlDatabaseToManualThroughput, migrateSqlContainerToAutoscale, migrateSqlContainerToManualThroughput, - updateSqlContainerThroughput + updateSqlContainerThroughput, } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { updateCassandraKeyspaceThroughput, @@ -25,7 +25,7 @@ import { migrateCassandraKeyspaceToManualThroughput, migrateCassandraTableToAutoscale, migrateCassandraTableToManualThroughput, - updateCassandraTableThroughput + updateCassandraTableThroughput, } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { updateMongoDBDatabaseThroughput, @@ -33,7 +33,7 @@ import { migrateMongoDBDatabaseToManualThroughput, migrateMongoDBCollectionToAutoscale, migrateMongoDBCollectionToManualThroughput, - updateMongoDBCollectionThroughput + updateMongoDBCollectionThroughput, } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { updateGremlinDatabaseThroughput, @@ -41,13 +41,13 @@ import { migrateGremlinDatabaseToManualThroughput, migrateGremlinGraphToAutoscale, migrateGremlinGraphToManualThroughput, - updateGremlinGraphThroughput + updateGremlinGraphThroughput, } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { userContext } from "../../UserContext"; import { migrateTableToAutoscale, migrateTableToManualThroughput, - updateTableThroughput + updateTableThroughput, } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; export const updateOffer = async (params: UpdateOfferParams): Promise => { @@ -110,7 +110,7 @@ const updateCollectionOfferWithARM = async (params: UpdateOfferParams): Promise< return await readCollectionOffer({ collectionId: params.collectionId, databaseId: params.databaseId, - offerId: params.currentOffer.id + offerId: params.currentOffer.id, }); }; @@ -140,7 +140,7 @@ const updateDatabaseOfferWithARM = async (params: UpdateOfferParams): Promise { const body: ThroughputSettingsUpdateParameters = { properties: { - resource: {} - } + resource: {}, + }, }; if (params.autopilotThroughput) { body.properties.resource.autoscaleSettings = { - maxThroughput: params.autopilotThroughput + maxThroughput: params.autopilotThroughput, }; } else { body.properties.resource.throughput = params.manualThroughput; @@ -378,7 +378,7 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise => const newOffer: SDKOfferDefinition = { content: { offerThroughput: undefined, - offerIsRUPerMinuteThroughputEnabled: false + offerIsRUPerMinuteThroughputEnabled: false, }, _etag: undefined, _ts: undefined, @@ -388,12 +388,12 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise => offerResourceId: sdkOfferDefinition.offerResourceId, offerVersion: sdkOfferDefinition.offerVersion, offerType: sdkOfferDefinition.offerType, - resource: sdkOfferDefinition.resource + resource: sdkOfferDefinition.resource, }; if (params.autopilotThroughput) { newOffer.content.offerAutopilotSettings = { - maxThroughput: params.autopilotThroughput + maxThroughput: params.autopilotThroughput, }; } else { newOffer.content.offerThroughput = params.manualThroughput; @@ -402,12 +402,12 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise => const options: RequestOptions = {}; if (params.migrateToAutoPilot) { options.initialHeaders = { - [HttpHeaders.migrateOfferToAutopilot]: "true" + [HttpHeaders.migrateOfferToAutopilot]: "true", }; delete newOffer.content.offerAutopilotSettings; } else if (params.migrateToManual) { options.initialHeaders = { - [HttpHeaders.migrateOfferToManualThroughput]: "true" + [HttpHeaders.migrateOfferToManualThroughput]: "true", }; newOffer.content.offerAutopilotSettings = { maxThroughput: 0 }; } diff --git a/src/Common/dataAccess/updateStoredProcedure.ts b/src/Common/dataAccess/updateStoredProcedure.ts index 4ca900905..8c247c469 100644 --- a/src/Common/dataAccess/updateStoredProcedure.ts +++ b/src/Common/dataAccess/updateStoredProcedure.ts @@ -3,12 +3,12 @@ import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { SqlStoredProcedureCreateUpdateParameters, - SqlStoredProcedureResource + SqlStoredProcedureResource, } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { client } from "../CosmosClient"; import { createUpdateSqlStoredProcedure, - getSqlStoredProcedure + getSqlStoredProcedure, } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { handleError } from "../ErrorHandlingUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; @@ -39,8 +39,8 @@ export async function updateStoredProcedure( const createSprocParams: SqlStoredProcedureCreateUpdateParameters = { properties: { resource: storedProcedure as SqlStoredProcedureResource, - options: {} - } + options: {}, + }, }; const rpResponse = await createUpdateSqlStoredProcedure( userContext.subscriptionId, diff --git a/src/Common/dataAccess/updateTrigger.ts b/src/Common/dataAccess/updateTrigger.ts index 49759bb2f..2a7bd61ce 100644 --- a/src/Common/dataAccess/updateTrigger.ts +++ b/src/Common/dataAccess/updateTrigger.ts @@ -2,7 +2,7 @@ import { AuthType } from "../../AuthType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { SqlTriggerCreateUpdateParameters, - SqlTriggerResource + SqlTriggerResource, } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { TriggerDefinition } from "@azure/cosmos"; import { client } from "../CosmosClient"; @@ -36,8 +36,8 @@ export async function updateTrigger( const createTriggerParams: SqlTriggerCreateUpdateParameters = { properties: { resource: trigger as SqlTriggerResource, - options: {} - } + options: {}, + }, }; const rpResponse = await createUpdateSqlTrigger( userContext.subscriptionId, diff --git a/src/Common/dataAccess/updateUserDefinedFunction.ts b/src/Common/dataAccess/updateUserDefinedFunction.ts index 3e2ebab1b..21f07300f 100644 --- a/src/Common/dataAccess/updateUserDefinedFunction.ts +++ b/src/Common/dataAccess/updateUserDefinedFunction.ts @@ -3,12 +3,12 @@ import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { SqlUserDefinedFunctionCreateUpdateParameters, - SqlUserDefinedFunctionResource + SqlUserDefinedFunctionResource, } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { client } from "../CosmosClient"; import { createUpdateSqlUserDefinedFunction, - getSqlUserDefinedFunction + getSqlUserDefinedFunction, } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { handleError } from "../ErrorHandlingUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; @@ -39,8 +39,8 @@ export async function updateUserDefinedFunction( const createUDFParams: SqlUserDefinedFunctionCreateUpdateParameters = { properties: { resource: userDefinedFunction as SqlUserDefinedFunctionResource, - options: {} - } + options: {}, + }, }; const rpResponse = await createUpdateSqlUserDefinedFunction( userContext.subscriptionId, diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 21f759969..2ca4b9ada 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,7 +1,7 @@ export enum Platform { Portal = "Portal", Hosted = "Hosted", - Emulator = "Emulator" + Emulator = "Emulator", } interface ConfigContext { @@ -37,7 +37,7 @@ let configContext: Readonly = { `^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`, `^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`, - `^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$` + `^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`, ], // Webpack injects this at build time gitSha: process.env.GIT_SHA, @@ -52,7 +52,7 @@ let configContext: Readonly = { ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306 JUNO_ENDPOINT: "https://tools.cosmos.azure.com", - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com" + BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", }; export function resetConfigContext(): void { @@ -73,7 +73,7 @@ if (process.env.NODE_ENV === "development") { BACKEND_ENDPOINT: "https://localhost:" + port, MONGO_BACKEND_ENDPOINT: "https://localhost:" + port, PROXY_PATH: "/proxy", - EMULATOR_ENDPOINT: "https://localhost:8081" + EMULATOR_ENDPOINT: "https://localhost:8081", }); } @@ -86,7 +86,7 @@ export async function initializeConfiguration(): Promise { Object.assign(configContext, externalConfig); if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) { updateConfigContext({ - allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins] + allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins], }); } } catch (error) { diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index cb07e24db..dac45dbe0 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -7,7 +7,7 @@ export enum TabKind { TableEntities, Graph, SQLQuery, - ScaleSettings + ScaleSettings, } /** @@ -20,7 +20,7 @@ export enum PaneKind { DeleteDatabase, GlobalSettings, AdHocAccess, - SwitchDirectory + SwitchDirectory, } /** @@ -79,5 +79,5 @@ export enum ActionType { OpenCollectionTab, OpenPane, TransmitCachedData, - OpenSampleNotebook + OpenSampleNotebook, } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 6c1be4093..39d758ec9 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,589 +1,589 @@ -export interface DatabaseAccount { - id: string; - name: string; - location: string; - type: string; - kind: string; - tags: any; - properties: DatabaseAccountExtendedProperties; -} - -export interface DatabaseAccountExtendedProperties { - documentEndpoint: string; - tableEndpoint: string; - gremlinEndpoint: string; - cassandraEndpoint: string; - configurationOverrides?: ConfigurationOverrides; - capabilities?: Capability[]; - enableMultipleWriteLocations?: boolean; - mongoEndpoint?: string; - readLocations?: DatabaseAccountResponseLocation[]; - writeLocations?: DatabaseAccountResponseLocation[]; - enableFreeTier?: boolean; - enableAnalyticalStorage?: boolean; -} - -export interface DatabaseAccountResponseLocation { - documentEndpoint: string; - failoverPriority: number; - id: string; - locationId: string; - locationName: string; - provisioningState: string; -} - -export interface ConfigurationOverrides { - EnableBsonSchema: string; -} - -export interface Capability { - name: string; - description: string; -} - -export interface AccessInputMetadata { - accountName: string; - apiEndpoint: string; - apiKind: number; - documentEndpoint: string; - expiryTimestamp: string; - mongoEndpoint?: string; -} - -export enum ApiKind { - SQL = 0, - MongoDB, - Table, - Cassandra, - Graph, - MongoDBCompute -} - -export interface GenerateTokenResponse { - readWrite: string; - read: string; -} - -export interface Subscription { - uniqueDisplayName: string; - displayName: string; - subscriptionId: string; - tenantId: string; - state: string; - subscriptionPolicies: SubscriptionPolicies; - authorizationSource: string; -} - -export interface SubscriptionPolicies { - locationPlacementId: string; - quotaId: string; - spendingLimit?: string; -} - -export interface Resource { - _rid: string; - _self: string; - _etag: string; - _ts: number | string; - id: string; -} - -export interface IType { - name: string; - code: number; -} - -export interface IDataField { - dataType: IType; - hasNulls: boolean; - isArray: boolean; - schemaType: IType; - name: string; - path: string; - maxRepetitionLevel: number; - maxDefinitionLevel: number; -} - -export interface ISchema { - id: string; - accountName: string; - resource: string; - fields: IDataField[]; -} - -export interface ISchemaRequest { - id: string; - subscriptionId: string; - resourceGroup: string; - accountName: string; - resource: string; - status: string; -} - -export interface Collection extends Resource { - defaultTtl?: number; - indexingPolicy?: IndexingPolicy; - partitionKey?: PartitionKey; - statistics?: Statistic[]; - uniqueKeyPolicy?: UniqueKeyPolicy; - conflictResolutionPolicy?: ConflictResolutionPolicy; - changeFeedPolicy?: ChangeFeedPolicy; - analyticalStorageTtl?: number; - geospatialConfig?: GeospatialConfig; - schema?: ISchema; - requestSchema?: () => void; -} - -export interface Database extends Resource { - collections?: Collection[]; -} - -export interface DocumentId extends Resource {} - -export interface ConflictId extends Resource { - resourceId?: string; - resourceType?: string; - operationType?: string; - content?: string; -} - -export interface AuthHeaders { - "x-ms-date": string; - authorization: string; -} - -export interface KeyResource { - Token: string; -} - -export interface IndexingPolicy { - automatic: boolean; - indexingMode: string; - includedPaths: any; - excludedPaths: any; - compositeIndexes?: any; - spatialIndexes?: any; -} - -export interface PartitionKey { - paths: string[]; - kind: string; - version: number; - systemKey?: boolean; -} - -export interface Statistic { - documentCount: number; - id: string; - partitionKeys: any[]; - sizeInKB: number; -} - -export interface QueryPreparationTimes { - queryCompilationTime: any; - logicalPlanBuildTime: any; - physicalPlanBuildTime: any; - queryOptimizationTime: any; -} - -export interface RuntimeExecutionTimes { - queryEngineExecutionTime: any; - systemFunctionExecutionTime: any; - userDefinedFunctionExecutionTime: any; -} - -export interface QueryMetrics { - clientSideMetrics: any; - documentLoadTime: any; - documentWriteTime: any; - indexHitDocumentCount: number; - indexLookupTime: any; - outputDocumentCount: number; - outputDocumentSize: number; - queryPreparationTimes: QueryPreparationTimes; - retrievedDocumentCount: number; - retrievedDocumentSize: number; - runtimeExecutionTimes: RuntimeExecutionTimes; - totalQueryExecutionTime: any; - vmExecutionTime: any; -} - -export interface Offer { - id: string; - autoscaleMaxThroughput: number | undefined; - manualThroughput: number | undefined; - minimumThroughput: number | undefined; - offerDefinition?: SDKOfferDefinition; - offerReplacePending: boolean; -} - -export interface SDKOfferDefinition extends Resource { - offerVersion?: string; - offerType?: string; - content?: { - offerThroughput: number; - offerIsRUPerMinuteThroughputEnabled?: boolean; - collectionThroughputInfo?: OfferThroughputInfo; - offerAutopilotSettings?: AutoPilotOfferSettings; - }; - resource?: string; - offerResourceId?: string; -} - -export interface OfferThroughputInfo { - minimumRUForCollection: number; - numPhysicalPartitions: number; -} - -export interface UniqueKeyPolicy { - uniqueKeys: UniqueKey[]; -} - -export interface UniqueKey { - paths: string[]; -} - -export interface CreateDatabaseAndCollectionRequest { - databaseId: string; - collectionId: string; - offerThroughput: number; - databaseLevelThroughput: boolean; - partitionKey?: PartitionKey; - indexingPolicy?: IndexingPolicy; - uniqueKeyPolicy?: UniqueKeyPolicy; - autoPilot?: AutoPilotCreationSettings; - analyticalStorageTtl?: number; -} - -export interface AutoPilotCreationSettings { - maxThroughput?: number; -} - -export interface Query { - id: string; - resourceId: string; - queryName: string; - query: string; -} - -export interface AutoPilotOfferSettings { - maximumTierThroughput?: number; - maxThroughput?: number; - targetMaxThroughput?: number; -} - -export interface CreateDatabaseParams { - autoPilotMaxThroughput?: number; - databaseId: string; - databaseLevelThroughput?: boolean; - offerThroughput?: number; -} - -export interface CreateCollectionParams { - createNewDatabase: boolean; - collectionId: string; - databaseId: string; - databaseLevelThroughput: boolean; - offerThroughput: number; - analyticalStorageTtl?: number; - autoPilotMaxThroughput?: number; - indexingPolicy?: IndexingPolicy; - partitionKey?: PartitionKey; - uniqueKeyPolicy?: UniqueKeyPolicy; - createMongoWildcardIndex?: boolean; -} - -export interface ReadDatabaseOfferParams { - databaseId: string; - databaseResourceId?: string; - offerId?: string; -} - -export interface ReadCollectionOfferParams { - collectionId: string; - databaseId: string; - collectionResourceId?: string; - offerId?: string; -} - -export interface UpdateOfferParams { - currentOffer: Offer; - databaseId: string; - autopilotThroughput: number; - manualThroughput: number; - collectionId?: string; - migrateToAutoPilot?: boolean; - migrateToManual?: boolean; -} - -export interface Notification { - id: string; - kind: string; - accountName: string; - action: any; - buttonValue?: any; - collectionName?: string; - databaseName?: string; - description: string; - endDateUtc: number; - seenAtUtc: number; - state: string; - type: string; - updatedAtUtc: string; -} - -export enum ConflictResolutionMode { - Custom = "Custom", - LastWriterWins = "LastWriterWins" -} - -/** - * Represents the conflict resolution policy configuration for specifying how to resolve conflicts - * in case writes from different regions result in conflicts on documents in the collection in the Azure Cosmos DB service. - */ -export interface ConflictResolutionPolicy { - /** - * Gets or sets the ConflictResolutionMode in the Azure Cosmos DB service. By default it is ConflictResolutionMode.LastWriterWins. - */ - mode?: keyof typeof ConflictResolutionMode; - /** - * Gets or sets the path which is present in each document in the Azure Cosmos DB service for last writer wins conflict-resolution. - * This path must be present in each document and must be an integer value. - * In case of a conflict occuring on a document, the document with the higher integer value in the specified path will be picked. - * If the path is unspecified, by default the timestamp path will be used. - * - * This value should only be set when using ConflictResolutionMode.LastWriterWins. - * - * ```typescript - * conflictResolutionPolicy.ConflictResolutionPath = "/name/first"; - * ``` - * - */ - conflictResolutionPath?: string; - /** - * Gets or sets the StoredProcedure which is used for conflict resolution in the Azure Cosmos DB service. - * This stored procedure may be created after the Container is created and can be changed as required. - * - * 1. This value should only be set when using ConflictResolutionMode.Custom. - * 2. In case the stored procedure fails or throws an exception, the conflict resolution will default to registering conflicts in the conflicts feed. - * - * ```typescript - * conflictResolutionPolicy.ConflictResolutionProcedure = "resolveConflict" - * ``` - */ - conflictResolutionProcedure?: string; -} - -export interface ChangeFeedPolicy { - retentionDuration: number; -} - -export interface GeospatialConfig { - type: string; -} - -export interface GatewayDatabaseAccount { - MediaLink: string; - DatabasesLink: string; - MaxMediaStorageUsageInMB: number; - CurrentMediaStorageUsageInMB: number; - EnableMultipleWriteLocations?: boolean; - WritableLocations: RegionEndpoint[]; - ReadableLocations: RegionEndpoint[]; -} - -export interface RegionEndpoint { - name: string; - documentAccountEndpoint: string; -} - -export interface Tenant { - displayName: string; - id: string; - tenantId: string; - countryCode: string; - domains: Array; -} - -export interface AccountKeys { - primaryMasterKey: string; - secondaryMasterKey: string; - primaryReadonlyMasterKey: string; - secondaryReadonlyMasterKey: string; -} - -export interface AfecFeature { - id: string; - name: string; - properties: { state: string }; - type: string; -} - -export interface OperationStatus { - status: string; - id?: string; - name?: string; // operationId - properties?: any; - error?: { code: string; message: string }; -} - -export interface NotebookWorkspaceConnectionInfo { - authToken: string; - notebookServerEndpoint: string; -} - -export interface NotebookWorkspaceFeedResponse { - value: NotebookWorkspace[]; -} - -export interface NotebookWorkspace { - id: string; - name: string; - properties: { - status: string; - notebookServerEndpoint: string; - }; -} - -export interface NotebookConfigurationEndpoints { - path: string; - endpoints: NotebookConfigurationEndpointInfo[]; -} - -export interface NotebookConfigurationEndpointInfo { - type: string; - endpoint: string; - username: string; - password: string; - token: string; -} - -export interface SparkClusterConnectionInfo { - userName: string; - password: string; - endpoints: SparkClusterEndpoint[]; -} - -export interface SparkClusterEndpoint { - kind: SparkClusterEndpointKind; - endpoint: string; -} - -export enum SparkClusterEndpointKind { - SparkUI = "SparkUI", - HistoryServerUI = "HistoryServerUI", - Livy = "Livy" -} - -export interface RpParameters { - db: string; - offerThroughput?: number; - st: Boolean; - sid: string; - rg: string; - dba: string; - partitionKeyVersion?: number; -} - -export interface MongoParameters extends RpParameters { - pk?: string; - resourceUrl?: string; - coll?: string; - cd?: Boolean; - is?: Boolean; - rid?: string; - rtype?: string; - isAutoPilot?: Boolean; - autoPilotThroughput?: string; - analyticalStorageTtl?: number; -} - -export interface SparkClusterLibrary { - name: string; -} - -export interface Library extends SparkClusterLibrary { - properties: { - kind: "Jar"; - source: { - kind: "HttpsUri"; - uri: string; - libraryFileName: string; - }; - }; -} - -export interface LibraryFeedResponse { - value: Library[]; -} - -export interface ArmResource { - id: string; - location: string; - name: string; - type: string; - tags: { [key: string]: string }; -} - -export interface ArcadiaWorkspaceIdentity { - type: string; - principalId: string; - tenantId: string; -} - -export interface ArcadiaWorkspaceProperties { - managedResourceGroupName: string; - provisioningState: string; - sqlAdministratorLogin: string; - connectivityEndpoints: { - artifacts: string; - dev: string; - spark: string; - sql: string; - web: string; - }; - defaultDataLakeStorage: { - accountUrl: string; - filesystem: string; - }; -} - -export interface ArcadiaWorkspaceFeedResponse { - value: ArcadiaWorkspace[]; -} - -export interface ArcadiaWorkspace extends ArmResource { - identity: ArcadiaWorkspaceIdentity; - properties: ArcadiaWorkspaceProperties; -} - -export interface SparkPoolFeedResponse { - value: SparkPool[]; -} - -export interface SparkPoolProperties { - creationDate: string; - sparkVersion: string; - nodeCount: number; - nodeSize: string; - nodeSizeFamily: string; - provisioningState: string; - autoScale: { - enabled: boolean; - minNodeCount: number; - maxNodeCount: number; - }; - autoPause: { - enabled: boolean; - delayInMinutes: number; - }; -} - -export interface SparkPool extends ArmResource { - properties: SparkPoolProperties; -} - -export interface MemoryUsageInfo { - freeKB: number; - totalKB: number; -} +export interface DatabaseAccount { + id: string; + name: string; + location: string; + type: string; + kind: string; + tags: any; + properties: DatabaseAccountExtendedProperties; +} + +export interface DatabaseAccountExtendedProperties { + documentEndpoint: string; + tableEndpoint: string; + gremlinEndpoint: string; + cassandraEndpoint: string; + configurationOverrides?: ConfigurationOverrides; + capabilities?: Capability[]; + enableMultipleWriteLocations?: boolean; + mongoEndpoint?: string; + readLocations?: DatabaseAccountResponseLocation[]; + writeLocations?: DatabaseAccountResponseLocation[]; + enableFreeTier?: boolean; + enableAnalyticalStorage?: boolean; +} + +export interface DatabaseAccountResponseLocation { + documentEndpoint: string; + failoverPriority: number; + id: string; + locationId: string; + locationName: string; + provisioningState: string; +} + +export interface ConfigurationOverrides { + EnableBsonSchema: string; +} + +export interface Capability { + name: string; + description: string; +} + +export interface AccessInputMetadata { + accountName: string; + apiEndpoint: string; + apiKind: number; + documentEndpoint: string; + expiryTimestamp: string; + mongoEndpoint?: string; +} + +export enum ApiKind { + SQL = 0, + MongoDB, + Table, + Cassandra, + Graph, + MongoDBCompute, +} + +export interface GenerateTokenResponse { + readWrite: string; + read: string; +} + +export interface Subscription { + uniqueDisplayName: string; + displayName: string; + subscriptionId: string; + tenantId: string; + state: string; + subscriptionPolicies: SubscriptionPolicies; + authorizationSource: string; +} + +export interface SubscriptionPolicies { + locationPlacementId: string; + quotaId: string; + spendingLimit?: string; +} + +export interface Resource { + _rid: string; + _self: string; + _etag: string; + _ts: number | string; + id: string; +} + +export interface IType { + name: string; + code: number; +} + +export interface IDataField { + dataType: IType; + hasNulls: boolean; + isArray: boolean; + schemaType: IType; + name: string; + path: string; + maxRepetitionLevel: number; + maxDefinitionLevel: number; +} + +export interface ISchema { + id: string; + accountName: string; + resource: string; + fields: IDataField[]; +} + +export interface ISchemaRequest { + id: string; + subscriptionId: string; + resourceGroup: string; + accountName: string; + resource: string; + status: string; +} + +export interface Collection extends Resource { + defaultTtl?: number; + indexingPolicy?: IndexingPolicy; + partitionKey?: PartitionKey; + statistics?: Statistic[]; + uniqueKeyPolicy?: UniqueKeyPolicy; + conflictResolutionPolicy?: ConflictResolutionPolicy; + changeFeedPolicy?: ChangeFeedPolicy; + analyticalStorageTtl?: number; + geospatialConfig?: GeospatialConfig; + schema?: ISchema; + requestSchema?: () => void; +} + +export interface Database extends Resource { + collections?: Collection[]; +} + +export interface DocumentId extends Resource {} + +export interface ConflictId extends Resource { + resourceId?: string; + resourceType?: string; + operationType?: string; + content?: string; +} + +export interface AuthHeaders { + "x-ms-date": string; + authorization: string; +} + +export interface KeyResource { + Token: string; +} + +export interface IndexingPolicy { + automatic: boolean; + indexingMode: string; + includedPaths: any; + excludedPaths: any; + compositeIndexes?: any; + spatialIndexes?: any; +} + +export interface PartitionKey { + paths: string[]; + kind: string; + version: number; + systemKey?: boolean; +} + +export interface Statistic { + documentCount: number; + id: string; + partitionKeys: any[]; + sizeInKB: number; +} + +export interface QueryPreparationTimes { + queryCompilationTime: any; + logicalPlanBuildTime: any; + physicalPlanBuildTime: any; + queryOptimizationTime: any; +} + +export interface RuntimeExecutionTimes { + queryEngineExecutionTime: any; + systemFunctionExecutionTime: any; + userDefinedFunctionExecutionTime: any; +} + +export interface QueryMetrics { + clientSideMetrics: any; + documentLoadTime: any; + documentWriteTime: any; + indexHitDocumentCount: number; + indexLookupTime: any; + outputDocumentCount: number; + outputDocumentSize: number; + queryPreparationTimes: QueryPreparationTimes; + retrievedDocumentCount: number; + retrievedDocumentSize: number; + runtimeExecutionTimes: RuntimeExecutionTimes; + totalQueryExecutionTime: any; + vmExecutionTime: any; +} + +export interface Offer { + id: string; + autoscaleMaxThroughput: number | undefined; + manualThroughput: number | undefined; + minimumThroughput: number | undefined; + offerDefinition?: SDKOfferDefinition; + offerReplacePending: boolean; +} + +export interface SDKOfferDefinition extends Resource { + offerVersion?: string; + offerType?: string; + content?: { + offerThroughput: number; + offerIsRUPerMinuteThroughputEnabled?: boolean; + collectionThroughputInfo?: OfferThroughputInfo; + offerAutopilotSettings?: AutoPilotOfferSettings; + }; + resource?: string; + offerResourceId?: string; +} + +export interface OfferThroughputInfo { + minimumRUForCollection: number; + numPhysicalPartitions: number; +} + +export interface UniqueKeyPolicy { + uniqueKeys: UniqueKey[]; +} + +export interface UniqueKey { + paths: string[]; +} + +export interface CreateDatabaseAndCollectionRequest { + databaseId: string; + collectionId: string; + offerThroughput: number; + databaseLevelThroughput: boolean; + partitionKey?: PartitionKey; + indexingPolicy?: IndexingPolicy; + uniqueKeyPolicy?: UniqueKeyPolicy; + autoPilot?: AutoPilotCreationSettings; + analyticalStorageTtl?: number; +} + +export interface AutoPilotCreationSettings { + maxThroughput?: number; +} + +export interface Query { + id: string; + resourceId: string; + queryName: string; + query: string; +} + +export interface AutoPilotOfferSettings { + maximumTierThroughput?: number; + maxThroughput?: number; + targetMaxThroughput?: number; +} + +export interface CreateDatabaseParams { + autoPilotMaxThroughput?: number; + databaseId: string; + databaseLevelThroughput?: boolean; + offerThroughput?: number; +} + +export interface CreateCollectionParams { + createNewDatabase: boolean; + collectionId: string; + databaseId: string; + databaseLevelThroughput: boolean; + offerThroughput: number; + analyticalStorageTtl?: number; + autoPilotMaxThroughput?: number; + indexingPolicy?: IndexingPolicy; + partitionKey?: PartitionKey; + uniqueKeyPolicy?: UniqueKeyPolicy; + createMongoWildcardIndex?: boolean; +} + +export interface ReadDatabaseOfferParams { + databaseId: string; + databaseResourceId?: string; + offerId?: string; +} + +export interface ReadCollectionOfferParams { + collectionId: string; + databaseId: string; + collectionResourceId?: string; + offerId?: string; +} + +export interface UpdateOfferParams { + currentOffer: Offer; + databaseId: string; + autopilotThroughput: number; + manualThroughput: number; + collectionId?: string; + migrateToAutoPilot?: boolean; + migrateToManual?: boolean; +} + +export interface Notification { + id: string; + kind: string; + accountName: string; + action: any; + buttonValue?: any; + collectionName?: string; + databaseName?: string; + description: string; + endDateUtc: number; + seenAtUtc: number; + state: string; + type: string; + updatedAtUtc: string; +} + +export enum ConflictResolutionMode { + Custom = "Custom", + LastWriterWins = "LastWriterWins", +} + +/** + * Represents the conflict resolution policy configuration for specifying how to resolve conflicts + * in case writes from different regions result in conflicts on documents in the collection in the Azure Cosmos DB service. + */ +export interface ConflictResolutionPolicy { + /** + * Gets or sets the ConflictResolutionMode in the Azure Cosmos DB service. By default it is ConflictResolutionMode.LastWriterWins. + */ + mode?: keyof typeof ConflictResolutionMode; + /** + * Gets or sets the path which is present in each document in the Azure Cosmos DB service for last writer wins conflict-resolution. + * This path must be present in each document and must be an integer value. + * In case of a conflict occuring on a document, the document with the higher integer value in the specified path will be picked. + * If the path is unspecified, by default the timestamp path will be used. + * + * This value should only be set when using ConflictResolutionMode.LastWriterWins. + * + * ```typescript + * conflictResolutionPolicy.ConflictResolutionPath = "/name/first"; + * ``` + * + */ + conflictResolutionPath?: string; + /** + * Gets or sets the StoredProcedure which is used for conflict resolution in the Azure Cosmos DB service. + * This stored procedure may be created after the Container is created and can be changed as required. + * + * 1. This value should only be set when using ConflictResolutionMode.Custom. + * 2. In case the stored procedure fails or throws an exception, the conflict resolution will default to registering conflicts in the conflicts feed. + * + * ```typescript + * conflictResolutionPolicy.ConflictResolutionProcedure = "resolveConflict" + * ``` + */ + conflictResolutionProcedure?: string; +} + +export interface ChangeFeedPolicy { + retentionDuration: number; +} + +export interface GeospatialConfig { + type: string; +} + +export interface GatewayDatabaseAccount { + MediaLink: string; + DatabasesLink: string; + MaxMediaStorageUsageInMB: number; + CurrentMediaStorageUsageInMB: number; + EnableMultipleWriteLocations?: boolean; + WritableLocations: RegionEndpoint[]; + ReadableLocations: RegionEndpoint[]; +} + +export interface RegionEndpoint { + name: string; + documentAccountEndpoint: string; +} + +export interface Tenant { + displayName: string; + id: string; + tenantId: string; + countryCode: string; + domains: Array; +} + +export interface AccountKeys { + primaryMasterKey: string; + secondaryMasterKey: string; + primaryReadonlyMasterKey: string; + secondaryReadonlyMasterKey: string; +} + +export interface AfecFeature { + id: string; + name: string; + properties: { state: string }; + type: string; +} + +export interface OperationStatus { + status: string; + id?: string; + name?: string; // operationId + properties?: any; + error?: { code: string; message: string }; +} + +export interface NotebookWorkspaceConnectionInfo { + authToken: string; + notebookServerEndpoint: string; +} + +export interface NotebookWorkspaceFeedResponse { + value: NotebookWorkspace[]; +} + +export interface NotebookWorkspace { + id: string; + name: string; + properties: { + status: string; + notebookServerEndpoint: string; + }; +} + +export interface NotebookConfigurationEndpoints { + path: string; + endpoints: NotebookConfigurationEndpointInfo[]; +} + +export interface NotebookConfigurationEndpointInfo { + type: string; + endpoint: string; + username: string; + password: string; + token: string; +} + +export interface SparkClusterConnectionInfo { + userName: string; + password: string; + endpoints: SparkClusterEndpoint[]; +} + +export interface SparkClusterEndpoint { + kind: SparkClusterEndpointKind; + endpoint: string; +} + +export enum SparkClusterEndpointKind { + SparkUI = "SparkUI", + HistoryServerUI = "HistoryServerUI", + Livy = "Livy", +} + +export interface RpParameters { + db: string; + offerThroughput?: number; + st: Boolean; + sid: string; + rg: string; + dba: string; + partitionKeyVersion?: number; +} + +export interface MongoParameters extends RpParameters { + pk?: string; + resourceUrl?: string; + coll?: string; + cd?: Boolean; + is?: Boolean; + rid?: string; + rtype?: string; + isAutoPilot?: Boolean; + autoPilotThroughput?: string; + analyticalStorageTtl?: number; +} + +export interface SparkClusterLibrary { + name: string; +} + +export interface Library extends SparkClusterLibrary { + properties: { + kind: "Jar"; + source: { + kind: "HttpsUri"; + uri: string; + libraryFileName: string; + }; + }; +} + +export interface LibraryFeedResponse { + value: Library[]; +} + +export interface ArmResource { + id: string; + location: string; + name: string; + type: string; + tags: { [key: string]: string }; +} + +export interface ArcadiaWorkspaceIdentity { + type: string; + principalId: string; + tenantId: string; +} + +export interface ArcadiaWorkspaceProperties { + managedResourceGroupName: string; + provisioningState: string; + sqlAdministratorLogin: string; + connectivityEndpoints: { + artifacts: string; + dev: string; + spark: string; + sql: string; + web: string; + }; + defaultDataLakeStorage: { + accountUrl: string; + filesystem: string; + }; +} + +export interface ArcadiaWorkspaceFeedResponse { + value: ArcadiaWorkspace[]; +} + +export interface ArcadiaWorkspace extends ArmResource { + identity: ArcadiaWorkspaceIdentity; + properties: ArcadiaWorkspaceProperties; +} + +export interface SparkPoolFeedResponse { + value: SparkPool[]; +} + +export interface SparkPoolProperties { + creationDate: string; + sparkVersion: string; + nodeCount: number; + nodeSize: string; + nodeSizeFamily: string; + provisioningState: string; + autoScale: { + enabled: boolean; + minNodeCount: number; + maxNodeCount: number; + }; + autoPause: { + enabled: boolean; + delayInMinutes: number; + }; +} + +export interface SparkPool extends ArmResource { + properties: SparkPoolProperties; +} + +export interface MemoryUsageInfo { + freeKB: number; + totalKB: number; +} diff --git a/src/Contracts/Diagnostics.ts b/src/Contracts/Diagnostics.ts index d6a54740a..cb601e0e6 100644 --- a/src/Contracts/Diagnostics.ts +++ b/src/Contracts/Diagnostics.ts @@ -21,7 +21,7 @@ export enum LogEntryLevel { /** * Error level. */ - Error = 2 + Error = 2, } /** * Schema of a log entry. diff --git a/src/Contracts/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts index 0a7807297..e9ed8a322 100644 --- a/src/Contracts/ExplorerContracts.ts +++ b/src/Contracts/ExplorerContracts.ts @@ -1,39 +1,39 @@ -import * as Versions from "./Versions"; -import * as ActionContracts from "./ActionContracts"; -import * as Diagnostics from "./Diagnostics"; - -/** - * Messaging types used with Data Explorer <-> Portal communication - * and Hosted <-> Explorer communication - */ -export enum MessageTypes { - TelemetryInfo, - LogInfo, - RefreshResources, - AllDatabases, - CollectionsForDatabase, - RefreshOffers, - AllOffers, - UpdateLocationHash, - SingleOffer, - RefreshOffer, - UpdateAccountName, - ForbiddenError, - AadSignIn, - GetAccessAadRequest, - GetAccessAadResponse, - UpdateAccountSwitch, - UpdateDirectoryControl, - SwitchAccount, - SendNotification, - ClearNotification, - ExplorerClickEvent, - LoadingStatus, - GetArcadiaToken, - CreateWorkspace, - CreateSparkPool, - RefreshDatabaseAccount, - InitTestExplorer -} - -export { Versions, ActionContracts, Diagnostics }; +import * as Versions from "./Versions"; +import * as ActionContracts from "./ActionContracts"; +import * as Diagnostics from "./Diagnostics"; + +/** + * Messaging types used with Data Explorer <-> Portal communication + * and Hosted <-> Explorer communication + */ +export enum MessageTypes { + TelemetryInfo, + LogInfo, + RefreshResources, + AllDatabases, + CollectionsForDatabase, + RefreshOffers, + AllOffers, + UpdateLocationHash, + SingleOffer, + RefreshOffer, + UpdateAccountName, + ForbiddenError, + AadSignIn, + GetAccessAadRequest, + GetAccessAadResponse, + UpdateAccountSwitch, + UpdateDirectoryControl, + SwitchAccount, + SendNotification, + ClearNotification, + ExplorerClickEvent, + LoadingStatus, + GetArcadiaToken, + CreateWorkspace, + CreateSparkPool, + RefreshDatabaseAccount, + InitTestExplorer, +} + +export { Versions, ActionContracts, Diagnostics }; diff --git a/src/Contracts/SubscriptionType.ts b/src/Contracts/SubscriptionType.ts index 9768dd835..244d6dab5 100644 --- a/src/Contracts/SubscriptionType.ts +++ b/src/Contracts/SubscriptionType.ts @@ -3,5 +3,5 @@ export enum SubscriptionType { EA, Free, Internal, - PAYG + PAYG, } diff --git a/src/Contracts/Versions.ts b/src/Contracts/Versions.ts index fd23c2c1c..9cb767bc9 100644 --- a/src/Contracts/Versions.ts +++ b/src/Contracts/Versions.ts @@ -1,4 +1,4 @@ -/** - * Data Explorer version {major.minor.patch} - */ -export const DataExplorer: string = "1.0.1"; +/** + * Data Explorer version {major.minor.patch} + */ +export const DataExplorer: string = "1.0.1"; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 604b7af11..7e8a16688 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -1,440 +1,440 @@ -import { - QueryMetrics, - Resource, - StoredProcedureDefinition, - TriggerDefinition, - UserDefinedFunctionDefinition -} from "@azure/cosmos"; -import Q from "q"; -import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent"; -import Explorer from "../Explorer/Explorer"; -import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient"; -import ConflictId from "../Explorer/Tree/ConflictId"; -import DocumentId from "../Explorer/Tree/DocumentId"; -import StoredProcedure from "../Explorer/Tree/StoredProcedure"; -import Trigger from "../Explorer/Tree/Trigger"; -import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; -import { SelfServeType } from "../SelfServe/SelfServeUtils"; -import { UploadDetails } from "../workers/upload/definitions"; -import * as DataModels from "./DataModels"; -import { SubscriptionType } from "./SubscriptionType"; - -export interface TokenProvider { - getAuthHeader(): Promise; -} - -export interface QueryResultsMetadata { - hasMoreResults: boolean; - firstItemIndex: number; - lastItemIndex: number; - itemCount: number; -} - -export interface QueryResults extends QueryResultsMetadata { - documents: any[]; - activityId: string; - requestCharge: number; - roundTrips?: number; - headers?: any; - queryMetrics?: QueryMetrics; -} - -export interface Button { - visible: ko.Computed; - enabled: ko.Computed; - isSelected?: ko.Computed; -} - -export interface NotificationConsole { - filteredConsoleData: ko.ObservableArray; - isConsoleExpanded: ko.Observable; - - expandConsole(source: any, evt: MouseEvent): void; - collapseConsole(source: any, evt: MouseEvent): void; -} - -export interface WaitsForTemplate { - isTemplateReady: ko.Observable; -} - -export interface TreeNode { - nodeKind: string; - rid: string; - id: ko.Observable; - database?: Database; - collection?: Collection; - - onNewQueryClick?(source: any, event: MouseEvent): void; - onNewStoredProcedureClick?(source: Collection, event: MouseEvent): void; - onNewUserDefinedFunctionClick?(source: Collection, event: MouseEvent): void; - onNewTriggerClick?(source: Collection, event: MouseEvent): void; -} - -export interface Database extends TreeNode { - container: Explorer; - self: string; - id: ko.Observable; - collections: ko.ObservableArray; - offer: ko.Observable; - isDatabaseExpanded: ko.Observable; - isDatabaseShared: ko.Computed; - - selectedSubnodeKind: ko.Observable; - - selectDatabase(): void; - expandDatabase(): Promise; - collapseDatabase(): void; - - loadCollections(): Promise; - findCollectionWithId(collectionId: string): Collection; - openAddCollection(database: Database, event: MouseEvent): void; - onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void; - onSettingsClick: () => void; - loadOffer(): Promise; -} - -export interface CollectionBase extends TreeNode { - container: Explorer; - databaseId: string; - self: string; - rawDataModel: DataModels.Collection; - partitionKey: DataModels.PartitionKey; - partitionKeyProperty: string; - partitionKeyPropertyHeader: string; - id: ko.Observable; - selectedSubnodeKind: ko.Observable; - children: ko.ObservableArray; - isCollectionExpanded: ko.Observable; - - onDocumentDBDocumentsClick(): void; - onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void; - expandCollection(): Q.Promise; - collapseCollection(): void; - getDatabase(): Database; -} - -export interface Collection extends CollectionBase { - defaultTtl: ko.Observable; - analyticalStorageTtl: ko.Observable; - schema?: DataModels.ISchema; - requestSchema?: () => void; - indexingPolicy: ko.Observable; - uniqueKeyPolicy: DataModels.UniqueKeyPolicy; - usageSizeInKB: ko.Observable; - offer: ko.Observable; - conflictResolutionPolicy: ko.Observable; - changeFeedPolicy: ko.Observable; - geospatialConfig: ko.Observable; - documentIds: ko.ObservableArray; - - cassandraKeys: CassandraTableKeys; - cassandraSchema: CassandraTableKey[]; - - onConflictsClick(): void; - onTableEntitiesClick(): void; - onGraphDocumentsClick(): void; - onMongoDBDocumentsClick(): void; - openTab(): void; - - onSettingsClick: () => Promise; - onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void; - - onNewGraphClick(): void; - onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string): void; - onNewMongoShellClick(): void; - onNewStoredProcedureClick(source: Collection, event: MouseEvent): void; - onNewUserDefinedFunctionClick(source: Collection, event: MouseEvent): void; - onNewTriggerClick(source: Collection, event: MouseEvent): void; - storedProcedures: ko.Computed; - userDefinedFunctions: ko.Computed; - triggers: ko.Computed; - - isStoredProceduresExpanded: ko.Observable; - isTriggersExpanded: ko.Observable; - isUserDefinedFunctionsExpanded: ko.Observable; - - expandStoredProcedures(): void; - expandUserDefinedFunctions(): void; - expandTriggers(): void; - - collapseStoredProcedures(): void; - collapseUserDefinedFunctions(): void; - collapseTriggers(): void; - - loadUserDefinedFunctions(): Promise; - loadStoredProcedures(): Promise; - loadTriggers(): Promise; - loadOffer(): Promise; - - createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure; - createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction; - createTriggerNode(data: TriggerDefinition & Resource): Trigger; - findStoredProcedureWithId(sprocRid: string): StoredProcedure; - findTriggerWithId(triggerRid: string): Trigger; - findUserDefinedFunctionWithId(udfRid: string): UserDefinedFunction; - - onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; - onDrop(source: Collection, event: { originalEvent: DragEvent }): void; - uploadFiles(fileList: FileList): Q.Promise; - - getLabel(): string; -} - -/** - * Options used to initialize pane - */ -export interface PaneOptions { - id: string; - visible: ko.Observable; - container?: Explorer; -} - -/** - * Graph configuration - */ -export enum NeighborType { - SOURCES_ONLY, - TARGETS_ONLY, - BOTH -} - -/** - * Set of observable related to graph configuration by user - */ -export interface GraphConfigUiData { - showNeighborType: ko.Observable; - nodeProperties: ko.ObservableArray; - nodePropertiesWithNone: ko.ObservableArray; - nodeCaptionChoice: ko.Observable; - nodeColorKeyChoice: ko.Observable; - nodeIconChoice: ko.Observable; - nodeIconSet: ko.Observable; -} - -/** - * User input for creating new vertex - */ -export interface NewVertexData { - label: string; - properties: InputProperty[]; -} - -export type GremlinPropertyValueType = string | boolean | number | null | undefined; -export type InputPropertyValueTypeString = "string" | "number" | "boolean" | "null"; -export interface InputPropertyValue { - value: GremlinPropertyValueType; - type: InputPropertyValueTypeString; -} -/** - * Property input by user - */ -export interface InputProperty { - key: string; - values: InputPropertyValue[]; -} - -export interface Editable extends ko.Observable { - setBaseline(baseline: T): void; - - editableIsDirty: ko.Computed; - editableIsValid: ko.Observable; - getEditableCurrentValue?: ko.Computed; - getEditableOriginalValue?: ko.Computed; - edits?: ko.ObservableArray; - validations?: ko.ObservableArray<(value: T) => boolean>; -} - -export interface QueryError { - message: string; - start: string; - end: string; - code: string; - severity: string; -} - -export interface DocumentRequestContainer { - self: string; - rid?: string; - resourceName?: string; -} - -export interface DocumentClientOption { - endpoint?: string; - masterKey?: string; - requestTimeoutMs?: number; -} - -// Tab options -export interface TabOptions { - tabKind: CollectionTabKind; - title: string; - tabPath: string; - isActive: ko.Observable; - hashLocation: string; - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void; - isTabsContentExpanded?: ko.Observable; - onLoadStartKey?: number; - - // TODO Remove the flag and use a context to handle this - // TODO: 145357 Remove dependency on collection/database and add abstraction - collection?: CollectionBase; - database?: Database; - rid?: string; - node?: TreeNode; - theme?: string; -} - -export interface DocumentsTabOptions extends TabOptions { - partitionKey: DataModels.PartitionKey; - documentIds: ko.ObservableArray; - container?: Explorer; - isPreferredApiMongoDB?: boolean; - resourceTokenPartitionKey?: string; -} - -export interface SettingsTabV2Options extends TabOptions { - getPendingNotification: Q.Promise; -} - -export interface ConflictsTabOptions extends TabOptions { - partitionKey: DataModels.PartitionKey; - conflictIds: ko.ObservableArray; - container?: Explorer; -} - -export interface QueryTabOptions extends TabOptions { - partitionKey?: DataModels.PartitionKey; - queryText?: string; - resourceTokenPartitionKey?: string; -} - -export interface ScriptTabOption extends TabOptions { - resource: any; - isNew: boolean; - partitionKey?: DataModels.PartitionKey; -} - -export interface EditorPosition { - line: number; - column: number; -} - -export enum DocumentExplorerState { - noDocumentSelected, - newDocumentValid, - newDocumentInvalid, - exisitingDocumentNoEdits, - exisitingDocumentDirtyValid, - exisitingDocumentDirtyInvalid -} - -export enum IndexingPolicyEditorState { - noCollectionSelected, - noEdits, - dirtyValid, - dirtyInvalid -} - -export enum ScriptEditorState { - newInvalid, - newValid, - exisitingNoEdits, - exisitingDirtyValid, - exisitingDirtyInvalid -} - -export enum CollectionTabKind { - Documents = 0, - Settings = 1, - StoredProcedures = 2, - UserDefinedFunctions = 3, - Triggers = 4, - Query = 5, - Graph = 6, - QueryTables = 9, - MongoShell = 10, - DatabaseSettings = 11, - Conflicts = 12, - Notebook = 13 /* Deprecated */, - Terminal = 14, - NotebookV2 = 15, - SparkMasterTab = 16, - Gallery = 17, - NotebookViewer = 18, - Schema = 19, - SettingsV2 = 20 -} - -export enum TerminalKind { - Default = 0, - Mongo = 1, - Cassandra = 2 -} - -export interface DataExplorerInputsFrame { - databaseAccount: any; - subscriptionId: string; - resourceGroup: string; - masterKey: string; - hasWriteAccess: boolean; - authorizationToken: string; - features: any; - csmEndpoint: string; - dnsSuffix: string; - serverId: string; - extensionEndpoint: string; - subscriptionType: SubscriptionType; - quotaId: string; - addCollectionDefaultFlight: string; - isTryCosmosDBSubscription: boolean; - loadDatabaseAccountTimestamp?: number; - sharedThroughputMinimum?: number; - sharedThroughputMaximum?: number; - sharedThroughputDefault?: number; - dataExplorerVersion?: string; - isAuthWithresourceToken?: boolean; - defaultCollectionThroughput?: CollectionCreationDefaults; - flights?: readonly string[]; - selfServeType?: SelfServeType; -} - -export interface CollectionCreationDefaults { - storage: string; - throughput: ThroughputDefaults; -} - -export interface ThroughputDefaults { - fixed: number; - unlimited: - | number - | { - collectionThreshold: number; - lessThanOrEqualToThreshold: number; - greatThanThreshold: number; - }; - unlimitedmax: number; - unlimitedmin: number; - shared: number; -} - -export class MonacoEditorSettings { - public readonly language: string; - public readonly readOnly: boolean; - - constructor(supportedLanguage: string, isReadOnly: boolean) { - this.language = supportedLanguage; - this.readOnly = isReadOnly; - } -} - -export interface AuthorizationTokenHeaderMetadata { - header: string; - token: string; -} - -export interface DropdownOption { - text: string; - value: T; - disable?: boolean; -} +import { + QueryMetrics, + Resource, + StoredProcedureDefinition, + TriggerDefinition, + UserDefinedFunctionDefinition, +} from "@azure/cosmos"; +import Q from "q"; +import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent"; +import Explorer from "../Explorer/Explorer"; +import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient"; +import ConflictId from "../Explorer/Tree/ConflictId"; +import DocumentId from "../Explorer/Tree/DocumentId"; +import StoredProcedure from "../Explorer/Tree/StoredProcedure"; +import Trigger from "../Explorer/Tree/Trigger"; +import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; +import { SelfServeType } from "../SelfServe/SelfServeUtils"; +import { UploadDetails } from "../workers/upload/definitions"; +import * as DataModels from "./DataModels"; +import { SubscriptionType } from "./SubscriptionType"; + +export interface TokenProvider { + getAuthHeader(): Promise; +} + +export interface QueryResultsMetadata { + hasMoreResults: boolean; + firstItemIndex: number; + lastItemIndex: number; + itemCount: number; +} + +export interface QueryResults extends QueryResultsMetadata { + documents: any[]; + activityId: string; + requestCharge: number; + roundTrips?: number; + headers?: any; + queryMetrics?: QueryMetrics; +} + +export interface Button { + visible: ko.Computed; + enabled: ko.Computed; + isSelected?: ko.Computed; +} + +export interface NotificationConsole { + filteredConsoleData: ko.ObservableArray; + isConsoleExpanded: ko.Observable; + + expandConsole(source: any, evt: MouseEvent): void; + collapseConsole(source: any, evt: MouseEvent): void; +} + +export interface WaitsForTemplate { + isTemplateReady: ko.Observable; +} + +export interface TreeNode { + nodeKind: string; + rid: string; + id: ko.Observable; + database?: Database; + collection?: Collection; + + onNewQueryClick?(source: any, event: MouseEvent): void; + onNewStoredProcedureClick?(source: Collection, event: MouseEvent): void; + onNewUserDefinedFunctionClick?(source: Collection, event: MouseEvent): void; + onNewTriggerClick?(source: Collection, event: MouseEvent): void; +} + +export interface Database extends TreeNode { + container: Explorer; + self: string; + id: ko.Observable; + collections: ko.ObservableArray; + offer: ko.Observable; + isDatabaseExpanded: ko.Observable; + isDatabaseShared: ko.Computed; + + selectedSubnodeKind: ko.Observable; + + selectDatabase(): void; + expandDatabase(): Promise; + collapseDatabase(): void; + + loadCollections(): Promise; + findCollectionWithId(collectionId: string): Collection; + openAddCollection(database: Database, event: MouseEvent): void; + onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void; + onSettingsClick: () => void; + loadOffer(): Promise; +} + +export interface CollectionBase extends TreeNode { + container: Explorer; + databaseId: string; + self: string; + rawDataModel: DataModels.Collection; + partitionKey: DataModels.PartitionKey; + partitionKeyProperty: string; + partitionKeyPropertyHeader: string; + id: ko.Observable; + selectedSubnodeKind: ko.Observable; + children: ko.ObservableArray; + isCollectionExpanded: ko.Observable; + + onDocumentDBDocumentsClick(): void; + onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void; + expandCollection(): Q.Promise; + collapseCollection(): void; + getDatabase(): Database; +} + +export interface Collection extends CollectionBase { + defaultTtl: ko.Observable; + analyticalStorageTtl: ko.Observable; + schema?: DataModels.ISchema; + requestSchema?: () => void; + indexingPolicy: ko.Observable; + uniqueKeyPolicy: DataModels.UniqueKeyPolicy; + usageSizeInKB: ko.Observable; + offer: ko.Observable; + conflictResolutionPolicy: ko.Observable; + changeFeedPolicy: ko.Observable; + geospatialConfig: ko.Observable; + documentIds: ko.ObservableArray; + + cassandraKeys: CassandraTableKeys; + cassandraSchema: CassandraTableKey[]; + + onConflictsClick(): void; + onTableEntitiesClick(): void; + onGraphDocumentsClick(): void; + onMongoDBDocumentsClick(): void; + openTab(): void; + + onSettingsClick: () => Promise; + onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void; + + onNewGraphClick(): void; + onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string): void; + onNewMongoShellClick(): void; + onNewStoredProcedureClick(source: Collection, event: MouseEvent): void; + onNewUserDefinedFunctionClick(source: Collection, event: MouseEvent): void; + onNewTriggerClick(source: Collection, event: MouseEvent): void; + storedProcedures: ko.Computed; + userDefinedFunctions: ko.Computed; + triggers: ko.Computed; + + isStoredProceduresExpanded: ko.Observable; + isTriggersExpanded: ko.Observable; + isUserDefinedFunctionsExpanded: ko.Observable; + + expandStoredProcedures(): void; + expandUserDefinedFunctions(): void; + expandTriggers(): void; + + collapseStoredProcedures(): void; + collapseUserDefinedFunctions(): void; + collapseTriggers(): void; + + loadUserDefinedFunctions(): Promise; + loadStoredProcedures(): Promise; + loadTriggers(): Promise; + loadOffer(): Promise; + + createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure; + createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction; + createTriggerNode(data: TriggerDefinition & Resource): Trigger; + findStoredProcedureWithId(sprocRid: string): StoredProcedure; + findTriggerWithId(triggerRid: string): Trigger; + findUserDefinedFunctionWithId(udfRid: string): UserDefinedFunction; + + onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; + onDrop(source: Collection, event: { originalEvent: DragEvent }): void; + uploadFiles(fileList: FileList): Q.Promise; + + getLabel(): string; +} + +/** + * Options used to initialize pane + */ +export interface PaneOptions { + id: string; + visible: ko.Observable; + container?: Explorer; +} + +/** + * Graph configuration + */ +export enum NeighborType { + SOURCES_ONLY, + TARGETS_ONLY, + BOTH, +} + +/** + * Set of observable related to graph configuration by user + */ +export interface GraphConfigUiData { + showNeighborType: ko.Observable; + nodeProperties: ko.ObservableArray; + nodePropertiesWithNone: ko.ObservableArray; + nodeCaptionChoice: ko.Observable; + nodeColorKeyChoice: ko.Observable; + nodeIconChoice: ko.Observable; + nodeIconSet: ko.Observable; +} + +/** + * User input for creating new vertex + */ +export interface NewVertexData { + label: string; + properties: InputProperty[]; +} + +export type GremlinPropertyValueType = string | boolean | number | null | undefined; +export type InputPropertyValueTypeString = "string" | "number" | "boolean" | "null"; +export interface InputPropertyValue { + value: GremlinPropertyValueType; + type: InputPropertyValueTypeString; +} +/** + * Property input by user + */ +export interface InputProperty { + key: string; + values: InputPropertyValue[]; +} + +export interface Editable extends ko.Observable { + setBaseline(baseline: T): void; + + editableIsDirty: ko.Computed; + editableIsValid: ko.Observable; + getEditableCurrentValue?: ko.Computed; + getEditableOriginalValue?: ko.Computed; + edits?: ko.ObservableArray; + validations?: ko.ObservableArray<(value: T) => boolean>; +} + +export interface QueryError { + message: string; + start: string; + end: string; + code: string; + severity: string; +} + +export interface DocumentRequestContainer { + self: string; + rid?: string; + resourceName?: string; +} + +export interface DocumentClientOption { + endpoint?: string; + masterKey?: string; + requestTimeoutMs?: number; +} + +// Tab options +export interface TabOptions { + tabKind: CollectionTabKind; + title: string; + tabPath: string; + isActive: ko.Observable; + hashLocation: string; + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void; + isTabsContentExpanded?: ko.Observable; + onLoadStartKey?: number; + + // TODO Remove the flag and use a context to handle this + // TODO: 145357 Remove dependency on collection/database and add abstraction + collection?: CollectionBase; + database?: Database; + rid?: string; + node?: TreeNode; + theme?: string; +} + +export interface DocumentsTabOptions extends TabOptions { + partitionKey: DataModels.PartitionKey; + documentIds: ko.ObservableArray; + container?: Explorer; + isPreferredApiMongoDB?: boolean; + resourceTokenPartitionKey?: string; +} + +export interface SettingsTabV2Options extends TabOptions { + getPendingNotification: Q.Promise; +} + +export interface ConflictsTabOptions extends TabOptions { + partitionKey: DataModels.PartitionKey; + conflictIds: ko.ObservableArray; + container?: Explorer; +} + +export interface QueryTabOptions extends TabOptions { + partitionKey?: DataModels.PartitionKey; + queryText?: string; + resourceTokenPartitionKey?: string; +} + +export interface ScriptTabOption extends TabOptions { + resource: any; + isNew: boolean; + partitionKey?: DataModels.PartitionKey; +} + +export interface EditorPosition { + line: number; + column: number; +} + +export enum DocumentExplorerState { + noDocumentSelected, + newDocumentValid, + newDocumentInvalid, + exisitingDocumentNoEdits, + exisitingDocumentDirtyValid, + exisitingDocumentDirtyInvalid, +} + +export enum IndexingPolicyEditorState { + noCollectionSelected, + noEdits, + dirtyValid, + dirtyInvalid, +} + +export enum ScriptEditorState { + newInvalid, + newValid, + exisitingNoEdits, + exisitingDirtyValid, + exisitingDirtyInvalid, +} + +export enum CollectionTabKind { + Documents = 0, + Settings = 1, + StoredProcedures = 2, + UserDefinedFunctions = 3, + Triggers = 4, + Query = 5, + Graph = 6, + QueryTables = 9, + MongoShell = 10, + DatabaseSettings = 11, + Conflicts = 12, + Notebook = 13 /* Deprecated */, + Terminal = 14, + NotebookV2 = 15, + SparkMasterTab = 16, + Gallery = 17, + NotebookViewer = 18, + Schema = 19, + SettingsV2 = 20, +} + +export enum TerminalKind { + Default = 0, + Mongo = 1, + Cassandra = 2, +} + +export interface DataExplorerInputsFrame { + databaseAccount: any; + subscriptionId: string; + resourceGroup: string; + masterKey: string; + hasWriteAccess: boolean; + authorizationToken: string; + features: any; + csmEndpoint: string; + dnsSuffix: string; + serverId: string; + extensionEndpoint: string; + subscriptionType: SubscriptionType; + quotaId: string; + addCollectionDefaultFlight: string; + isTryCosmosDBSubscription: boolean; + loadDatabaseAccountTimestamp?: number; + sharedThroughputMinimum?: number; + sharedThroughputMaximum?: number; + sharedThroughputDefault?: number; + dataExplorerVersion?: string; + isAuthWithresourceToken?: boolean; + defaultCollectionThroughput?: CollectionCreationDefaults; + flights?: readonly string[]; + selfServeType?: SelfServeType; +} + +export interface CollectionCreationDefaults { + storage: string; + throughput: ThroughputDefaults; +} + +export interface ThroughputDefaults { + fixed: number; + unlimited: + | number + | { + collectionThreshold: number; + lessThanOrEqualToThreshold: number; + greatThanThreshold: number; + }; + unlimitedmax: number; + unlimitedmin: number; + shared: number; +} + +export class MonacoEditorSettings { + public readonly language: string; + public readonly readOnly: boolean; + + constructor(supportedLanguage: string, isReadOnly: boolean) { + this.language = supportedLanguage; + this.readOnly = isReadOnly; + } +} + +export interface AuthorizationTokenHeaderMetadata { + header: string; + token: string; +} + +export interface DropdownOption { + text: string; + value: T; + disable?: boolean; +} diff --git a/src/Controls/Heatmap/Heatmap.test.ts b/src/Controls/Heatmap/Heatmap.test.ts index c33c0c989..473e55225 100644 --- a/src/Controls/Heatmap/Heatmap.test.ts +++ b/src/Controls/Heatmap/Heatmap.test.ts @@ -6,19 +6,19 @@ describe("The Heatmap Control", () => { const dataPoints = { "1": { "2019-06-19T00:59:10Z": { - "Normalized Throughput": 0.35 + "Normalized Throughput": 0.35, }, "2019-06-19T00:48:10Z": { - "Normalized Throughput": 0.25 - } - } + "Normalized Throughput": 0.25, + }, + }, }; const chartCaptions = { chartTitle: "chart title", yAxisTitle: "YAxisTitle", tooltipText: "Tooltip text", - timeWindow: 123456789 + timeWindow: 123456789, }; let heatmap: Heatmap; @@ -75,12 +75,12 @@ describe("The Heatmap Control", () => { if (dayjs().utcOffset()) { expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([ "2019-06-19T00:48:10Z", - "2019-06-19T00:59:10Z" + "2019-06-19T00:59:10Z", ]); } else { expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([ "2019-06-19T00:48:10Z", - "2019-06-19T00:59:10Z" + "2019-06-19T00:59:10Z", ]); } }); @@ -106,9 +106,9 @@ describe("iframe rendering when there is no data", () => { data: { chartData: {}, chartSettings: {}, - theme: 4 - } - } + theme: 4, + }, + }, }; const divElement: string = `
`; @@ -126,9 +126,9 @@ describe("iframe rendering when there is no data", () => { data: { chartData: {}, chartSettings: {}, - theme: 2 - } - } + theme: 2, + }, + }, }; const divElement: string = `
`; diff --git a/src/Controls/Heatmap/Heatmap.ts b/src/Controls/Heatmap/Heatmap.ts index 92069ac7d..aad6cd38a 100644 --- a/src/Controls/Heatmap/Heatmap.ts +++ b/src/Controls/Heatmap/Heatmap.ts @@ -9,7 +9,7 @@ import { HeatmapData, LayoutSettings, PartitionTimeStampToData, - PortalTheme + PortalTheme, } from "./HeatmapDatatypes"; import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; import { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler"; @@ -43,7 +43,7 @@ export class Heatmap { return { family: StyleConstants.DataExplorerFont, size, - color + color, }; } @@ -73,7 +73,7 @@ export class Heatmap { return 0; } } - }) + }), }; // go thru all rows and create 2d matrix for heatmap... for (let i = 0; i < rows.length; i++) { @@ -115,7 +115,7 @@ export class Heatmap { [0.7, "#E46612"], [0.8, "#E64914"], [0.9, "#B80016"], - [1.0, "#B80016"] + [1.0, "#B80016"], ], name: "", hovertemplate: this._heatmapCaptions.tooltipText, @@ -123,11 +123,11 @@ export class Heatmap { thickness: 15, outlinewidth: 0, tickcolor: StyleConstants.BaseDark, - tickfont: this._getFontStyles(10, this._defaultFontColor) + tickfont: this._getFontStyles(10, this._defaultFontColor), }, y: this._chartData.yAxisPoints, - x: this._chartData.xAxisPoints - } + x: this._chartData.xAxisPoints, + }, ]; } @@ -138,7 +138,7 @@ export class Heatmap { r: 10, b: 35, t: 30, - pad: 0 + pad: 0, }, paper_bgcolor: "transparent", plot_bgcolor: "transparent", @@ -154,7 +154,7 @@ export class Heatmap { autotick: true, fixedrange: true, ticks: "", - showticklabels: false + showticklabels: false, }, xaxis: { fixedrange: true, @@ -167,13 +167,13 @@ export class Heatmap { autotick: true, tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e", showticklabels: true, - tickfont: this._getFontStyles(10) + tickfont: this._getFontStyles(10), }, title: { text: this._heatmapCaptions.chartTitle, x: 0.01, - font: this._getFontStyles(13, this._defaultFontColor) - } + font: this._getFontStyles(13, this._defaultFontColor), + }, }; } @@ -181,7 +181,7 @@ export class Heatmap { return { /* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings responsive: true,*/ - displayModeBar: false + displayModeBar: false, }; } diff --git a/src/Controls/Heatmap/HeatmapDatatypes.ts b/src/Controls/Heatmap/HeatmapDatatypes.ts index 58897f5bc..3ef9a7109 100644 --- a/src/Controls/Heatmap/HeatmapDatatypes.ts +++ b/src/Controls/Heatmap/HeatmapDatatypes.ts @@ -8,7 +8,7 @@ export enum PortalTheme { blue = 1, azure, light, - dark + dark, } export interface HeatmapData { diff --git a/src/DefaultAccountExperienceType.ts b/src/DefaultAccountExperienceType.ts index a7cd708aa..19a5d0f13 100644 --- a/src/DefaultAccountExperienceType.ts +++ b/src/DefaultAccountExperienceType.ts @@ -4,5 +4,5 @@ export enum DefaultAccountExperienceType { MongoDB = "MongoDB", Table = "Table", Cassandra = "Cassandra", - ApiForMongoDB = "Azure Cosmos DB for MongoDB API" + ApiForMongoDB = "Azure Cosmos DB for MongoDB API", } diff --git a/src/Definitions/datatables.d.ts b/src/Definitions/datatables.d.ts index 3373f7479..b8ebd606e 100644 --- a/src/Definitions/datatables.d.ts +++ b/src/Definitions/datatables.d.ts @@ -1,1954 +1,1954 @@ -// Type definitions for JQuery DataTables 1.10.9 -// Project: http://www.datatables.net -// Definitions by: Kiarash Ghiaseddin , Omid Rad , Armin Sander -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -// missing: -// - Static methods that are defined in JQueryStatic.fn are not typed. -// - Plugin and extension definitions are not typed. -// - Some return types are not fully wokring - -/// - -interface JQuery { - DataTable(param?: DataTables.Settings): DataTables.DataTable; -} - -//TODO: Wrong, as jquery.d.ts has no interface for fn -//interface JQueryStatic { -// dataTable: DataTables.StaticFunctions; -//} - -declare namespace DataTables { - export interface DataTable extends DataTableCore { - /** - * Get the data for the whole table. - */ - data(): DataTable; - - /** - * Order Methods / Object - */ - order: OrderMethods; - - //#region "Cell/Cells" - - /** - * Select the cell found by a cell selector - * - * @param cellSelector Cell selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - cell( - cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], - modifier?: ObjectSelectorModifier - ): CellMethods; - - /** - * Select the cell found by a cell selector - * - * @param rowSelector Row selector. - * @param cellSelector Cell selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - cell( - rowSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], - cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], - modifier?: ObjectSelectorModifier - ): CellMethods; - - /** - * Select all cells - * - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - cells(modifier?: ObjectSelectorModifier): CellsMethods; - - /** - * Select cells found by a cell selector - * - * @param cellSelector Cell selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - cells( - cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], - modifier?: ObjectSelectorModifier - ): CellsMethods; - - /** - * Select cells found by both row and column selectors - * - * @param rowSelector Row selector. - * @param cellSelector Cell selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - cells( - rowSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], - cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], - modifier?: ObjectSelectorModifier - ): CellsMethods; - //#endregion "Cell/Cells" - - //#region "Column/Columns" - - /** - * Column Methods / Object - */ - column: ColumnMethodsModel; - - /** - * Columns Methods / Object - */ - columns: ColumnsMethodsModel; - - //#endregion "Column/Columns" - - //#region "Row/Rows" - - /** - * Row Methode / Object - */ - row: RowMethodsModel; - - /** - * Rows Methods / Object - */ - rows: RowsMethodsModel; - - //#endregion "Row/Rows" - - //#region "Table/Tables" - - /** - * Select a table based on a selector from the API's context - * - * @param tableSelector Table selector. - */ - table( - tableSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[] - ): TableMethods; - - /** - * Select all tables - */ - tables(): TablesMethods; - - /** - * Select tables based on the given selector - * - * @param tableSelector Table selector. - */ - tables( - tableSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[] - ): TablesMethods; - - //#endregion "Table/Tables" - } - - export interface DataTables extends DataTableCore { - [index: number]: DataTable; - } - - interface ObjectSelectorModifier { - /** - * The order modifier provides the ability to control which order the rows are processed in. - * Values: 'current', 'applied', 'index', 'original' - */ - order?: string; - - /** - * The search modifier provides the ability to govern which rows are used by the selector using the search options that are applied to the table. - * Values: 'none', 'applied', 'removed' - */ - search?: string; - - /** - * The page modifier allows you to control if the selector should consider all data in the table, regardless of paging, or if only the rows in the currently disabled page should be used. - * Values: 'all', 'current' - */ - page?: string; - } - - //#region "Namespaces" - - //#region "core-methods" - - interface DataTableCore extends UtilityMethods { - /** - * Get jquery object - */ - $(selector: string | Node | Node[] | JQuery, modifier?: ObjectSelectorModifier): JQuery; - - ///// Almost identical to $ in operation, but in this case returns the data for the matched rows. - //_(selector: string | Node | Node[] | JQuery, modifier?: ObjectSelectorModifier): JQuery; - - /** - * Ajax Methods - */ - ajax: AjaxMethodModel; - - /** - * Clear the table of all data. - */ - clear(): DataTable; - - /** - * Destroy the DataTables in the current context. - * - * @param remove Completely remove the table from the DOM (true) or leave it in the DOM in its original plain un-enhanced HTML state (default, false). - */ - destroy(remove?: boolean): DataTable; - - /** - * Redraw the DataTables in the current context, optionally updating ordering, searching and paging as required. - * - * @param paging This parameter is used to determine what kind of draw DataTables will perform. - */ - draw(paging?: boolean | string): DataTable; - - /* - * Look up a language token that was defined in the DataTables' language initialisation object. - * - * @param token The language token to lookup from the language object. - * @param def The default value to use if the DataTables initialisation has not specified a value. - * @param numeric If handling numeric output, the number to be presented should be given in this parameter. If not numeric operator is required (for example button label text) this parameter is not required. - * - * @returns Resulting internationalised string. - */ - i18n(token: string, def: any | string, numeric?: number): string; - - /* - * Get the initialisation options used for the table. Since: DataTables 1.10.6 - */ - init(): Settings; - - /** - * Table events removal. - * - * @param event Event name to remove. - * @param callback Specific callback function to remove if you want to unbind a single event listener. - */ - off(event: string, callback?: Function): DataTable; - - /** - * Table events listener. - * - * @param event Event to listen for. - * @param callback Specific callback function to remove if you want to unbind a single event listener. - */ - on(event: string, callback: Function): DataTable; - - /** - * Listen for a table event once and then remove the listener. - * - * @param event Event to listen for. - * @param callback Specific callback function to remove if you want to unbind a single event listener. - */ - one(event: string, callback: Function): DataTable; - - /** - * Page Methods / Object - */ - page: PageMethods; - - /** - * Get current search - */ - search(): string; - - /** - * Search for data in the table. - * - * @param input Search string to apply to the table. - * @param regex Treat as a regular expression (true) or not (default, false). - * @param smart Perform smart search. - * @param caseInsen Do case-insensitive matching (default, true) or not (false). - */ - search(input: string, regex?: boolean, smart?: boolean, caseInsen?: boolean): DataTable; - - /** - * Obtain the table's settings object - */ - settings(): DataTable; - - /** - * Page Methods / Object - */ - state: StateMethods; - } - - //#region "ajax-methods" - - interface AjaxMethods extends DataTable { - /** - * Reload the table data from the Ajax data source. - * - * @param callback Function which is executed when the data as been reloaded and the table fully redrawn. - * @param resetPaging Reset (default action or true) or hold the current paging position (false). - */ - load(callback?: Function, resetPaging?: boolean): DataTable; - } - - interface AjaxMethodModel { - /** - * Get the latest JSON data obtained from the last Ajax request DataTables made - */ - json(): Object; - - /** - * Get the data submitted by DataTables to the server in the last Ajax request - */ - params(): Object; - - /** - * Reload the table data from the Ajax data source. - * - * @param callback Function which is executed when the data as been reloaded and the table fully redrawn. - * @param resetPaging Reset (default action or true) or hold the current paging position (false). - */ - reload(callback?: Function, resetPaging?: boolean): DataTable; - - /** - * Reload the table data from the Ajax data source - */ - url(): string; - - /** - * Reload the table data from the Ajax data source - * - * @param url URL to set to be the Ajax data source for the table. - */ - url(url: string): AjaxMethods; - } - - //#endregion "ajax-methods" - - //#region "order-methods" - - interface OrderMethods { - /** - * Get the ordering applied to the table. - */ - (): (string | number)[][]; - - /** - * Set the ordering applied to the table. - * - * @param order Order Model - */ - (order?: (string | number)[]): DataTable; - (order?: (string | number)[][]): DataTable; - (order: (string | number)[], ...args: any[]): DataTable; - - /** - * Add an ordering listener to an element, for a given column. - * - * @param node Selector - * @param column Column index - * @param callback Callback function - */ - listener(node: string | Node | JQuery, column: number, callback: Function): DataTable; - } - //#endregion "order-methods" - - //#region "page-methods" - - interface PageMethods { - /** - * Get the current page of the table. - */ - (): number; - - /** - * Set the current page of the table. - * - * @param page Index or 'first', 'next', 'previous', 'last' - */ - (page: number | string): DataTable; - - /** - * Get paging information about the table - */ - info(): PageMethodeModelInfoReturn; - - /** - * Get the table's page length. - */ - len(): number; - - /** - * Set the table's page length. - * - * @param length Page length to set. use -1 to show all records. - */ - len(length: number): DataTable; - } - - interface PageMethodeModelInfoReturn { - page: number; - pages: number; - start: number; - end: number; - length: number; - recordsTotal: number; - recordsDisplay: number; - serverSide: boolean; - } - - //#endregion "page-methods" - - //#region "state-methods" - - interface StateMethods { - /** - * Get the last saved state of the table - */ - (): StateReturnModel; - - /** - * Clear the saved state of the table. - */ - clear(): DataTable; - - /** - * Get the table state that was loaded during initialisation. - */ - loaded(): StateReturnModel; - - /** - * Trigger a state save. - */ - save(): DataTable; - } - - interface StateReturnModel { - time: number; - start: number; - length: number; - order: (string | number)[][]; - search: SearchSettings; - columns: StateReturnModelColumns[]; - } - - interface StateReturnModelColumns { - search: SearchSettings; - visible: boolean; - } - - //#endregion "state-methods" - - //#endregion "core-methods" - - //#region "util-methods" - - interface UtilityMethods { - /* - * Get a boolean value to indicate if there are any entries in the API instance's result set (i.e. any data, selected rows, etc). - */ - any(): boolean; - - /** - * Concatenate two or more API instances together - * - * @param a API instance to concatenate to the initial instance. - * @param b Additional API instance(s) to concatenate to the initial instance. - */ - concat(a: Object, ...b: Object[]): DataTable; - - /** - * Get the number of entries in an API instance's result set, regardless of multi-table grouping (e.g. any data, selected rows, etc). Since: 1.10.8 - */ - count(): number; - - /** - * Iterate over the contents of the API result set. - * - * @param fn Callback function which is called for each item in the API instance result set. The callback is called with three parameters - */ - each(fn: Function): DataTable; - - /** - * Reduce an Api instance to a single context and result set. - * - * @param idx Index to select - */ - eq(idx: number): DataTable; - - /** - * Iterate over the result set of an API instance and test each item, creating a new instance from those items which pass. - * - * @param fn Callback function which is called for each item in the API instance result set. The callback is called with three parameters. - */ - filter(fn: Function): DataTable; - - /** - * Flatten a 2D array structured API instance to a 1D array structure. - */ - flatten(): DataTable; - - /** - * Find the first instance of a value in the API instance's result set. - * - * @param value Value to find in the instance's result set. - */ - indexOf(value: any): number; - - /** - * Join the elements in the result set into a string. - * - * @param separator The string that will be used to separate each element of the result set. - */ - join(separator: string): string; - - /** - * Find the last instance of a value in the API instance's result set. - * - * @param value Value to find in the instance's result set. - */ - lastIndexOf(value: any): number; - - /** - * Number of elements in an API instance's result set. - */ - length: number; - - /** - * Iterate over the result set of an API instance, creating a new API instance from the values returned by the callback. - * - * @param fn Callback function which is called for each item in the API instance result set. The callback is called with three parameters. - */ - map(fn: Function): DataTable; - - /** - * Iterate over the result set of an API instance, creating a new API instance from the values retrieved from the original elements. - * - * @param property Object property name to use from the element in the original result set for the new result set. - */ - pluck(property: number | string): DataTable; - - /** - * Remove the last item from an API instance's result set. - */ - pop(): any; - - /** - * Add one or more items to the end of an API instance's result set. - * - * @param value_1 Item to add to the API instance's result set. - */ - push(value_1: any | any[], ...value_2: any[]): number; - - /** - * Apply a callback function against and accumulator and each element in the Api's result set (left-to-right). - * - * @param fn Callback function which is called for each item in the API instance result set. The callback is called with four parameters. - * @param initialValue Value to use as the first argument of the first call to the fn callback. - */ - reduce(fn: Function, initialValue?: any): any; - - /** - * Apply a callback function against and accumulator and each element in the Api's result set (right-to-left). - * - * @param fn Callback function which is called for each item in the API instance result set. The callback is called with four parameters. - * @param initialValue Value to use as the first argument of the first call to the fn callback. - */ - reduceRight(fn: Function, initialValue?: any): any; - - /** - * Reverse the result set of the API instance and return the original array. - */ - reverse(): DataTable; - - /** - * Remove the first item from an API instance's result set. - */ - shift(): any; - - /** - * Sort the elements of the API instance's result set. - * - * @param fn This is a standard Javascript sort comparison function. It accepts two parameters. - */ - sort(fn?: Function): DataTable; - - /** - * Modify the contents of an Api instance's result set, adding or removing items from it as required. - * - * @param index Index at which to start modifying the Api instance's result set. - * @param howMany Number of elements to remove from the result set. - * @param value_1 Item to add to the result set at the index specified by the first parameter. - */ - splice(index: number, howMany: number, value_1?: any | any[], ...value_2: any[]): any[]; - - /** - * Convert the API instance to a jQuery object, with the objects from the instance's result set in the jQuery result set. - */ - to$(): JQuery; - - /** - * Create a native Javascript array object from an API instance. - */ - toArray(): any[]; - - /** - * Convert the API instance to a jQuery object, with the objects from the instance's result set in the jQuery result set. - */ - toJQuery(): JQuery; - - /** - * Create a new API instance containing only the unique items from a the elements in an instance's result set. - */ - unique(): DataTable; - - /** - * Add one or more items to the start of an API instance's result set. - * - * @param value_1 Item to add to the API instance's result set. - */ - unshift(value_1: any | any[], ...value_2: any[]): number; - } - - //#endregion "util-methods" - - interface CommonSubMethods { - /** - * Get the DataTables cached data for the selected cell - * - * @param t Specify which cache the data should be read from. Can take one of two values: search or order - */ - cache(t: string): DataTable; - } - - //#region "cell-methods" - - interface CommonCellMethods extends CommonSubMethods { - /** - * Invalidate the data held in DataTables for the selected cells - * - * @param source Data source to read the new data from. - */ - invalidate(source?: string): DataTable; - - /** - * Get data for the selected cell - * - * @param f Data type to get. This can be one of: 'display', 'filter', 'sort', 'type' - */ - render(t: string): any; - } - - interface CellMethods extends DataTableCore, CommonCellMethods { - /** - * Get data for the selected cell - */ - data(): any; - - /** - * Get data for the selected cell - * - * @param data Value to assign to the data for the cell - */ - data(data: any): DataTable; - - /** - * Get index information about the selected cell - */ - index(): CellIndexReturn; - - /** - * Get the DOM element for the selected cell - */ - node(): Node; - } - - interface CellIndexReturn { - row: number; - column: number; - columnVisible: number; - } - - interface CellsMethods extends DataTableCore, CommonCellMethods { - /** - * Get data for the selected cells - */ - data(): DataTable; - - /** - * Iterate over each selected cell, with the function context set to be the cell in question. Since: DataTables 1.10.6 - * - * @param fn Function to execute for every cell selected. - */ - every(fn: (cellRowIdx: number, cellColIdx: number, tableLoop: number, cellLoop: number) => void): DataTable; - - /** - * Get index information about the selected cells - */ - indexes(): DataTable; - - /** - * Get the DOM elements for the selected cells - */ - nodes(): DataTable; - } - //#endregion "cell-methods" - - //#region "column-methods" - - interface CommonColumnMethod extends CommonSubMethods { - /** - * Get the footer th / td cell for the selected column. - */ - footer(): any; - - /** - * Get the header th / td cell for a column. - */ - header(): Node; - - /** - * Order the table, in the direction specified, by the column selected by the column()DT selector. - * - * @param direction Direction of sort to apply to the selected column - desc (descending) or asc (ascending). - */ - order(direction: string): DataTable; - - /** - * Get the visibility of the selected column. - */ - visible(): boolean; - - /** - * Set the visibility of the selected column. - * - * @param show Specify if the column should be visible (true) or not (false). - * @param redrawCalculations Indicate if DataTables should recalculate the column layout (true - default) or not (false). Typically this would be left as the default value, but it can be useful to disable when using the method in a loop - so the calculations are performed on every call as they can hamper performance. - */ - visible(show: boolean, redrawCalculations?: boolean): DataTable; - } - - interface ColumnMethodsModel { - /** - * Select the column found by a column selector - * - * @param cellSelector Cell selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - (columnSelector: any, modifier?: ObjectSelectorModifier): ColumnMethods; - - /** - * Convert from the input column index type to that required. - * - * @param t The type on conversion that should take place: 'fromVisible', 'toData', 'fromData', 'toVisible' - * @param index The index to be converted - */ - index(t: string, index: number): number; - } - - interface ColumnMethods extends DataTableCore, CommonColumnMethod { - /** - * Get the data for the cells in the selected column. - */ - data(): DataTable; - - /** - * Get the data source property for the selected column - */ - dataSrc(): number | string | Function; - - /** - * Get index information about the selected cell - * - * @param t Specify if you want to get the column data index (default) or the visible index (visible). - */ - index(t?: string): DataTable; - - /** - * Obtain the th / td nodes for the selected column - */ - nodes(): DataTable[]; - } - - interface ColumnsMethodsModel { - /** - * Select all columns - * - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - (modifier?: ObjectSelectorModifier): ColumnsMethods; - - /** - * Select columns found by a cell selector - * - * @param cellSelector Cell selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - (columnSelector: any, modifier?: ObjectSelectorModifier): ColumnsMethods; - - /** - * Recalculate the column widths for layout. - */ - adjust(): DataTable; - } - - interface ColumnsMethods extends DataTableCore, CommonColumnMethod { - /** - * Obtain the data for the columns from the selector - */ - data(): DataTable; - - /** - * Get the data source property for the selected columns. - */ - dataSrc(): DataTable; - - /** - * Iterate over each selected column, with the function context set to be the column in question. Since: DataTables 1.10.6 - * - * @param fn Function to execute for every column selected. - */ - every(fn: (colIdx: number, tableLoop: number, colLoop: number) => void): DataTable; - - /** - * Get the column indexes of the selected columns. - * - * @param t Specify if you want to get the column data index (default) or the visible index (visible). - */ - indexes(t?: string): DataTable; - - /** - * Obtain the th / td nodes for the selected columns - */ - nodes(): DataTable[][]; - } - //#endregion "column-methods" - - //#region "row-methods" - - interface CommonRowMethod extends CommonSubMethods { - /** - * Obtain the th / td nodes for the selected column - * - * @param source Data source to read the new data from. Values: 'auto', 'data', 'dom' - */ - invalidate(source?: string): DataTable; - } - - interface RowChildMethodModel { - /** - * Get the child row(s) that have been set for a parent row - */ - (): JQuery; - - /** - * Get the child row(s) that have been set for a parent row - * - * @param showRemove This parameter can be given as true or false - */ - (showRemove: boolean): RowChildMethods; - - /** - * Set the data to show in the child row(s). Note that calling this method will replace any child rows which are already attached to the parent row. - * - * @param data The data to be shown in the child row can be given in multiple different ways. - * @param className Class name that is added to the td cell node(s) of the child row(s). As of 1.10.1 it is also added to the tr row node of the child row(s). - */ - (data: (string | Node | JQuery) | (string | Node | JQuery)[], className?: string): RowChildMethods; - - /** - * Hide the child row(s) of a parent row - */ - hide(): DataTable; - - /** - * Check if the child rows of a parent row are visible - */ - isShown(): DataTable; - - /** - * Remove child row(s) from display and release any allocated memory - */ - remove(): DataTable; - - /** - * Show the child row(s) of a parent row - */ - show(): DataTable; - } - - interface RowChildMethods extends DataTableCore { - /** - * Hide the child row(s) of a parent row - */ - hide(): DataTable; - - /** - * Remove child row(s) from display and release any allocated memory - */ - remove(): DataTable; - - /** - * Make newly defined child rows visible - */ - show(): DataTable; - } - - interface RowMethodsModel { - /** - * Select a row found by a row selector - * - * @param rowSelector Row selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - (rowSelector: any, modifier?: ObjectSelectorModifier): RowMethods; - - /** - * Add a new row to the table using the given data - * - * @param data Data to use for the new row. This may be an array, object or Javascript object instance, but must be in the same format as the other data in the table - */ - add(data: any[] | Object): DataTable; - } - - interface RowMethods extends DataTableCore, CommonRowMethod { - /** - * Order Methods / Object - */ - child: RowChildMethodModel; - - /** - * Get the data for the selected row - */ - data(): any[] | Object; - - /** - * Set the data for the selected row - * - * @param d Data to use for the row. - */ - data(d: any[] | Object): DataTable; - - /** - - * Get the id of the selected row. Since: 1.10.8 - * - * @param hash true - Append a hash (#) to the start of the row id. This can be useful for then using the id as a selector - * false - Do not modify the id value. - * @returns Row id. If the row does not have an id available 'undefined' will be returned. - */ - id(hash?: boolean): string; - - /** - * Get the row index of the row column. - */ - index(): number; - - /** - * Obtain the tr node for the selected row - */ - node(): Node; - - /** - * Delete the selected row from the DataTable. - */ - remove(): Node; - } - - interface RowsMethodsModel { - /** - * Select all rows - * - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - (modifier?: ObjectSelectorModifier): RowsMethods; - - /** - * Select rows found by a row selector - * - * @param cellSelector Row selector. - * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. - */ - (rowSelector: any, modifier?: ObjectSelectorModifier): RowsMethods; - - /** - * Add new rows to the table using the data given - * - * @param data Array of data elements, with each one describing a new row to be added to the table - */ - add(data: any[]): DataTable; - } - - interface RowsMethods extends DataTableCore, CommonRowMethod { - /** - * Get the data for the rows from the selector - */ - data(): DataTable; - - /** - * Set the data for the selected row - * - * @param d Data to use for the row. - */ - data(d: any[] | Object): DataTable; - - /** - * Iterate over each selected row, with the function context set to be the row in question. Since: DataTables 1.10.6 - * - * @param fn Function to execute for every row selected. - */ - every(fn: (rowIdx: number, tableLoop: number, rowLoop: number) => void): DataTable; - - /** - * Get the ids of the selected rows. Since: 1.10.8 - * - * @param hash true - Append a hash (#) to the start of each row id. This can be useful for then using the ids as selectors - * false - Do not modify the id value. - * @returns Api instance with the selected rows in its result set. If a row does not have an id available 'undefined' will be returned as the value. - */ - ids(hash?: boolean): DataTable; - - /** - * Get the row indexes of the selected rows. - */ - indexes(): DataTable; - - /** - * Obtain the tr nodes for the selected rows - */ - nodes(): DataTable; - - /** - * Delete the selected rows from the DataTable. - */ - remove(): DataTable; - } - //#endregion "row-methods" - - //#region "table-methods" - - interface TableMethods extends DataTableCore { - /** - * Get the tfoot node for the table in the API's context - */ - footer(): Node; - - /** - * Get the thead node for the table in the API's context - */ - header(): Node; - - /** - * Get the tbody node for the table in the API's context - */ - body(): Node; - - /** - * Get the div container node for the table in the API's context - */ - container(): Node; - - /** - * Get the table node for the table in the API's context - */ - node(): Node; - } - - interface TablesMethods extends DataTableCore { - /** - * Get the tfoot nodes for the tables in the API's context - */ - footer(): DataTable; - - /** - * Get the thead nodes for the tables in the API's context - */ - header(): DataTable; - - /** - * Get the tbody nodes for the tables in the API's context - */ - body(): DataTable; - - /** - * Get the div container nodes for the tables in the API's context - */ - containers(): DataTable; - - /** - * Get the table nodes for the tables in the API's context - */ - nodes(): DataTable; - } - //#endregion "table-methods" - - //#endregion "Namespaces" - - //#region "Static-Methods" - - export interface StaticFunctions { - /** - * Check is a table node is a DataTable or not - * - * @param table Selector string for table - */ - isDataTable(table: string): boolean; - - /** - * Get all DataTable tables that have been initialised - optionally you can select to get only currently visible tables and / or retrieve the tables as API instances. - * - * @param visible As a boolean value this options is used to indicate if you want all tables on the page should be returned (false), or visible tables only (true). - * Since 1.10.8 this option can also be given as an object. - */ - tables(visible?: boolean | ObjectTablesStatic): DataTables.DataTable[] | DataTables.DataTable; - - /** - * Version number compatibility check function - * - * @param version Version string - */ - versionCheck(version: string): boolean; - - /** - * Utils - */ - util: StaticUtilFunctions; - - /** - * Check is a table node is a DataTable or not - * - * @param table Selector string for table - */ - Api(selector: string | Node | Node[] | JQuery): DataTables.DataTable; - } - - export interface StaticUtilFunctions { - /** - * Escape special characters in a regular expression string. Since: 1.10.4 - * - * @param str String to escape - */ - escapeRegex(str: string): string; - - /** - * Throttle the calls to a method to reduce call frequency. Since: 1.10.3 - * - * @param fn Function - * @param period ms - */ - throttle(fn: Function, period?: number): Function; - } - - interface ObjectTablesStatic { - /** - * Get only visible tables (true) or all tables regardless of visibility (false). - */ - visible: boolean; - - /** - * Return a DataTables API instance for the selected tables (true) or an array (false). - */ - api: boolean; - } - - //#endregion "Static-Methods" - - //#region "Settings" - - export interface Settings { - //#region "Features" - - /** - * Feature control DataTables' smart column width handling. Since: 1.10 - */ - autoWidth?: boolean; - - /** - * Feature control deferred rendering for additional speed of initialisation. Since: 1.10 - */ - deferRender?: boolean; - - /** - * Feature control table information display field. Since: 1.10 - */ - info?: boolean; - - /** - * Use markup and classes for the table to be themed by jQuery UI ThemeRoller. Since: 1.10 - */ - jQueryUI?: boolean; - - /** - * Feature control the end user's ability to change the paging display length of the table. Since: 1.10 - */ - lengthChange?: boolean; - - /** - * Feature control ordering (sorting) abilities in DataTables. Since: 1.10 - */ - ordering?: boolean; - - /** - * Enable or disable table pagination. Since: 1.10 - */ - paging?: boolean; - - /** - * Feature control the processing indicator. Since: 1.10 - */ - processing?: boolean; - - /** - * Horizontal scrolling. Since: 1.10 - */ - scrollX?: boolean; - - /** - * Vertical scrolling. Since: 1.10 Exp: "200px" - */ - scrollY?: string; - - /** - * Feature control search (filtering) abilities Since: 1.10 - */ - searching?: boolean; - - /** - * Feature control DataTables' server-side processing mode. Since: 1.10 - */ - serverSide?: boolean; - - /** - * State saving - restore table state on page reload. Since: 1.10 - */ - stateSave?: boolean; - - //#endregion "Features" - - //#region "Data" - - /** - * Load data for the table's content from an Ajax source. Since: 1.10 - */ - ajax?: string | AjaxSettings | FunctionAjax; - - /** - * Data to use as the display data for the table. Since: 1.10 - */ - data?: Object; - - //#endregion "Data" - - //#region "Options" - - /** - * Data to use as the display data for the table. Since: 1.10 - */ - columns?: ColumnSettings[]; - - /** - * Assign a column definition to one or more columns.. Since: 1.10 - */ - columnDefs?: ColumnDefsSettings[]; - - /** - * Delay the loading of server-side data until second draw - */ - deferLoading?: number | number[]; - - /** - * Destroy any existing table matching the selector and replace with the new options. Since: 1.10 - */ - destroy?: boolean; - - /** - * Initial paging start point. Since: 1.10 - */ - displayStart?: number; - - /** - * Define the table control elements to appear on the page and in what order. Since: 1.10 - */ - dom?: string; - - /** - * Change the options in the page length select list. Since: 1.10 - */ - lengthMenu?: (number | string)[] | (number | string)[][]; - - /** - * Control which cell the order event handler will be applied to in a column. Since: 1.10 - */ - orderCellsTop?: boolean; - - /** - * Highlight the columns being ordered in the table's body. Since: 1.10 - */ - orderClasses?: boolean; - - /** - * Initial order (sort) to apply to the table. Since: 1.10 - */ - order?: (string | number)[] | (string | number)[][]; - - /** - * Ordering to always be applied to the table. Since: 1.10 - */ - orderFixed?: (string | number)[] | (string | number)[][] | Object; - - /** - * Multiple column ordering ability control. Since: 1.10 - */ - orderMulti?: boolean; - - /** - * Change the initial page length (number of rows per page). Since: 1.10 - */ - pageLength?: number; - - /** - * Pagination button display options. Basic Types: numbers (1.10.8) simple, simple_numbers, full, full_numbers - */ - pagingType?: string; - - /** - * Retrieve an existing DataTables instance. Since: 1.10 - */ - retrieve?: boolean; - - /** - * Display component renderer types. Since: 1.10 - */ - renderer?: string | RendererSettings; - - /** - * Data property name that DataTables will use to set element DOM IDs. Since: 1.10.8 - */ - rowId?: string; - - /** - * Allow the table to reduce in height when a limited number of rows are shown. Since: 1.10 - */ - scrollCollapse?: boolean; - - /** - * Set an initial filter in DataTables and / or filtering options. Since: 1.10 - */ - search?: SearchSettings; - - /** - * Define an initial search for individual columns. Since: 1.10 - */ - searchCols?: SearchSettings[]; - - /** - * Set a throttle frequency for searching. Since: 1.10 - */ - searchDelay?: number; - - /** - * Saved state validity duration. Since: 1.10 - */ - stateDuration?: number; - - /** - * Set the zebra stripe class names for the rows in the table. Since: 1.10 - */ - stripeClasses?: string[]; - - /** - * Tab index control for keyboard navigation. Since: 1.10 - */ - tabIndex?: number; - - //#endregion "Options" - - //#region "Callbacks" - - /** - * Callback for whenever a TR element is created for the table's body. Since: 1.10 - */ - createdRow?: FunctionCreateRow; - - /** - * Function that is called every time DataTables performs a draw. Since: 1.10 - */ - drawCallback?: FunctionDrawCallback; - - /** - * Footer display callback function. Since: 1.10 - */ - footerCallback?: FunctionFooterCallback; - - /** - * Number formatting callback function. Since: 1.10 - */ - formatNumber?: FunctionFormatNumber; - - /** - * Header display callback function. Since: 1.10 - */ - headerCallback?: FunctionHeaderCallback; - - /** - * Table summary information display callback. Since: 1.10 - */ - infoCallback?: FunctionInfoCallback; - - /** - * Initialisation complete callback. Since: 1.10 - */ - initComplete?: FunctionInitComplete; - - /** - * Pre-draw callback. Since: 1.10 - */ - preDrawCallback?: FunctionPreDrawCallback; - - /** - * Row draw callback.. Since: 1.10 - */ - rowCallback?: FunctionRowCallback; - - /** - * Callback that defines where and how a saved state should be loaded. Since: 1.10 - */ - stateLoadCallback?: FunctionStateLoadCallback; - - /** - * State loaded callback. Since: 1.10 - */ - stateLoaded?: FunctionStateLoaded; - - /** - * State loaded - data manipulation callback. Since: 1.10 - */ - stateLoadParams?: FunctionStateLoadParams; - - /** - * Callback that defines how the table state is stored and where. Since: 1.10 - */ - stateSaveCallback?: FunctionStateSaveCallback; - - /** - * State save - data manipulation callback. Since: 1.10 - */ - stateSaveParams?: FunctionStateSaveParams; - - //#endregion "Callbacks" - - //#region "Language" - - language?: LanguageSettings; - - //#endregion "Language" - } - - //#region "ajax-settings" - - export interface AjaxDataRequest { - draw: number; - start: number; - length: number; - data: any; - order: AjaxDataRequestOrder[]; - columns: AjaxDataRequestColumn[]; - search: AjaxDataRequestSearch; - } - - export interface AjaxDataRequestSearch { - value: string; - regex: boolean; - } - - export interface AjaxDataRequestOrder { - column: number; - dir: string; - } - - export interface AjaxDataRequestColumn { - data: string | number; - name: string; - searchable: boolean; - orderable: boolean; - search: AjaxDataRequestSearch; - } - - export interface AjaxData { - draw?: number; - recordsTotal?: number; - recordsFiltered?: number; - data: any; - error?: string; - } - - interface AjaxSettings extends JQueryAjaxSettings { - /** - * Add or modify data submitted to the server upon an Ajax request. Since: 1.10 - */ - data?: Object | FunctionAjaxData; - - /** - * Data property or manipulation method for table data. Since: 1.10 - */ - dataSrc?: string | Function; - } - - interface FunctionAjax { - (data: Object, callback: Function, settings: SettingsLegacy): void; - } - - interface FunctionAjaxData { - /* - * @param data Data that DataTables has constructed for the request. - * @param settings DataTables settings object. Since 1.10.6 - */ - (data: Object, settings: Settings): string | Object; - } - - //#endregion "ajax-settings" - - //#region "colunm-settings" - - export interface ColumnSettings { - /** - * Cell type to be created for a column. th/td Since: 1.10 - */ - cellType?: string; - - /** - * Class to assign to each cell in the column. Since: 1.10 - */ - className?: string; - - /** - * Add padding to the text content used when calculating the optimal with for a table. Since: 1.10 - */ - contentPadding?: string; - - /** - * Cell created callback to allow DOM manipulation. Since: 1.10 - */ - createdCell?: FunctionColumnCreatedCell; - - /** - * Class to assign to each cell in the column. Since: 1.10 - */ - data?: number | string | ObjectColumnData | FunctionColumnData; - - /** - * Set default, static, content for a column. Since: 1.10 - */ - defaultContent?: string; - - /** - * Set a descriptive name for a column. Since: 1.10 - */ - name?: string; - - /** - * Enable or disable ordering on this column. Since: 1.10 - */ - orderable?: boolean; - - /** - * Define multiple column ordering as the default order for a column. Since: 1.10 - */ - orderData?: number | number[]; - - /** - * Live DOM sorting type assignment. Since: 1.10 - */ - orderDataType?: string; - - /** - * Order direction application sequence. Since: 1.10 - */ - orderSequence?: string[]; - - /** - * Render (process) the data for use in the table. Since: 1.10 - */ - render?: number | string | ObjectColumnRender | FunctionColumnRender; - - /** - * Enable or disable filtering on the data in this column. Since: 1.10 - */ - searchable?: boolean; - - /** - * Set the column title. Since: 1.10 - */ - title?: string; - - /** - * Set the column type - used for filtering and sorting string processing. Since: 1.10 - */ - type?: string; - - /** - * Enable or disable the display of this column. Since: 1.10 - */ - visible?: boolean; - - /** - * Column width assignment. Since: 1.10 - */ - width?: string; - } - - interface ColumnDefsSettings extends ColumnSettings { - targets: string | number | (number | string)[]; - } - - interface FunctionColumnCreatedCell { - (cell: Node, cellData: any, rowData: any, row: number, col: number): void; - } - - interface FunctionColumnData { - (row: any, t: "set", s: any, meta: CellMetaSettings): void; - (row: any, t: "display" | "sort" | "filter" | "type", s: undefined, meta: CellMetaSettings): any; - } - - interface ObjectColumnData { - _: string; - filter?: string; - display?: string; - type?: string; - sort?: string; - } - - interface ObjectColumnRender extends ObjectColumnData {} - - interface FunctionColumnRender { - (data: any, t: string, row: any, meta: CellMetaSettings): void; - } - - interface CellMetaSettings { - row: number; - col: number; - settings: DataTables.Settings; - } - - //#endregion "colunm-settings" - - //#region "other-settings" - - export interface RendererSettings { - header?: string; - pageButton?: string; - } - - export interface SearchSettings { - /** - * Control case-sensitive filtering option. Since: 1.10 - */ - caseInsensitive?: boolean; - - /** - * Enable / disable escaping of regular expression characters in the search term. Since: 1.10 - */ - regex?: boolean; - - /** - * Enable / disable DataTables' smart filtering. Since: 1.10 - */ - smart?: boolean; - - /** - * Set an initial filtering condition on the table. Since: 1.10 - */ - search?: string; - } - - //#endregion "other-settings" - - //#region "callback-functions" - - interface FunctionCreateRow { - (row: Node, data: any[] | Object, dataIndex: number): void; - } - - interface FunctionDrawCallback { - (settings: SettingsLegacy): void; - } - - interface FunctionFooterCallback { - (tfoot: Node, data: any[], start: number, end: number, display: any[]): void; - } - - interface FunctionFormatNumber { - (formatNumber: number): void; - } - - interface FunctionHeaderCallback { - (thead: Node, data: any[], start: number, end: number, display: any[]): void; - } - - interface FunctionInfoCallback { - (settings: SettingsLegacy, start: number, end: number, mnax: number, total: number, pre: string): void; - } - - interface FunctionInitComplete { - (settings: SettingsLegacy, json: Object): void; - } - - interface FunctionPreDrawCallback { - (settings: SettingsLegacy): void; - } - - interface FunctionRowCallback { - (row: Node, data: any[] | Object, index: number): void; - } - - interface FunctionStateLoadCallback { - (settings: SettingsLegacy): void; - } - - interface FunctionStateLoaded { - (settings: SettingsLegacy, data: Object): void; - } - - interface FunctionStateLoadParams { - (settings: SettingsLegacy, data: Object): void; - } - - interface FunctionStateSaveCallback { - (settings: SettingsLegacy, data: Object): void; - } - - interface FunctionStateSaveParams { - (settings: SettingsLegacy, data: Object): void; - } - - //#endregion "callback-functions" - - //#region "language-settings" - - // these are all optional - interface LanguageSettings { - emptyTable?: string; - info?: string; - infoEmpty?: string; - infoFiltered?: string; - infoPostFix?: string; - thousands?: string; - lengthMenu?: string; - loadingRecords?: string; - processing?: string; - search?: string; - zeroRecords?: string; - paginate?: LanguagePaginateSettings; - aria?: LanguageAriaSettings; - url?: string; - } - - interface LanguagePaginateSettings { - first: string; - last: string; - next: string; - previous: string; - } - - interface LanguageAriaSettings { - sortAscending: string; - sortDescending: string; - } - - //#endregion "language-settings" - - //#endregion "Settings" - - //#region "SettingsLegacy" - - interface ArrayStringNode { - [index: string]: Node; - } - - export interface SettingsLegacy { - ajax: any; - oApi: any; - oFeatures: FeaturesLegacy; - oScroll: ScrollingLegacy; - oLanguage: LanguageLegacy; // | { fnInfoCallback: FunctionInfoCallback; }; - oBrowser: BrowserLegacy; - aanFeatures: ArrayStringNode[][]; - aoData: RowLegacy[]; - aIds: any; - aiDisplay: number[]; - aiDisplayMaster: number[]; - aoColumns: ColumnLegacy[]; - aoHeader: any[]; - aoFooter: any[]; - asDataSearch: string[]; - oPreviousSearch: any; - aoPreSearchCols: any[]; - aaSorting: any[][]; - aaSortingFixed: any[][]; - asStripeClasses: string[]; - asDestroyStripes: string[]; - sDestroyWidth: number; - aoRowCallback: FunctionRowCallback[]; - aoHeaderCallback: FunctionHeaderCallback[]; - aoFooterCallback: FunctionFooterCallback[]; - aoDrawCallback: FunctionDrawCallback[]; - aoRowCreatedCallback: FunctionCreateRow[]; - aoPreDrawCallback: FunctionPreDrawCallback[]; - aoInitComplete: FunctionInitComplete[]; - aoStateSaveParams: FunctionStateSaveParams[]; - aoStateLoadParams: FunctionStateLoadParams[]; - aoStateLoaded: FunctionStateLoaded[]; - sTableId: string; - nTable: Node; - nTHead: Node; - nTFoot: Node; - nTBody: Node; - nTableWrapper: Node; - bDeferLoading: boolean; - bInitialized: boolean; - aoOpenRows: any[]; - sDom: string; - sPaginationType: string; - iCookieDuration: number; - sCookiePrefix: string; - fnCookieCallback: CookieCallbackLegacy; - aoStateSave: FunctionStateSaveCallback[]; - aoStateLoad: FunctionStateLoadCallback[]; - oLoadedState: any; - sAjaxSource: string; - sAjaxDataProp: string; - bAjaxDataGet: boolean; - jqXHR: any; - fnServerData: any; - aoServerParams: any[]; - sServerMethod: string; - fnFormatNumber: FunctionFormatNumber; - aLengthMenu: any[]; - iDraw: number; - bDrawing: boolean; - iDrawError: number; - _iDisplayLength: number; - _iDisplayStart: number; - _iDisplayEnd: number; - _iRecordsTotal: number; - _iRecordsDisplay: number; - bJUI: boolean; - oClasses: any; - bFiltered: boolean; - bSorted: boolean; - bSortCellsTop: boolean; - oInit: any; - aoDestroyCallback: any[]; - fnRecordsTotal: () => number; - fnRecordsDisplay: () => number; - fnDisplayEnd: () => number; - oInstance: any; - sInstance: string; - iTabIndex: number; - nScrollHead: Node; - nScrollFoot: Node; - rowIdFn: (mSource: string | number | Function) => Function; - } - - export interface BrowserLegacy { - barWidth: number; - bBounding: boolean; - bScrollbarLeft: boolean; - bScrollOversize: boolean; - } - - export interface FeaturesLegacy { - bAutoWidth: boolean; - bDeferRender: boolean; - bFilter: boolean; - bInfo: boolean; - bLengthChange: boolean; - bPaginate: boolean; - bProcessing: boolean; - bServerSide: boolean; - bSort: boolean; - bSortClasses: boolean; - bStateSave: boolean; - } - - export interface ScrollingLegacy { - bAutoCss: boolean; - bCollapse: boolean; - bInfinite: boolean; - iBarWidth: number; - iLoadGap: number; - sX: string; - sY: string; - } - - export interface RowLegacy { - nTr: Node; - _aData: any; - _aSortData: any[]; - _anHidden: Node[]; - _sRowStripe: string; - } - - export interface ColumnLegacy { - aDataSort: any; - asSorting: string[]; - bSearchable: boolean; - bSortable: boolean; - bVisible: boolean; - _bAutoType: boolean; - fnCreatedCell: FunctionColumnCreatedCell; - fnGetData: (data: any, specific: string) => any; - fnSetData: (data: any, value: any) => void; - mData: any; - mRender: any; - nTh: Node; - nIf: Node; - sClass: string; - sContentPadding: string; - sDefaultContent: string; - sName: string; - sSortDataType: string; - sSortingClass: string; - sSortingClassJUI: string; - sTitle: string; - sType: string; - sWidth: string; - sWidthOrig: string; - } - - export interface CookieCallbackLegacy { - (name: string, data: any, expires: string, path: string, cookie: string): void; - } - - export interface LanguageLegacy { - oAria?: LanguageAriaLegacy; - oPaginate?: LanguagePaginateLegacy; - sEmptyTable?: string; - sInfo?: string; - sInfoEmpty?: string; - sInfoFiltered?: string; - sInfoPostFix?: string; - sInfoThousands?: string; - sLengthMenu?: string; - sLoadingRecords?: string; - sProcessing?: string; - sSearch?: string; - sUrl?: string; - sZeroRecords?: string; - } - - export interface LanguageAriaLegacy { - sSortAscending?: string; - sSortDescending?: string; - } - - export interface LanguagePaginateLegacy { - sFirst?: string; - sLast?: string; - sNext?: string; - sPrevious?: string; - } - //#endregion "SettingsLegacy" -} - -declare module "datatables" { - export = DataTables; -} +// Type definitions for JQuery DataTables 1.10.9 +// Project: http://www.datatables.net +// Definitions by: Kiarash Ghiaseddin , Omid Rad , Armin Sander +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +// missing: +// - Static methods that are defined in JQueryStatic.fn are not typed. +// - Plugin and extension definitions are not typed. +// - Some return types are not fully wokring + +/// + +interface JQuery { + DataTable(param?: DataTables.Settings): DataTables.DataTable; +} + +//TODO: Wrong, as jquery.d.ts has no interface for fn +//interface JQueryStatic { +// dataTable: DataTables.StaticFunctions; +//} + +declare namespace DataTables { + export interface DataTable extends DataTableCore { + /** + * Get the data for the whole table. + */ + data(): DataTable; + + /** + * Order Methods / Object + */ + order: OrderMethods; + + //#region "Cell/Cells" + + /** + * Select the cell found by a cell selector + * + * @param cellSelector Cell selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + cell( + cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], + modifier?: ObjectSelectorModifier + ): CellMethods; + + /** + * Select the cell found by a cell selector + * + * @param rowSelector Row selector. + * @param cellSelector Cell selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + cell( + rowSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], + cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], + modifier?: ObjectSelectorModifier + ): CellMethods; + + /** + * Select all cells + * + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + cells(modifier?: ObjectSelectorModifier): CellsMethods; + + /** + * Select cells found by a cell selector + * + * @param cellSelector Cell selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + cells( + cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], + modifier?: ObjectSelectorModifier + ): CellsMethods; + + /** + * Select cells found by both row and column selectors + * + * @param rowSelector Row selector. + * @param cellSelector Cell selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + cells( + rowSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], + cellSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[], + modifier?: ObjectSelectorModifier + ): CellsMethods; + //#endregion "Cell/Cells" + + //#region "Column/Columns" + + /** + * Column Methods / Object + */ + column: ColumnMethodsModel; + + /** + * Columns Methods / Object + */ + columns: ColumnsMethodsModel; + + //#endregion "Column/Columns" + + //#region "Row/Rows" + + /** + * Row Methode / Object + */ + row: RowMethodsModel; + + /** + * Rows Methods / Object + */ + rows: RowsMethodsModel; + + //#endregion "Row/Rows" + + //#region "Table/Tables" + + /** + * Select a table based on a selector from the API's context + * + * @param tableSelector Table selector. + */ + table( + tableSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[] + ): TableMethods; + + /** + * Select all tables + */ + tables(): TablesMethods; + + /** + * Select tables based on the given selector + * + * @param tableSelector Table selector. + */ + tables( + tableSelector: (string | Node | Function | JQuery | Object) | (string | Node | Function | JQuery | Object)[] + ): TablesMethods; + + //#endregion "Table/Tables" + } + + export interface DataTables extends DataTableCore { + [index: number]: DataTable; + } + + interface ObjectSelectorModifier { + /** + * The order modifier provides the ability to control which order the rows are processed in. + * Values: 'current', 'applied', 'index', 'original' + */ + order?: string; + + /** + * The search modifier provides the ability to govern which rows are used by the selector using the search options that are applied to the table. + * Values: 'none', 'applied', 'removed' + */ + search?: string; + + /** + * The page modifier allows you to control if the selector should consider all data in the table, regardless of paging, or if only the rows in the currently disabled page should be used. + * Values: 'all', 'current' + */ + page?: string; + } + + //#region "Namespaces" + + //#region "core-methods" + + interface DataTableCore extends UtilityMethods { + /** + * Get jquery object + */ + $(selector: string | Node | Node[] | JQuery, modifier?: ObjectSelectorModifier): JQuery; + + ///// Almost identical to $ in operation, but in this case returns the data for the matched rows. + //_(selector: string | Node | Node[] | JQuery, modifier?: ObjectSelectorModifier): JQuery; + + /** + * Ajax Methods + */ + ajax: AjaxMethodModel; + + /** + * Clear the table of all data. + */ + clear(): DataTable; + + /** + * Destroy the DataTables in the current context. + * + * @param remove Completely remove the table from the DOM (true) or leave it in the DOM in its original plain un-enhanced HTML state (default, false). + */ + destroy(remove?: boolean): DataTable; + + /** + * Redraw the DataTables in the current context, optionally updating ordering, searching and paging as required. + * + * @param paging This parameter is used to determine what kind of draw DataTables will perform. + */ + draw(paging?: boolean | string): DataTable; + + /* + * Look up a language token that was defined in the DataTables' language initialisation object. + * + * @param token The language token to lookup from the language object. + * @param def The default value to use if the DataTables initialisation has not specified a value. + * @param numeric If handling numeric output, the number to be presented should be given in this parameter. If not numeric operator is required (for example button label text) this parameter is not required. + * + * @returns Resulting internationalised string. + */ + i18n(token: string, def: any | string, numeric?: number): string; + + /* + * Get the initialisation options used for the table. Since: DataTables 1.10.6 + */ + init(): Settings; + + /** + * Table events removal. + * + * @param event Event name to remove. + * @param callback Specific callback function to remove if you want to unbind a single event listener. + */ + off(event: string, callback?: Function): DataTable; + + /** + * Table events listener. + * + * @param event Event to listen for. + * @param callback Specific callback function to remove if you want to unbind a single event listener. + */ + on(event: string, callback: Function): DataTable; + + /** + * Listen for a table event once and then remove the listener. + * + * @param event Event to listen for. + * @param callback Specific callback function to remove if you want to unbind a single event listener. + */ + one(event: string, callback: Function): DataTable; + + /** + * Page Methods / Object + */ + page: PageMethods; + + /** + * Get current search + */ + search(): string; + + /** + * Search for data in the table. + * + * @param input Search string to apply to the table. + * @param regex Treat as a regular expression (true) or not (default, false). + * @param smart Perform smart search. + * @param caseInsen Do case-insensitive matching (default, true) or not (false). + */ + search(input: string, regex?: boolean, smart?: boolean, caseInsen?: boolean): DataTable; + + /** + * Obtain the table's settings object + */ + settings(): DataTable; + + /** + * Page Methods / Object + */ + state: StateMethods; + } + + //#region "ajax-methods" + + interface AjaxMethods extends DataTable { + /** + * Reload the table data from the Ajax data source. + * + * @param callback Function which is executed when the data as been reloaded and the table fully redrawn. + * @param resetPaging Reset (default action or true) or hold the current paging position (false). + */ + load(callback?: Function, resetPaging?: boolean): DataTable; + } + + interface AjaxMethodModel { + /** + * Get the latest JSON data obtained from the last Ajax request DataTables made + */ + json(): Object; + + /** + * Get the data submitted by DataTables to the server in the last Ajax request + */ + params(): Object; + + /** + * Reload the table data from the Ajax data source. + * + * @param callback Function which is executed when the data as been reloaded and the table fully redrawn. + * @param resetPaging Reset (default action or true) or hold the current paging position (false). + */ + reload(callback?: Function, resetPaging?: boolean): DataTable; + + /** + * Reload the table data from the Ajax data source + */ + url(): string; + + /** + * Reload the table data from the Ajax data source + * + * @param url URL to set to be the Ajax data source for the table. + */ + url(url: string): AjaxMethods; + } + + //#endregion "ajax-methods" + + //#region "order-methods" + + interface OrderMethods { + /** + * Get the ordering applied to the table. + */ + (): (string | number)[][]; + + /** + * Set the ordering applied to the table. + * + * @param order Order Model + */ + (order?: (string | number)[]): DataTable; + (order?: (string | number)[][]): DataTable; + (order: (string | number)[], ...args: any[]): DataTable; + + /** + * Add an ordering listener to an element, for a given column. + * + * @param node Selector + * @param column Column index + * @param callback Callback function + */ + listener(node: string | Node | JQuery, column: number, callback: Function): DataTable; + } + //#endregion "order-methods" + + //#region "page-methods" + + interface PageMethods { + /** + * Get the current page of the table. + */ + (): number; + + /** + * Set the current page of the table. + * + * @param page Index or 'first', 'next', 'previous', 'last' + */ + (page: number | string): DataTable; + + /** + * Get paging information about the table + */ + info(): PageMethodeModelInfoReturn; + + /** + * Get the table's page length. + */ + len(): number; + + /** + * Set the table's page length. + * + * @param length Page length to set. use -1 to show all records. + */ + len(length: number): DataTable; + } + + interface PageMethodeModelInfoReturn { + page: number; + pages: number; + start: number; + end: number; + length: number; + recordsTotal: number; + recordsDisplay: number; + serverSide: boolean; + } + + //#endregion "page-methods" + + //#region "state-methods" + + interface StateMethods { + /** + * Get the last saved state of the table + */ + (): StateReturnModel; + + /** + * Clear the saved state of the table. + */ + clear(): DataTable; + + /** + * Get the table state that was loaded during initialisation. + */ + loaded(): StateReturnModel; + + /** + * Trigger a state save. + */ + save(): DataTable; + } + + interface StateReturnModel { + time: number; + start: number; + length: number; + order: (string | number)[][]; + search: SearchSettings; + columns: StateReturnModelColumns[]; + } + + interface StateReturnModelColumns { + search: SearchSettings; + visible: boolean; + } + + //#endregion "state-methods" + + //#endregion "core-methods" + + //#region "util-methods" + + interface UtilityMethods { + /* + * Get a boolean value to indicate if there are any entries in the API instance's result set (i.e. any data, selected rows, etc). + */ + any(): boolean; + + /** + * Concatenate two or more API instances together + * + * @param a API instance to concatenate to the initial instance. + * @param b Additional API instance(s) to concatenate to the initial instance. + */ + concat(a: Object, ...b: Object[]): DataTable; + + /** + * Get the number of entries in an API instance's result set, regardless of multi-table grouping (e.g. any data, selected rows, etc). Since: 1.10.8 + */ + count(): number; + + /** + * Iterate over the contents of the API result set. + * + * @param fn Callback function which is called for each item in the API instance result set. The callback is called with three parameters + */ + each(fn: Function): DataTable; + + /** + * Reduce an Api instance to a single context and result set. + * + * @param idx Index to select + */ + eq(idx: number): DataTable; + + /** + * Iterate over the result set of an API instance and test each item, creating a new instance from those items which pass. + * + * @param fn Callback function which is called for each item in the API instance result set. The callback is called with three parameters. + */ + filter(fn: Function): DataTable; + + /** + * Flatten a 2D array structured API instance to a 1D array structure. + */ + flatten(): DataTable; + + /** + * Find the first instance of a value in the API instance's result set. + * + * @param value Value to find in the instance's result set. + */ + indexOf(value: any): number; + + /** + * Join the elements in the result set into a string. + * + * @param separator The string that will be used to separate each element of the result set. + */ + join(separator: string): string; + + /** + * Find the last instance of a value in the API instance's result set. + * + * @param value Value to find in the instance's result set. + */ + lastIndexOf(value: any): number; + + /** + * Number of elements in an API instance's result set. + */ + length: number; + + /** + * Iterate over the result set of an API instance, creating a new API instance from the values returned by the callback. + * + * @param fn Callback function which is called for each item in the API instance result set. The callback is called with three parameters. + */ + map(fn: Function): DataTable; + + /** + * Iterate over the result set of an API instance, creating a new API instance from the values retrieved from the original elements. + * + * @param property Object property name to use from the element in the original result set for the new result set. + */ + pluck(property: number | string): DataTable; + + /** + * Remove the last item from an API instance's result set. + */ + pop(): any; + + /** + * Add one or more items to the end of an API instance's result set. + * + * @param value_1 Item to add to the API instance's result set. + */ + push(value_1: any | any[], ...value_2: any[]): number; + + /** + * Apply a callback function against and accumulator and each element in the Api's result set (left-to-right). + * + * @param fn Callback function which is called for each item in the API instance result set. The callback is called with four parameters. + * @param initialValue Value to use as the first argument of the first call to the fn callback. + */ + reduce(fn: Function, initialValue?: any): any; + + /** + * Apply a callback function against and accumulator and each element in the Api's result set (right-to-left). + * + * @param fn Callback function which is called for each item in the API instance result set. The callback is called with four parameters. + * @param initialValue Value to use as the first argument of the first call to the fn callback. + */ + reduceRight(fn: Function, initialValue?: any): any; + + /** + * Reverse the result set of the API instance and return the original array. + */ + reverse(): DataTable; + + /** + * Remove the first item from an API instance's result set. + */ + shift(): any; + + /** + * Sort the elements of the API instance's result set. + * + * @param fn This is a standard Javascript sort comparison function. It accepts two parameters. + */ + sort(fn?: Function): DataTable; + + /** + * Modify the contents of an Api instance's result set, adding or removing items from it as required. + * + * @param index Index at which to start modifying the Api instance's result set. + * @param howMany Number of elements to remove from the result set. + * @param value_1 Item to add to the result set at the index specified by the first parameter. + */ + splice(index: number, howMany: number, value_1?: any | any[], ...value_2: any[]): any[]; + + /** + * Convert the API instance to a jQuery object, with the objects from the instance's result set in the jQuery result set. + */ + to$(): JQuery; + + /** + * Create a native Javascript array object from an API instance. + */ + toArray(): any[]; + + /** + * Convert the API instance to a jQuery object, with the objects from the instance's result set in the jQuery result set. + */ + toJQuery(): JQuery; + + /** + * Create a new API instance containing only the unique items from a the elements in an instance's result set. + */ + unique(): DataTable; + + /** + * Add one or more items to the start of an API instance's result set. + * + * @param value_1 Item to add to the API instance's result set. + */ + unshift(value_1: any | any[], ...value_2: any[]): number; + } + + //#endregion "util-methods" + + interface CommonSubMethods { + /** + * Get the DataTables cached data for the selected cell + * + * @param t Specify which cache the data should be read from. Can take one of two values: search or order + */ + cache(t: string): DataTable; + } + + //#region "cell-methods" + + interface CommonCellMethods extends CommonSubMethods { + /** + * Invalidate the data held in DataTables for the selected cells + * + * @param source Data source to read the new data from. + */ + invalidate(source?: string): DataTable; + + /** + * Get data for the selected cell + * + * @param f Data type to get. This can be one of: 'display', 'filter', 'sort', 'type' + */ + render(t: string): any; + } + + interface CellMethods extends DataTableCore, CommonCellMethods { + /** + * Get data for the selected cell + */ + data(): any; + + /** + * Get data for the selected cell + * + * @param data Value to assign to the data for the cell + */ + data(data: any): DataTable; + + /** + * Get index information about the selected cell + */ + index(): CellIndexReturn; + + /** + * Get the DOM element for the selected cell + */ + node(): Node; + } + + interface CellIndexReturn { + row: number; + column: number; + columnVisible: number; + } + + interface CellsMethods extends DataTableCore, CommonCellMethods { + /** + * Get data for the selected cells + */ + data(): DataTable; + + /** + * Iterate over each selected cell, with the function context set to be the cell in question. Since: DataTables 1.10.6 + * + * @param fn Function to execute for every cell selected. + */ + every(fn: (cellRowIdx: number, cellColIdx: number, tableLoop: number, cellLoop: number) => void): DataTable; + + /** + * Get index information about the selected cells + */ + indexes(): DataTable; + + /** + * Get the DOM elements for the selected cells + */ + nodes(): DataTable; + } + //#endregion "cell-methods" + + //#region "column-methods" + + interface CommonColumnMethod extends CommonSubMethods { + /** + * Get the footer th / td cell for the selected column. + */ + footer(): any; + + /** + * Get the header th / td cell for a column. + */ + header(): Node; + + /** + * Order the table, in the direction specified, by the column selected by the column()DT selector. + * + * @param direction Direction of sort to apply to the selected column - desc (descending) or asc (ascending). + */ + order(direction: string): DataTable; + + /** + * Get the visibility of the selected column. + */ + visible(): boolean; + + /** + * Set the visibility of the selected column. + * + * @param show Specify if the column should be visible (true) or not (false). + * @param redrawCalculations Indicate if DataTables should recalculate the column layout (true - default) or not (false). Typically this would be left as the default value, but it can be useful to disable when using the method in a loop - so the calculations are performed on every call as they can hamper performance. + */ + visible(show: boolean, redrawCalculations?: boolean): DataTable; + } + + interface ColumnMethodsModel { + /** + * Select the column found by a column selector + * + * @param cellSelector Cell selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + (columnSelector: any, modifier?: ObjectSelectorModifier): ColumnMethods; + + /** + * Convert from the input column index type to that required. + * + * @param t The type on conversion that should take place: 'fromVisible', 'toData', 'fromData', 'toVisible' + * @param index The index to be converted + */ + index(t: string, index: number): number; + } + + interface ColumnMethods extends DataTableCore, CommonColumnMethod { + /** + * Get the data for the cells in the selected column. + */ + data(): DataTable; + + /** + * Get the data source property for the selected column + */ + dataSrc(): number | string | Function; + + /** + * Get index information about the selected cell + * + * @param t Specify if you want to get the column data index (default) or the visible index (visible). + */ + index(t?: string): DataTable; + + /** + * Obtain the th / td nodes for the selected column + */ + nodes(): DataTable[]; + } + + interface ColumnsMethodsModel { + /** + * Select all columns + * + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + (modifier?: ObjectSelectorModifier): ColumnsMethods; + + /** + * Select columns found by a cell selector + * + * @param cellSelector Cell selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + (columnSelector: any, modifier?: ObjectSelectorModifier): ColumnsMethods; + + /** + * Recalculate the column widths for layout. + */ + adjust(): DataTable; + } + + interface ColumnsMethods extends DataTableCore, CommonColumnMethod { + /** + * Obtain the data for the columns from the selector + */ + data(): DataTable; + + /** + * Get the data source property for the selected columns. + */ + dataSrc(): DataTable; + + /** + * Iterate over each selected column, with the function context set to be the column in question. Since: DataTables 1.10.6 + * + * @param fn Function to execute for every column selected. + */ + every(fn: (colIdx: number, tableLoop: number, colLoop: number) => void): DataTable; + + /** + * Get the column indexes of the selected columns. + * + * @param t Specify if you want to get the column data index (default) or the visible index (visible). + */ + indexes(t?: string): DataTable; + + /** + * Obtain the th / td nodes for the selected columns + */ + nodes(): DataTable[][]; + } + //#endregion "column-methods" + + //#region "row-methods" + + interface CommonRowMethod extends CommonSubMethods { + /** + * Obtain the th / td nodes for the selected column + * + * @param source Data source to read the new data from. Values: 'auto', 'data', 'dom' + */ + invalidate(source?: string): DataTable; + } + + interface RowChildMethodModel { + /** + * Get the child row(s) that have been set for a parent row + */ + (): JQuery; + + /** + * Get the child row(s) that have been set for a parent row + * + * @param showRemove This parameter can be given as true or false + */ + (showRemove: boolean): RowChildMethods; + + /** + * Set the data to show in the child row(s). Note that calling this method will replace any child rows which are already attached to the parent row. + * + * @param data The data to be shown in the child row can be given in multiple different ways. + * @param className Class name that is added to the td cell node(s) of the child row(s). As of 1.10.1 it is also added to the tr row node of the child row(s). + */ + (data: (string | Node | JQuery) | (string | Node | JQuery)[], className?: string): RowChildMethods; + + /** + * Hide the child row(s) of a parent row + */ + hide(): DataTable; + + /** + * Check if the child rows of a parent row are visible + */ + isShown(): DataTable; + + /** + * Remove child row(s) from display and release any allocated memory + */ + remove(): DataTable; + + /** + * Show the child row(s) of a parent row + */ + show(): DataTable; + } + + interface RowChildMethods extends DataTableCore { + /** + * Hide the child row(s) of a parent row + */ + hide(): DataTable; + + /** + * Remove child row(s) from display and release any allocated memory + */ + remove(): DataTable; + + /** + * Make newly defined child rows visible + */ + show(): DataTable; + } + + interface RowMethodsModel { + /** + * Select a row found by a row selector + * + * @param rowSelector Row selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + (rowSelector: any, modifier?: ObjectSelectorModifier): RowMethods; + + /** + * Add a new row to the table using the given data + * + * @param data Data to use for the new row. This may be an array, object or Javascript object instance, but must be in the same format as the other data in the table + */ + add(data: any[] | Object): DataTable; + } + + interface RowMethods extends DataTableCore, CommonRowMethod { + /** + * Order Methods / Object + */ + child: RowChildMethodModel; + + /** + * Get the data for the selected row + */ + data(): any[] | Object; + + /** + * Set the data for the selected row + * + * @param d Data to use for the row. + */ + data(d: any[] | Object): DataTable; + + /** + + * Get the id of the selected row. Since: 1.10.8 + * + * @param hash true - Append a hash (#) to the start of the row id. This can be useful for then using the id as a selector + * false - Do not modify the id value. + * @returns Row id. If the row does not have an id available 'undefined' will be returned. + */ + id(hash?: boolean): string; + + /** + * Get the row index of the row column. + */ + index(): number; + + /** + * Obtain the tr node for the selected row + */ + node(): Node; + + /** + * Delete the selected row from the DataTable. + */ + remove(): Node; + } + + interface RowsMethodsModel { + /** + * Select all rows + * + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + (modifier?: ObjectSelectorModifier): RowsMethods; + + /** + * Select rows found by a row selector + * + * @param cellSelector Row selector. + * @param Option used to specify how the cells should be ordered, and if paging or filtering in the table should be taken into account. + */ + (rowSelector: any, modifier?: ObjectSelectorModifier): RowsMethods; + + /** + * Add new rows to the table using the data given + * + * @param data Array of data elements, with each one describing a new row to be added to the table + */ + add(data: any[]): DataTable; + } + + interface RowsMethods extends DataTableCore, CommonRowMethod { + /** + * Get the data for the rows from the selector + */ + data(): DataTable; + + /** + * Set the data for the selected row + * + * @param d Data to use for the row. + */ + data(d: any[] | Object): DataTable; + + /** + * Iterate over each selected row, with the function context set to be the row in question. Since: DataTables 1.10.6 + * + * @param fn Function to execute for every row selected. + */ + every(fn: (rowIdx: number, tableLoop: number, rowLoop: number) => void): DataTable; + + /** + * Get the ids of the selected rows. Since: 1.10.8 + * + * @param hash true - Append a hash (#) to the start of each row id. This can be useful for then using the ids as selectors + * false - Do not modify the id value. + * @returns Api instance with the selected rows in its result set. If a row does not have an id available 'undefined' will be returned as the value. + */ + ids(hash?: boolean): DataTable; + + /** + * Get the row indexes of the selected rows. + */ + indexes(): DataTable; + + /** + * Obtain the tr nodes for the selected rows + */ + nodes(): DataTable; + + /** + * Delete the selected rows from the DataTable. + */ + remove(): DataTable; + } + //#endregion "row-methods" + + //#region "table-methods" + + interface TableMethods extends DataTableCore { + /** + * Get the tfoot node for the table in the API's context + */ + footer(): Node; + + /** + * Get the thead node for the table in the API's context + */ + header(): Node; + + /** + * Get the tbody node for the table in the API's context + */ + body(): Node; + + /** + * Get the div container node for the table in the API's context + */ + container(): Node; + + /** + * Get the table node for the table in the API's context + */ + node(): Node; + } + + interface TablesMethods extends DataTableCore { + /** + * Get the tfoot nodes for the tables in the API's context + */ + footer(): DataTable; + + /** + * Get the thead nodes for the tables in the API's context + */ + header(): DataTable; + + /** + * Get the tbody nodes for the tables in the API's context + */ + body(): DataTable; + + /** + * Get the div container nodes for the tables in the API's context + */ + containers(): DataTable; + + /** + * Get the table nodes for the tables in the API's context + */ + nodes(): DataTable; + } + //#endregion "table-methods" + + //#endregion "Namespaces" + + //#region "Static-Methods" + + export interface StaticFunctions { + /** + * Check is a table node is a DataTable or not + * + * @param table Selector string for table + */ + isDataTable(table: string): boolean; + + /** + * Get all DataTable tables that have been initialised - optionally you can select to get only currently visible tables and / or retrieve the tables as API instances. + * + * @param visible As a boolean value this options is used to indicate if you want all tables on the page should be returned (false), or visible tables only (true). + * Since 1.10.8 this option can also be given as an object. + */ + tables(visible?: boolean | ObjectTablesStatic): DataTables.DataTable[] | DataTables.DataTable; + + /** + * Version number compatibility check function + * + * @param version Version string + */ + versionCheck(version: string): boolean; + + /** + * Utils + */ + util: StaticUtilFunctions; + + /** + * Check is a table node is a DataTable or not + * + * @param table Selector string for table + */ + Api(selector: string | Node | Node[] | JQuery): DataTables.DataTable; + } + + export interface StaticUtilFunctions { + /** + * Escape special characters in a regular expression string. Since: 1.10.4 + * + * @param str String to escape + */ + escapeRegex(str: string): string; + + /** + * Throttle the calls to a method to reduce call frequency. Since: 1.10.3 + * + * @param fn Function + * @param period ms + */ + throttle(fn: Function, period?: number): Function; + } + + interface ObjectTablesStatic { + /** + * Get only visible tables (true) or all tables regardless of visibility (false). + */ + visible: boolean; + + /** + * Return a DataTables API instance for the selected tables (true) or an array (false). + */ + api: boolean; + } + + //#endregion "Static-Methods" + + //#region "Settings" + + export interface Settings { + //#region "Features" + + /** + * Feature control DataTables' smart column width handling. Since: 1.10 + */ + autoWidth?: boolean; + + /** + * Feature control deferred rendering for additional speed of initialisation. Since: 1.10 + */ + deferRender?: boolean; + + /** + * Feature control table information display field. Since: 1.10 + */ + info?: boolean; + + /** + * Use markup and classes for the table to be themed by jQuery UI ThemeRoller. Since: 1.10 + */ + jQueryUI?: boolean; + + /** + * Feature control the end user's ability to change the paging display length of the table. Since: 1.10 + */ + lengthChange?: boolean; + + /** + * Feature control ordering (sorting) abilities in DataTables. Since: 1.10 + */ + ordering?: boolean; + + /** + * Enable or disable table pagination. Since: 1.10 + */ + paging?: boolean; + + /** + * Feature control the processing indicator. Since: 1.10 + */ + processing?: boolean; + + /** + * Horizontal scrolling. Since: 1.10 + */ + scrollX?: boolean; + + /** + * Vertical scrolling. Since: 1.10 Exp: "200px" + */ + scrollY?: string; + + /** + * Feature control search (filtering) abilities Since: 1.10 + */ + searching?: boolean; + + /** + * Feature control DataTables' server-side processing mode. Since: 1.10 + */ + serverSide?: boolean; + + /** + * State saving - restore table state on page reload. Since: 1.10 + */ + stateSave?: boolean; + + //#endregion "Features" + + //#region "Data" + + /** + * Load data for the table's content from an Ajax source. Since: 1.10 + */ + ajax?: string | AjaxSettings | FunctionAjax; + + /** + * Data to use as the display data for the table. Since: 1.10 + */ + data?: Object; + + //#endregion "Data" + + //#region "Options" + + /** + * Data to use as the display data for the table. Since: 1.10 + */ + columns?: ColumnSettings[]; + + /** + * Assign a column definition to one or more columns.. Since: 1.10 + */ + columnDefs?: ColumnDefsSettings[]; + + /** + * Delay the loading of server-side data until second draw + */ + deferLoading?: number | number[]; + + /** + * Destroy any existing table matching the selector and replace with the new options. Since: 1.10 + */ + destroy?: boolean; + + /** + * Initial paging start point. Since: 1.10 + */ + displayStart?: number; + + /** + * Define the table control elements to appear on the page and in what order. Since: 1.10 + */ + dom?: string; + + /** + * Change the options in the page length select list. Since: 1.10 + */ + lengthMenu?: (number | string)[] | (number | string)[][]; + + /** + * Control which cell the order event handler will be applied to in a column. Since: 1.10 + */ + orderCellsTop?: boolean; + + /** + * Highlight the columns being ordered in the table's body. Since: 1.10 + */ + orderClasses?: boolean; + + /** + * Initial order (sort) to apply to the table. Since: 1.10 + */ + order?: (string | number)[] | (string | number)[][]; + + /** + * Ordering to always be applied to the table. Since: 1.10 + */ + orderFixed?: (string | number)[] | (string | number)[][] | Object; + + /** + * Multiple column ordering ability control. Since: 1.10 + */ + orderMulti?: boolean; + + /** + * Change the initial page length (number of rows per page). Since: 1.10 + */ + pageLength?: number; + + /** + * Pagination button display options. Basic Types: numbers (1.10.8) simple, simple_numbers, full, full_numbers + */ + pagingType?: string; + + /** + * Retrieve an existing DataTables instance. Since: 1.10 + */ + retrieve?: boolean; + + /** + * Display component renderer types. Since: 1.10 + */ + renderer?: string | RendererSettings; + + /** + * Data property name that DataTables will use to set element DOM IDs. Since: 1.10.8 + */ + rowId?: string; + + /** + * Allow the table to reduce in height when a limited number of rows are shown. Since: 1.10 + */ + scrollCollapse?: boolean; + + /** + * Set an initial filter in DataTables and / or filtering options. Since: 1.10 + */ + search?: SearchSettings; + + /** + * Define an initial search for individual columns. Since: 1.10 + */ + searchCols?: SearchSettings[]; + + /** + * Set a throttle frequency for searching. Since: 1.10 + */ + searchDelay?: number; + + /** + * Saved state validity duration. Since: 1.10 + */ + stateDuration?: number; + + /** + * Set the zebra stripe class names for the rows in the table. Since: 1.10 + */ + stripeClasses?: string[]; + + /** + * Tab index control for keyboard navigation. Since: 1.10 + */ + tabIndex?: number; + + //#endregion "Options" + + //#region "Callbacks" + + /** + * Callback for whenever a TR element is created for the table's body. Since: 1.10 + */ + createdRow?: FunctionCreateRow; + + /** + * Function that is called every time DataTables performs a draw. Since: 1.10 + */ + drawCallback?: FunctionDrawCallback; + + /** + * Footer display callback function. Since: 1.10 + */ + footerCallback?: FunctionFooterCallback; + + /** + * Number formatting callback function. Since: 1.10 + */ + formatNumber?: FunctionFormatNumber; + + /** + * Header display callback function. Since: 1.10 + */ + headerCallback?: FunctionHeaderCallback; + + /** + * Table summary information display callback. Since: 1.10 + */ + infoCallback?: FunctionInfoCallback; + + /** + * Initialisation complete callback. Since: 1.10 + */ + initComplete?: FunctionInitComplete; + + /** + * Pre-draw callback. Since: 1.10 + */ + preDrawCallback?: FunctionPreDrawCallback; + + /** + * Row draw callback.. Since: 1.10 + */ + rowCallback?: FunctionRowCallback; + + /** + * Callback that defines where and how a saved state should be loaded. Since: 1.10 + */ + stateLoadCallback?: FunctionStateLoadCallback; + + /** + * State loaded callback. Since: 1.10 + */ + stateLoaded?: FunctionStateLoaded; + + /** + * State loaded - data manipulation callback. Since: 1.10 + */ + stateLoadParams?: FunctionStateLoadParams; + + /** + * Callback that defines how the table state is stored and where. Since: 1.10 + */ + stateSaveCallback?: FunctionStateSaveCallback; + + /** + * State save - data manipulation callback. Since: 1.10 + */ + stateSaveParams?: FunctionStateSaveParams; + + //#endregion "Callbacks" + + //#region "Language" + + language?: LanguageSettings; + + //#endregion "Language" + } + + //#region "ajax-settings" + + export interface AjaxDataRequest { + draw: number; + start: number; + length: number; + data: any; + order: AjaxDataRequestOrder[]; + columns: AjaxDataRequestColumn[]; + search: AjaxDataRequestSearch; + } + + export interface AjaxDataRequestSearch { + value: string; + regex: boolean; + } + + export interface AjaxDataRequestOrder { + column: number; + dir: string; + } + + export interface AjaxDataRequestColumn { + data: string | number; + name: string; + searchable: boolean; + orderable: boolean; + search: AjaxDataRequestSearch; + } + + export interface AjaxData { + draw?: number; + recordsTotal?: number; + recordsFiltered?: number; + data: any; + error?: string; + } + + interface AjaxSettings extends JQueryAjaxSettings { + /** + * Add or modify data submitted to the server upon an Ajax request. Since: 1.10 + */ + data?: Object | FunctionAjaxData; + + /** + * Data property or manipulation method for table data. Since: 1.10 + */ + dataSrc?: string | Function; + } + + interface FunctionAjax { + (data: Object, callback: Function, settings: SettingsLegacy): void; + } + + interface FunctionAjaxData { + /* + * @param data Data that DataTables has constructed for the request. + * @param settings DataTables settings object. Since 1.10.6 + */ + (data: Object, settings: Settings): string | Object; + } + + //#endregion "ajax-settings" + + //#region "colunm-settings" + + export interface ColumnSettings { + /** + * Cell type to be created for a column. th/td Since: 1.10 + */ + cellType?: string; + + /** + * Class to assign to each cell in the column. Since: 1.10 + */ + className?: string; + + /** + * Add padding to the text content used when calculating the optimal with for a table. Since: 1.10 + */ + contentPadding?: string; + + /** + * Cell created callback to allow DOM manipulation. Since: 1.10 + */ + createdCell?: FunctionColumnCreatedCell; + + /** + * Class to assign to each cell in the column. Since: 1.10 + */ + data?: number | string | ObjectColumnData | FunctionColumnData; + + /** + * Set default, static, content for a column. Since: 1.10 + */ + defaultContent?: string; + + /** + * Set a descriptive name for a column. Since: 1.10 + */ + name?: string; + + /** + * Enable or disable ordering on this column. Since: 1.10 + */ + orderable?: boolean; + + /** + * Define multiple column ordering as the default order for a column. Since: 1.10 + */ + orderData?: number | number[]; + + /** + * Live DOM sorting type assignment. Since: 1.10 + */ + orderDataType?: string; + + /** + * Order direction application sequence. Since: 1.10 + */ + orderSequence?: string[]; + + /** + * Render (process) the data for use in the table. Since: 1.10 + */ + render?: number | string | ObjectColumnRender | FunctionColumnRender; + + /** + * Enable or disable filtering on the data in this column. Since: 1.10 + */ + searchable?: boolean; + + /** + * Set the column title. Since: 1.10 + */ + title?: string; + + /** + * Set the column type - used for filtering and sorting string processing. Since: 1.10 + */ + type?: string; + + /** + * Enable or disable the display of this column. Since: 1.10 + */ + visible?: boolean; + + /** + * Column width assignment. Since: 1.10 + */ + width?: string; + } + + interface ColumnDefsSettings extends ColumnSettings { + targets: string | number | (number | string)[]; + } + + interface FunctionColumnCreatedCell { + (cell: Node, cellData: any, rowData: any, row: number, col: number): void; + } + + interface FunctionColumnData { + (row: any, t: "set", s: any, meta: CellMetaSettings): void; + (row: any, t: "display" | "sort" | "filter" | "type", s: undefined, meta: CellMetaSettings): any; + } + + interface ObjectColumnData { + _: string; + filter?: string; + display?: string; + type?: string; + sort?: string; + } + + interface ObjectColumnRender extends ObjectColumnData {} + + interface FunctionColumnRender { + (data: any, t: string, row: any, meta: CellMetaSettings): void; + } + + interface CellMetaSettings { + row: number; + col: number; + settings: DataTables.Settings; + } + + //#endregion "colunm-settings" + + //#region "other-settings" + + export interface RendererSettings { + header?: string; + pageButton?: string; + } + + export interface SearchSettings { + /** + * Control case-sensitive filtering option. Since: 1.10 + */ + caseInsensitive?: boolean; + + /** + * Enable / disable escaping of regular expression characters in the search term. Since: 1.10 + */ + regex?: boolean; + + /** + * Enable / disable DataTables' smart filtering. Since: 1.10 + */ + smart?: boolean; + + /** + * Set an initial filtering condition on the table. Since: 1.10 + */ + search?: string; + } + + //#endregion "other-settings" + + //#region "callback-functions" + + interface FunctionCreateRow { + (row: Node, data: any[] | Object, dataIndex: number): void; + } + + interface FunctionDrawCallback { + (settings: SettingsLegacy): void; + } + + interface FunctionFooterCallback { + (tfoot: Node, data: any[], start: number, end: number, display: any[]): void; + } + + interface FunctionFormatNumber { + (formatNumber: number): void; + } + + interface FunctionHeaderCallback { + (thead: Node, data: any[], start: number, end: number, display: any[]): void; + } + + interface FunctionInfoCallback { + (settings: SettingsLegacy, start: number, end: number, mnax: number, total: number, pre: string): void; + } + + interface FunctionInitComplete { + (settings: SettingsLegacy, json: Object): void; + } + + interface FunctionPreDrawCallback { + (settings: SettingsLegacy): void; + } + + interface FunctionRowCallback { + (row: Node, data: any[] | Object, index: number): void; + } + + interface FunctionStateLoadCallback { + (settings: SettingsLegacy): void; + } + + interface FunctionStateLoaded { + (settings: SettingsLegacy, data: Object): void; + } + + interface FunctionStateLoadParams { + (settings: SettingsLegacy, data: Object): void; + } + + interface FunctionStateSaveCallback { + (settings: SettingsLegacy, data: Object): void; + } + + interface FunctionStateSaveParams { + (settings: SettingsLegacy, data: Object): void; + } + + //#endregion "callback-functions" + + //#region "language-settings" + + // these are all optional + interface LanguageSettings { + emptyTable?: string; + info?: string; + infoEmpty?: string; + infoFiltered?: string; + infoPostFix?: string; + thousands?: string; + lengthMenu?: string; + loadingRecords?: string; + processing?: string; + search?: string; + zeroRecords?: string; + paginate?: LanguagePaginateSettings; + aria?: LanguageAriaSettings; + url?: string; + } + + interface LanguagePaginateSettings { + first: string; + last: string; + next: string; + previous: string; + } + + interface LanguageAriaSettings { + sortAscending: string; + sortDescending: string; + } + + //#endregion "language-settings" + + //#endregion "Settings" + + //#region "SettingsLegacy" + + interface ArrayStringNode { + [index: string]: Node; + } + + export interface SettingsLegacy { + ajax: any; + oApi: any; + oFeatures: FeaturesLegacy; + oScroll: ScrollingLegacy; + oLanguage: LanguageLegacy; // | { fnInfoCallback: FunctionInfoCallback; }; + oBrowser: BrowserLegacy; + aanFeatures: ArrayStringNode[][]; + aoData: RowLegacy[]; + aIds: any; + aiDisplay: number[]; + aiDisplayMaster: number[]; + aoColumns: ColumnLegacy[]; + aoHeader: any[]; + aoFooter: any[]; + asDataSearch: string[]; + oPreviousSearch: any; + aoPreSearchCols: any[]; + aaSorting: any[][]; + aaSortingFixed: any[][]; + asStripeClasses: string[]; + asDestroyStripes: string[]; + sDestroyWidth: number; + aoRowCallback: FunctionRowCallback[]; + aoHeaderCallback: FunctionHeaderCallback[]; + aoFooterCallback: FunctionFooterCallback[]; + aoDrawCallback: FunctionDrawCallback[]; + aoRowCreatedCallback: FunctionCreateRow[]; + aoPreDrawCallback: FunctionPreDrawCallback[]; + aoInitComplete: FunctionInitComplete[]; + aoStateSaveParams: FunctionStateSaveParams[]; + aoStateLoadParams: FunctionStateLoadParams[]; + aoStateLoaded: FunctionStateLoaded[]; + sTableId: string; + nTable: Node; + nTHead: Node; + nTFoot: Node; + nTBody: Node; + nTableWrapper: Node; + bDeferLoading: boolean; + bInitialized: boolean; + aoOpenRows: any[]; + sDom: string; + sPaginationType: string; + iCookieDuration: number; + sCookiePrefix: string; + fnCookieCallback: CookieCallbackLegacy; + aoStateSave: FunctionStateSaveCallback[]; + aoStateLoad: FunctionStateLoadCallback[]; + oLoadedState: any; + sAjaxSource: string; + sAjaxDataProp: string; + bAjaxDataGet: boolean; + jqXHR: any; + fnServerData: any; + aoServerParams: any[]; + sServerMethod: string; + fnFormatNumber: FunctionFormatNumber; + aLengthMenu: any[]; + iDraw: number; + bDrawing: boolean; + iDrawError: number; + _iDisplayLength: number; + _iDisplayStart: number; + _iDisplayEnd: number; + _iRecordsTotal: number; + _iRecordsDisplay: number; + bJUI: boolean; + oClasses: any; + bFiltered: boolean; + bSorted: boolean; + bSortCellsTop: boolean; + oInit: any; + aoDestroyCallback: any[]; + fnRecordsTotal: () => number; + fnRecordsDisplay: () => number; + fnDisplayEnd: () => number; + oInstance: any; + sInstance: string; + iTabIndex: number; + nScrollHead: Node; + nScrollFoot: Node; + rowIdFn: (mSource: string | number | Function) => Function; + } + + export interface BrowserLegacy { + barWidth: number; + bBounding: boolean; + bScrollbarLeft: boolean; + bScrollOversize: boolean; + } + + export interface FeaturesLegacy { + bAutoWidth: boolean; + bDeferRender: boolean; + bFilter: boolean; + bInfo: boolean; + bLengthChange: boolean; + bPaginate: boolean; + bProcessing: boolean; + bServerSide: boolean; + bSort: boolean; + bSortClasses: boolean; + bStateSave: boolean; + } + + export interface ScrollingLegacy { + bAutoCss: boolean; + bCollapse: boolean; + bInfinite: boolean; + iBarWidth: number; + iLoadGap: number; + sX: string; + sY: string; + } + + export interface RowLegacy { + nTr: Node; + _aData: any; + _aSortData: any[]; + _anHidden: Node[]; + _sRowStripe: string; + } + + export interface ColumnLegacy { + aDataSort: any; + asSorting: string[]; + bSearchable: boolean; + bSortable: boolean; + bVisible: boolean; + _bAutoType: boolean; + fnCreatedCell: FunctionColumnCreatedCell; + fnGetData: (data: any, specific: string) => any; + fnSetData: (data: any, value: any) => void; + mData: any; + mRender: any; + nTh: Node; + nIf: Node; + sClass: string; + sContentPadding: string; + sDefaultContent: string; + sName: string; + sSortDataType: string; + sSortingClass: string; + sSortingClassJUI: string; + sTitle: string; + sType: string; + sWidth: string; + sWidthOrig: string; + } + + export interface CookieCallbackLegacy { + (name: string, data: any, expires: string, path: string, cookie: string): void; + } + + export interface LanguageLegacy { + oAria?: LanguageAriaLegacy; + oPaginate?: LanguagePaginateLegacy; + sEmptyTable?: string; + sInfo?: string; + sInfoEmpty?: string; + sInfoFiltered?: string; + sInfoPostFix?: string; + sInfoThousands?: string; + sLengthMenu?: string; + sLoadingRecords?: string; + sProcessing?: string; + sSearch?: string; + sUrl?: string; + sZeroRecords?: string; + } + + export interface LanguageAriaLegacy { + sSortAscending?: string; + sSortDescending?: string; + } + + export interface LanguagePaginateLegacy { + sFirst?: string; + sLast?: string; + sNext?: string; + sPrevious?: string; + } + //#endregion "SettingsLegacy" +} + +declare module "datatables" { + export = DataTables; +} diff --git a/src/Definitions/jquery-typescript.d.ts b/src/Definitions/jquery-typescript.d.ts index 392e6e699..ebcd22447 100644 --- a/src/Definitions/jquery-typescript.d.ts +++ b/src/Definitions/jquery-typescript.d.ts @@ -1,34 +1,34 @@ -/* Type definitions for code-runner's jquery-typeahead v2.8.0 - * https://github.com/running-coder/jquery-typeahead - * - * There is no DefinitelyTyped support for this library, yet, so we only define here what we use. - * https://github.com/running-coder/jquery-typeahead/issues/156 - * TODO: Replace this minimum definition by the official one when it comes out. - */ -/// - -interface JQueryTypeaheadParam { - input: string; - order?: string; - source: any; - callback?: any; - minLength?: number; - searchOnFocus?: boolean; - template?: string | { (query: string, item: any): string }; - dynamic?: boolean; - mustSelectItem?: boolean; -} - -/** - * For use with: $.typeahead() - */ -interface JQueryStatic { - typeahead(arg: JQueryTypeaheadParam): void; -} - -/** - * For use with $('').typehead() - */ -// interface JQuery { -// typeahead(arg: JQueryTypeaheadParam): void; -// } +/* Type definitions for code-runner's jquery-typeahead v2.8.0 + * https://github.com/running-coder/jquery-typeahead + * + * There is no DefinitelyTyped support for this library, yet, so we only define here what we use. + * https://github.com/running-coder/jquery-typeahead/issues/156 + * TODO: Replace this minimum definition by the official one when it comes out. + */ +/// + +interface JQueryTypeaheadParam { + input: string; + order?: string; + source: any; + callback?: any; + minLength?: number; + searchOnFocus?: boolean; + template?: string | { (query: string, item: any): string }; + dynamic?: boolean; + mustSelectItem?: boolean; +} + +/** + * For use with: $.typeahead() + */ +interface JQueryStatic { + typeahead(arg: JQueryTypeaheadParam): void; +} + +/** + * For use with $('').typehead() + */ +// interface JQuery { +// typeahead(arg: JQueryTypeaheadParam): void; +// } diff --git a/src/Definitions/jquery-ui.d.ts b/src/Definitions/jquery-ui.d.ts index 50d57020a..f7ccfe3b2 100644 --- a/src/Definitions/jquery-ui.d.ts +++ b/src/Definitions/jquery-ui.d.ts @@ -1,1771 +1,1771 @@ -// Type definitions for jQueryUI 1.9 -// Project: http://jqueryui.com/ -// Definitions by: Boris Yankov , John Reilly -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -/// - -declare namespace JQueryUI { - // Accordion ////////////////////////////////////////////////// - - interface AccordionOptions { - active?: any; // boolean or number - animate?: any; // boolean, number, string or object - collapsible?: boolean; - disabled?: boolean; - event?: string; - header?: string; - heightStyle?: string; - icons?: any; - } - - interface AccordionUIParams { - newHeader: JQuery; - oldHeader: JQuery; - newPanel: JQuery; - oldPanel: JQuery; - } - - interface AccordionEvent { - (event: Event, ui: AccordionUIParams): void; - } - - interface AccordionEvents { - activate?: AccordionEvent; - beforeActivate?: AccordionEvent; - create?: AccordionEvent; - } - - interface Accordion extends Widget, AccordionOptions, AccordionEvents {} - - // Autocomplete ////////////////////////////////////////////////// - - interface AutocompleteOptions { - appendTo?: any; //Selector; - autoFocus?: boolean; - delay?: number; - disabled?: boolean; - minLength?: number; - position?: string; - source?: any; // [], string or () - } - - interface AutocompleteUIParams {} - - interface AutocompleteEvent { - (event: Event, ui: AutocompleteUIParams): void; - } - - interface AutocompleteEvents { - change?: AutocompleteEvent; - close?: AutocompleteEvent; - create?: AutocompleteEvent; - focus?: AutocompleteEvent; - open?: AutocompleteEvent; - response?: AutocompleteEvent; - search?: AutocompleteEvent; - select?: AutocompleteEvent; - } - - interface Autocomplete extends Widget, AutocompleteOptions, AutocompleteEvents { - escapeRegex: (value: string) => string; - } - - // Button ////////////////////////////////////////////////// - - interface ButtonOptions { - disabled?: boolean; - icons?: any; - label?: string; - text?: boolean; - } - - interface Button extends Widget, ButtonOptions {} - - // Datepicker ////////////////////////////////////////////////// - - interface DatepickerOptions { - /** - * An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. - */ - altField?: any; // Selector, jQuery or Element - /** - * The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function - */ - altFormat?: string; - /** - * The text to display after each date field, e.g., to show the required format. - */ - appendText?: string; - /** - * Set to true to automatically resize the input field to accommodate dates in the current dateFormat. - */ - autoSize?: boolean; - /** - * A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed. - */ - beforeShow?: (input: Element, inst: any) => JQueryUI.DatepickerOptions; - /** - * A function that takes a date as a parameter and must return an array with: - * [0]: true/false indicating whether or not this date is selectable - * [1]: a CSS class name to add to the date's cell or "" for the default presentation - * [2]: an optional popup tooltip for this date - * The function is called for each day in the datepicker before it is displayed. - */ - beforeShowDay?: (date: Date) => any[]; - /** - * A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed. - */ - buttonImage?: string; - /** - * Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set. - */ - buttonImageOnly?: boolean; - /** - * The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both". - */ - buttonText?: string; - /** - * A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year. - */ - calculateWeek?: (date: Date) => string; - /** - * Whether the month should be rendered as a dropdown instead of text. - */ - changeMonth?: boolean; - /** - * Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection. - */ - changeYear?: boolean; - /** - * The text to display for the close link. Use the showButtonPanel option to display this button. - */ - closeText?: string; - /** - * When true, entry in the input field is constrained to those characters allowed by the current dateFormat option. - */ - constrainInput?: boolean; - /** - * The text to display for the current day link. Use the showButtonPanel option to display this button. - */ - currentText?: string; - /** - * The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function. - */ - dateFormat?: string; - /** - * The list of long day names, starting from Sunday, for use as requested via the dateFormat option. - */ - dayNames?: string[]; - /** - * The list of minimised day names, starting from Sunday, for use as column headers within the datepicker. - */ - dayNamesMin?: string[]; - /** - * The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option. - */ - dayNamesShort?: string[]; - /** - * Set the date to highlight on first opening if the field is blank. Specify either an actual date via a Date object or as a string in the current dateFormat, or a number of days from today (e.g. +7) or a string of values and periods ('y' for years, 'm' for months, 'w' for weeks, 'd' for days, e.g. '+1m +7d'), or null for today. - * Multiple types supported: - * Date: A date object containing the default date. - * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. - * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. - */ - defaultDate?: any; // Date, number or string - /** - * Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast"). - */ - duration?: string; - /** - * Set the first day of the week: Sunday is 0, Monday is 1, etc. - */ - firstDay?: number; - /** - * When true, the current day link moves to the currently selected date instead of today. - */ - gotoCurrent?: boolean; - /** - * Normally the previous and next links are disabled when not applicable (see the minDate and maxDate options). You can hide them altogether by setting this attribute to true. - */ - hideIfNoPrevNext?: boolean; - /** - * Whether the current language is drawn from right to left. - */ - isRTL?: boolean; - /** - * The maximum selectable date. When set to null, there is no maximum. - * Multiple types supported: - * Date: A date object containing the maximum date. - * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. - * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. - */ - maxDate?: any; // Date, number or string - /** - * The minimum selectable date. When set to null, there is no minimum. - * Multiple types supported: - * Date: A date object containing the minimum date. - * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. - * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. - */ - minDate?: any; // Date, number or string - /** - * The list of full month names, for use as requested via the dateFormat option. - */ - monthNames?: string[]; - /** - * The list of abbreviated month names, as used in the month header on each datepicker and as requested via the dateFormat option. - */ - monthNamesShort?: string[]; - /** - * Whether the prevText and nextText options should be parsed as dates by the formatDate function, allowing them to display the target month names for example. - */ - navigationAsDateFormat?: boolean; - /** - * The text to display for the next month link. With the standard ThemeRoller styling, this value is replaced by an icon. - */ - nextText?: string; - /** - * The number of months to show at once. - * Multiple types supported: - * Number: The number of months to display in a single row. - * Array: An array defining the number of rows and columns to display. - */ - numberOfMonths?: any; // number or number[] - /** - * Called when the datepicker moves to a new month and/or year. The function receives the selected year, month (1-12), and the datepicker instance as parameters. this refers to the associated input field. - */ - onChangeMonthYear?: (year: number, month: number, inst: any) => void; - /** - * Called when the datepicker is closed, whether or not a date is selected. The function receives the selected date as text ("" if none) and the datepicker instance as parameters. this refers to the associated input field. - */ - onClose?: (dateText: string, inst: any) => void; - /** - * Called when the datepicker is selected. The function receives the selected date as text and the datepicker instance as parameters. this refers to the associated input field. - */ - onSelect?: (dateText: string, inst: any) => void; - /** - * The text to display for the previous month link. With the standard ThemeRoller styling, this value is replaced by an icon. - */ - prevText?: string; - /** - * Whether days in other months shown before or after the current month are selectable. This only applies if the showOtherMonths option is set to true. - */ - selectOtherMonths?: boolean; - /** - * The cutoff year for determining the century for a date (used in conjunction with dateFormat 'y'). Any dates entered with a year value less than or equal to the cutoff year are considered to be in the current century, while those greater than it are deemed to be in the previous century. - * Multiple types supported: - * Number: A value between 0 and 99 indicating the cutoff year. - * String: A relative number of years from the current year, e.g., "+3" or "-5". - */ - shortYearCutoff?: any; // number or string - /** - * The name of the animation used to show and hide the datepicker. Use "show" (the default), "slideDown", "fadeIn", any of the jQuery UI effects. Set to an empty string to disable animation. - */ - showAnim?: string; - /** - * Whether to display a button pane underneath the calendar. The button pane contains two buttons, a Today button that links to the current day, and a Done button that closes the datepicker. The buttons' text can be customized using the currentText and closeText options respectively. - */ - showButtonPanel?: boolean; - /** - * When displaying multiple months via the numberOfMonths option, the showCurrentAtPos option defines which position to display the current month in. - */ - showCurrentAtPos?: number; - /** - * Whether to show the month after the year in the header. - */ - showMonthAfterYear?: boolean; - /** - * When the datepicker should appear. The datepicker can appear when the field receives focus ("focus"), when a button is clicked ("button"), or when either event occurs ("both"). - */ - showOn?: string; - /** - * If using one of the jQuery UI effects for the showAnim option, you can provide additional settings for that animation via this option. - */ - showOptions?: any; // TODO - /** - * Whether to display dates in other months (non-selectable) at the start or end of the current month. To make these days selectable use the selectOtherMonths option. - */ - showOtherMonths?: boolean; - /** - * When true, a column is added to show the week of the year. The calculateWeek option determines how the week of the year is calculated. You may also want to change the firstDay option. - */ - showWeek?: boolean; - /** - * Set how many months to move when clicking the previous/next links. - */ - stepMonths?: number; - /** - * The text to display for the week of the year column heading. Use the showWeek option to display this column. - */ - weekHeader?: string; - /** - * The range of years displayed in the year drop-down: either relative to today's year ("-nn:+nn"), relative to the currently selected year ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats ("nnnn:-nn"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the minDate and/or maxDate options. - */ - yearRange?: string; - /** - * Additional text to display after the year in the month headers. - */ - yearSuffix?: string; - } - - interface DatepickerFormatDateOptions { - dayNamesShort?: string[]; - dayNames?: string[]; - monthNamesShort?: string[]; - monthNames?: string[]; - } - - interface Datepicker extends Widget, DatepickerOptions { - regional: { [languageCod3: string]: any }; - setDefaults(defaults: DatepickerOptions): void; - formatDate(format: string, date: Date, settings?: DatepickerFormatDateOptions): string; - parseDate(format: string, date: string, settings?: DatepickerFormatDateOptions): Date; - iso8601Week(date: Date): number; - noWeekends(date: Date): any[]; - } - - // Dialog ////////////////////////////////////////////////// - - interface DialogOptions { - autoOpen?: boolean; - buttons?: any; // object or [] - closeOnEscape?: boolean; - closeText?: string; - dialogClass?: string; - disabled?: boolean; - draggable?: boolean; - height?: any; // number or string - maxHeight?: number; - maxWidth?: number; - minHeight?: number; - minWidth?: number; - modal?: boolean; - position?: any; // object, string or [] - resizable?: boolean; - show?: any; // number, string or object - stack?: boolean; - title?: string; - width?: any; // number or string - zIndex?: number; - - close?: DialogEvent; - } - - interface DialogUIParams {} - - interface DialogEvent { - (event: Event, ui: DialogUIParams): void; - } - - interface DialogEvents { - beforeClose?: DialogEvent; - close?: DialogEvent; - create?: DialogEvent; - drag?: DialogEvent; - dragStart?: DialogEvent; - dragStop?: DialogEvent; - focus?: DialogEvent; - open?: DialogEvent; - resize?: DialogEvent; - resizeStart?: DialogEvent; - resizeStop?: DialogEvent; - } - - interface Dialog extends Widget, DialogOptions, DialogEvents {} - - // Draggable ////////////////////////////////////////////////// - - interface DraggableEventUIParams { - helper: JQuery; - position: { top: number; left: number }; - offset: { top: number; left: number }; - } - - interface DraggableEvent { - (event: Event, ui: DraggableEventUIParams): void; - } - - interface DraggableOptions { - disabled?: boolean; - addClasses?: boolean; - appendTo?: any; - axis?: string; - cancel?: string; - connectToSortable?: string; - containment?: any; - cursor?: string; - cursorAt?: any; - delay?: number; - distance?: number; - grid?: number[]; - handle?: any; - helper?: any; - iframeFix?: any; - opacity?: number; - refreshPositions?: boolean; - revert?: any; - revertDuration?: number; - scope?: string; - scroll?: boolean; - scrollSensitivity?: number; - scrollSpeed?: number; - snap?: any; - snapMode?: string; - snapTolerance?: number; - stack?: string; - zIndex?: number; - } - - interface DraggableEvents { - create?: DraggableEvent; - start?: DraggableEvent; - drag?: DraggableEvent; - stop?: DraggableEvent; - } - - interface Draggable extends Widget, DraggableOptions, DraggableEvent {} - - // Droppable ////////////////////////////////////////////////// - - interface DroppableEventUIParam { - draggable: JQuery; - helper: JQuery; - position: { top: number; left: number }; - offset: { top: number; left: number }; - } - - interface DroppableEvent { - (event: Event, ui: DroppableEventUIParam): void; - } - - interface DroppableOptions { - disabled?: boolean; - accept?: any; - activeClass?: string; - greedy?: boolean; - hoverClass?: string; - scope?: string; - tolerance?: string; - } - - interface DroppableEvents { - create?: DroppableEvent; - activate?: DroppableEvent; - deactivate?: DroppableEvent; - over?: DroppableEvent; - out?: DroppableEvent; - drop?: DroppableEvent; - } - - interface Droppable extends Widget, DroppableOptions, DroppableEvents {} - - // Menu ////////////////////////////////////////////////// - - interface MenuOptions { - disabled?: boolean; - icons?: any; - menus?: string; - position?: any; // TODO - role?: string; - } - - interface MenuUIParams {} - - interface MenuEvent { - (event: Event, ui: MenuUIParams): void; - } - - interface MenuEvents { - blur?: MenuEvent; - create?: MenuEvent; - focus?: MenuEvent; - select?: MenuEvent; - } - - interface Menu extends Widget, MenuOptions, MenuEvents {} - - // Progressbar ////////////////////////////////////////////////// - - interface ProgressbarOptions { - disabled?: boolean; - value?: number; - } - - interface ProgressbarUIParams {} - - interface ProgressbarEvent { - (event: Event, ui: ProgressbarUIParams): void; - } - - interface ProgressbarEvents { - change?: ProgressbarEvent; - complete?: ProgressbarEvent; - create?: ProgressbarEvent; - } - - interface Progressbar extends Widget, ProgressbarOptions, ProgressbarEvents {} - - // Resizable ////////////////////////////////////////////////// - - interface ResizableOptions { - alsoResize?: any; // Selector, JQuery or Element - animate?: boolean; - animateDuration?: any; // number or string - animateEasing?: string; - aspectRatio?: any; // boolean or number - autoHide?: boolean; - cancel?: string; - containment?: any; // Selector, Element or string - delay?: number; - disabled?: boolean; - distance?: number; - ghost?: boolean; - grid?: any; - handles?: any; // string or object - helper?: string; - maxHeight?: number; - maxWidth?: number; - minHeight?: number; - minWidth?: number; - resizeHeight?: boolean; - create?: ResizableEvent; - resize?: ResizableEvent; - start?: ResizableEvent; - stop?: ResizableEvent; - } - - interface ResizableUIParams { - element: JQuery; - helper: JQuery; - originalElement: JQuery; - originalPosition: any; - originalSize: any; - position: any; - size: any; - } - - interface ResizableEvent { - (event: Event, ui: ResizableUIParams): void; - } - - interface ResizableEvents { - resize?: ResizableEvent; - start?: ResizableEvent; - stop?: ResizableEvent; - } - - interface Resizable extends Widget, ResizableOptions, ResizableEvents {} - - // Selectable ////////////////////////////////////////////////// - - interface SelectableOptions { - autoRefresh?: boolean; - cancel?: string; - delay?: number; - disabled?: boolean; - distance?: number; - filter?: string; - tolerance?: string; - } - - interface SelectableEvents { - selected?(event: Event, ui: { selected?: Element }): void; - selecting?(event: Event, ui: { selecting?: Element }): void; - start?(event: Event, ui: any): void; - stop?(event: Event, ui: any): void; - unselected?(event: Event, ui: { unselected: Element }): void; - unselecting?(event: Event, ui: { unselecting: Element }): void; - } - - interface Selectable extends Widget, SelectableOptions, SelectableEvents {} - - // Slider ////////////////////////////////////////////////// - - interface SliderOptions { - animate?: any; // boolean, string or number - disabled?: boolean; - max?: number; - min?: number; - orientation?: string; - range?: any; // boolean or string - step?: number; - value?: number; - values?: number[]; - } - - interface SliderUIParams { - handle?: JQuery; - value?: number; - values?: number[]; - } - - interface SliderEvent { - (event: Event, ui: SliderUIParams): void; - } - - interface SliderEvents { - change?: SliderEvent; - create?: SliderEvent; - slide?: SliderEvent; - start?: SliderEvent; - stop?: SliderEvent; - } - - interface Slider extends Widget, SliderOptions, SliderEvents {} - - // Sortable ////////////////////////////////////////////////// - - interface SortableOptions extends SortableEvents { - appendTo?: any; // jQuery, Element, Selector or string - axis?: string; - cancel?: any; // Selector - connectWith?: any; // Selector - containment?: any; // Element, Selector or string - cursor?: string; - cursorAt?: any; - delay?: number; - disabled?: boolean; - distance?: number; - dropOnEmpty?: boolean; - forceHelperSize?: boolean; - forcePlaceholderSize?: boolean; - grid?: number[]; - handle?: any; // Selector or Element - items?: any; // Selector - opacity?: number; - placeholder?: string; - revert?: any; // boolean or number - scroll?: boolean; - scrollSensitivity?: number; - scrollSpeed?: number; - tolerance?: string; - zIndex?: number; - } - - interface SortableUIParams { - helper: JQuery; - item: JQuery; - offset: any; - position: any; - originalPosition: any; - sender: JQuery; - placeholder: JQuery; - } - - interface SortableEvent { - (event: JQueryEventObject, ui: SortableUIParams): void; - } - - interface SortableEvents { - activate?: SortableEvent; - beforeStop?: SortableEvent; - change?: SortableEvent; - deactivate?: SortableEvent; - out?: SortableEvent; - over?: SortableEvent; - receive?: SortableEvent; - remove?: SortableEvent; - sort?: SortableEvent; - start?: SortableEvent; - stop?: SortableEvent; - update?: SortableEvent; - } - - interface Sortable extends Widget, SortableOptions, SortableEvents {} - - // Spinner ////////////////////////////////////////////////// - - interface SpinnerOptions { - culture?: string; - disabled?: boolean; - icons?: any; - incremental?: any; // boolean or () - max?: any; // number or string - min?: any; // number or string - numberFormat?: string; - page?: number; - step?: any; // number or string - } - - interface SpinnerUIParams {} - - interface SpinnerEvent { - (event: Event, ui: SpinnerUIParams): void; - } - - interface SpinnerEvents { - spin?: SpinnerEvent; - start?: SpinnerEvent; - stop?: SpinnerEvent; - } - - interface Spinner extends Widget, SpinnerOptions, SpinnerEvents {} - - // Tabs ////////////////////////////////////////////////// - - interface TabsOptions { - active?: any; // boolean or number - collapsible?: boolean; - disabled?: any; // boolean or [] - event?: string; - heightStyle?: string; - hide?: any; // boolean, number, string or object - show?: any; // boolean, number, string or object - - activate?: TabsEvent; - } - - interface TabsUIParams { - newTab: JQuery; - oldTab: JQuery; - newPanel: JQuery; - oldPanel: JQuery; - } - - interface TabsEvent { - (event: Event, ui: TabsUIParams): void; - } - - interface TabsEvents { - activate?: TabsEvent; - beforeActivate?: TabsEvent; - beforeLoad?: TabsEvent; - load?: TabsEvent; - } - - interface Tabs extends Widget, TabsOptions, TabsEvents {} - - // Tooltip ////////////////////////////////////////////////// - - interface TooltipOptions { - content?: any; // () or string - disabled?: boolean; - hide?: any; // boolean, number, string or object - items?: string; - position?: any; // TODO - show?: any; // boolean, number, string or object - tooltipClass?: string; - track?: boolean; - } - - interface TooltipUIParams {} - - interface TooltipEvent { - (event: Event, ui: TooltipUIParams): void; - } - - interface TooltipEvents { - close?: TooltipEvent; - open?: TooltipEvent; - } - - interface Tooltip extends Widget, TooltipOptions, TooltipEvents {} - - // Effects ////////////////////////////////////////////////// - - interface EffectOptions { - effect: string; - easing?: string; - duration: any; - complete: Function; - } - - interface BlindEffect { - direction?: string; - } - - interface BounceEffect { - distance?: number; - times?: number; - } - - interface ClipEffect { - direction?: number; - } - - interface DropEffect { - direction?: number; - } - - interface ExplodeEffect { - pieces?: number; - } - - interface FadeEffect {} - - interface FoldEffect { - size?: any; - horizFirst?: boolean; - } - - interface HighlightEffect { - color?: string; - } - - interface PuffEffect { - percent?: number; - } - - interface PulsateEffect { - times?: number; - } - - interface ScaleEffect { - direction?: string; - origin?: string[]; - percent?: number; - scale?: string; - } - - interface ShakeEffect { - direction?: string; - distance?: number; - times?: number; - } - - interface SizeEffect { - to?: any; - origin?: string[]; - scale?: string; - } - - interface SlideEffect { - direction?: string; - distance?: number; - } - - interface TransferEffect { - className?: string; - to?: string; - } - - interface JQueryPositionOptions { - my?: string; - at?: string; - of?: any; - collision?: string; - using?: Function; - within?: any; - } - - // UI ////////////////////////////////////////////////// - - interface MouseOptions { - cancel?: string; - delay?: number; - distance?: number; - } - - interface KeyCode { - BACKSPACE: number; - COMMA: number; - DELETE: number; - DOWN: number; - END: number; - ENTER: number; - ESCAPE: number; - HOME: number; - LEFT: number; - NUMPAD_ADD: number; - NUMPAD_DECIMAL: number; - NUMPAD_DIVIDE: number; - NUMPAD_ENTER: number; - NUMPAD_MULTIPLY: number; - NUMPAD_SUBTRACT: number; - PAGE_DOWN: number; - PAGE_UP: number; - PERIOD: number; - RIGHT: number; - SPACE: number; - TAB: number; - UP: number; - } - - interface UI { - mouse(method: string): JQuery; - mouse(options: MouseOptions): JQuery; - mouse(optionLiteral: string, optionName: string, optionValue: any): JQuery; - mouse(optionLiteral: string, optionValue: any): any; - - accordion: Accordion; - autocomplete: Autocomplete; - button: Button; - buttonset: Button; - datepicker: Datepicker; - dialog: Dialog; - keyCode: KeyCode; - menu: Menu; - progressbar: Progressbar; - slider: Slider; - spinner: Spinner; - tabs: Tabs; - tooltip: Tooltip; - version: string; - } - - // Widget ////////////////////////////////////////////////// - - interface WidgetOptions { - disabled?: boolean; - hide?: any; - show?: any; - } - - interface Widget { - (methodName: string): JQuery; - (options: WidgetOptions): JQuery; - (options: AccordionOptions): JQuery; - (optionLiteral: string, optionName: string): any; - (optionLiteral: string, options: WidgetOptions): any; - (optionLiteral: string, optionName: string, optionValue: any): JQuery; - - (name: string, prototype: any): JQuery; - (name: string, base: Function, prototype: any): JQuery; - } - - //////////////////////////////////////////////////////////////////////////////////////////////////// -} - -interface JQuery { - accordion(): JQuery; - accordion(methodName: "destroy"): void; - accordion(methodName: "disable"): void; - accordion(methodName: "enable"): void; - accordion(methodName: "refresh"): void; - accordion(methodName: "widget"): JQuery; - accordion(methodName: string): JQuery; - accordion(options: JQueryUI.AccordionOptions): JQuery; - accordion(optionLiteral: string, optionName: string): any; - accordion(optionLiteral: string, options: JQueryUI.AccordionOptions): any; - accordion(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - autocomplete(): JQuery; - autocomplete(methodName: "close"): void; - autocomplete(methodName: "destroy"): void; - autocomplete(methodName: "disable"): void; - autocomplete(methodName: "enable"): void; - autocomplete(methodName: "search", value?: string): void; - autocomplete(methodName: "widget"): JQuery; - autocomplete(methodName: string): JQuery; - autocomplete(options: JQueryUI.AutocompleteOptions): JQuery; - autocomplete(optionLiteral: string, optionName: string): any; - autocomplete(optionLiteral: string, options: JQueryUI.AutocompleteOptions): any; - autocomplete(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - button(): JQuery; - button(methodName: "destroy"): void; - button(methodName: "disable"): void; - button(methodName: "enable"): void; - button(methodName: "refresh"): void; - button(methodName: "widget"): JQuery; - button(methodName: string): JQuery; - button(options: JQueryUI.ButtonOptions): JQuery; - button(optionLiteral: string, optionName: string): any; - button(optionLiteral: string, options: JQueryUI.ButtonOptions): any; - button(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - buttonset(): JQuery; - buttonset(methodName: "destroy"): void; - buttonset(methodName: "disable"): void; - buttonset(methodName: "enable"): void; - buttonset(methodName: "refresh"): void; - buttonset(methodName: "widget"): JQuery; - buttonset(methodName: string): JQuery; - buttonset(options: JQueryUI.ButtonOptions): JQuery; - buttonset(optionLiteral: string, optionName: string): any; - buttonset(optionLiteral: string, options: JQueryUI.ButtonOptions): any; - buttonset(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - /** - * Initialize a datepicker - */ - datepicker(): JQuery; - /** - * Removes the datepicker functionality completely. This will return the element back to its pre-init state. - * - * @param methodName 'destroy' - */ - datepicker(methodName: "destroy"): JQuery; - /** - * Opens the datepicker in a dialog box. - * - * @param methodName 'dialog' - * @param date The initial date. - * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. - * @param settings The new settings for the date picker. - * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. - */ - datepicker( - methodName: "dialog", - date: Date, - onSelect?: () => void, - settings?: JQueryUI.DatepickerOptions, - pos?: number[] - ): JQuery; - /** - * Opens the datepicker in a dialog box. - * - * @param methodName 'dialog' - * @param date The initial date. - * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. - * @param settings The new settings for the date picker. - * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. - */ - datepicker( - methodName: "dialog", - date: Date, - onSelect?: () => void, - settings?: JQueryUI.DatepickerOptions, - pos?: MouseEvent - ): JQuery; - /** - * Opens the datepicker in a dialog box. - * - * @param methodName 'dialog' - * @param date The initial date. - * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. - * @param settings The new settings for the date picker. - * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. - */ - datepicker( - methodName: "dialog", - date: string, - onSelect?: () => void, - settings?: JQueryUI.DatepickerOptions, - pos?: number[] - ): JQuery; - /** - * Opens the datepicker in a dialog box. - * - * @param methodName 'dialog' - * @param date The initial date. - * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. - * @param settings The new settings for the date picker. - * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. - */ - datepicker( - methodName: "dialog", - date: string, - onSelect?: () => void, - settings?: JQueryUI.DatepickerOptions, - pos?: MouseEvent - ): JQuery; - /** - * Returns the current date for the datepicker or null if no date has been selected. - * - * @param methodName 'getDate' - */ - datepicker(methodName: "getDate"): Date; - /** - * Close a previously opened date picker. - * - * @param methodName 'hide' - */ - datepicker(methodName: "hide"): JQuery; - /** - * Determine whether a date picker has been disabled. - * - * @param methodName 'isDisabled' - */ - datepicker(methodName: "isDisabled"): boolean; - /** - * Redraw the date picker, after having made some external modifications. - * - * @param methodName 'refresh' - */ - datepicker(methodName: "refresh"): JQuery; - /** - * Sets the date for the datepicker. The new date may be a Date object or a string in the current date format (e.g., "01/26/2009"), a number of days from today (e.g., +7) or a string of values and periods ("y" for years, "m" for months, "w" for weeks, "d" for days, e.g., "+1m +7d"), or null to clear the selected date. - * - * @param methodName 'setDate' - * @param date The new date. - */ - datepicker(methodName: "setDate", date: Date): JQuery; - /** - * Sets the date for the datepicker. The new date may be a Date object or a string in the current date format (e.g., "01/26/2009"), a number of days from today (e.g., +7) or a string of values and periods ("y" for years, "m" for months, "w" for weeks, "d" for days, e.g., "+1m +7d"), or null to clear the selected date. - * - * @param methodName 'setDate' - * @param date The new date. - */ - datepicker(methodName: "setDate", date: string): JQuery; - /** - * Open the date picker. If the datepicker is attached to an input, the input must be visible for the datepicker to be shown. - * - * @param methodName 'show' - */ - datepicker(methodName: "show"): JQuery; - /** - * Returns a jQuery object containing the datepicker. - * - * @param methodName 'widget' - */ - datepicker(methodName: "widget"): JQuery; - - /** - * Get the altField option, after initialization - * - * @param methodName 'option' - * @param optionName 'altField' - */ - datepicker(methodName: "option", optionName: "altField"): any; - /** - * Set the altField option, after initialization - * - * @param methodName 'option' - * @param optionName 'altField' - * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. - */ - datepicker(methodName: "option", optionName: "altField", altFieldValue: string): JQuery; - /** - * Set the altField option, after initialization - * - * @param methodName 'option' - * @param optionName 'altField' - * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. - */ - datepicker(methodName: "option", optionName: "altField", altFieldValue: JQuery): JQuery; - /** - * Set the altField option, after initialization - * - * @param methodName 'option' - * @param optionName 'altField' - * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. - */ - datepicker(methodName: "option", optionName: "altField", altFieldValue: Element): JQuery; - - /** - * Get the altFormat option, after initialization - * - * @param methodName 'option' - * @param optionName 'altFormat' - */ - datepicker(methodName: "option", optionName: "altFormat"): string; - /** - * Set the altFormat option, after initialization - * - * @param methodName 'option' - * @param optionName 'altFormat' - * @param altFormatValue The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function - */ - datepicker(methodName: "option", optionName: "altFormat", altFormatValue: string): JQuery; - - /** - * Get the appendText option, after initialization - * - * @param methodName 'option' - * @param optionName 'appendText' - */ - datepicker(methodName: "option", optionName: "appendText"): string; - /** - * Set the appendText option, after initialization - * - * @param methodName 'option' - * @param optionName 'appendText' - * @param appendTextValue The text to display after each date field, e.g., to show the required format. - */ - datepicker(methodName: "option", optionName: "appendText", appendTextValue: string): JQuery; - - /** - * Get the autoSize option, after initialization - * - * @param methodName 'option' - * @param optionName 'autoSize' - */ - datepicker(methodName: "option", optionName: "autoSize"): boolean; - /** - * Set the autoSize option, after initialization - * - * @param methodName 'option' - * @param optionName 'autoSize' - * @param autoSizeValue Set to true to automatically resize the input field to accommodate dates in the current dateFormat. - */ - datepicker(methodName: "option", optionName: "autoSize", autoSizeValue: boolean): JQuery; - - /** - * Get the beforeShow option, after initialization - * - * @param methodName 'option' - * @param optionName 'beforeShow' - */ - datepicker(methodName: "option", optionName: "beforeShow"): (input: Element, inst: any) => JQueryUI.DatepickerOptions; - /** - * Set the beforeShow option, after initialization - * - * @param methodName 'option' - * @param optionName 'beforeShow' - * @param beforeShowValue A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed. - */ - datepicker( - methodName: "option", - optionName: "beforeShow", - beforeShowValue: (input: Element, inst: any) => JQueryUI.DatepickerOptions - ): JQuery; - - /** - * Get the beforeShow option, after initialization - * - * @param methodName 'option' - * @param optionName 'beforeShowDay' - */ - datepicker(methodName: "option", optionName: "beforeShowDay"): (date: Date) => any[]; - /** - * Set the beforeShow option, after initialization - * - * @param methodName 'option' - * @param optionName 'beforeShowDay' - * @param beforeShowDayValue A function that takes a date as a parameter and must return an array with: - * [0]: true/false indicating whether or not this date is selectable - * [1]: a CSS class name to add to the date's cell or "" for the default presentation - * [2]: an optional popup tooltip for this date - * The function is called for each day in the datepicker before it is displayed. - */ - datepicker(methodName: "option", optionName: "beforeShowDay", beforeShowDayValue: (date: Date) => any[]): JQuery; - - /** - * Get the buttonImage option, after initialization - * - * @param methodName 'option' - * @param optionName 'buttonImage' - */ - datepicker(methodName: "option", optionName: "buttonImage"): string; - /** - * Set the buttonImage option, after initialization - * - * @param methodName 'option' - * @param optionName 'buttonImage' - * @param buttonImageValue A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed. - */ - datepicker(methodName: "option", optionName: "buttonImage", buttonImageValue: string): JQuery; - - /** - * Get the buttonImageOnly option, after initialization - * - * @param methodName 'option' - * @param optionName 'buttonImageOnly' - */ - datepicker(methodName: "option", optionName: "buttonImageOnly"): boolean; - /** - * Set the buttonImageOnly option, after initialization - * - * @param methodName 'option' - * @param optionName 'buttonImageOnly' - * @param buttonImageOnlyValue Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set. - */ - datepicker(methodName: "option", optionName: "buttonImageOnly", buttonImageOnlyValue: boolean): JQuery; - - /** - * Get the buttonText option, after initialization - * - * @param methodName 'option' - * @param optionName 'buttonText' - */ - datepicker(methodName: "option", optionName: "buttonText"): string; - /** - * Set the buttonText option, after initialization - * - * @param methodName 'option' - * @param optionName 'buttonText' - * @param buttonTextValue The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both". - */ - datepicker(methodName: "option", optionName: "buttonText", buttonTextValue: string): JQuery; - - /** - * Get the calculateWeek option, after initialization - * - * @param methodName 'option' - * @param optionName 'calculateWeek' - */ - datepicker(methodName: "option", optionName: "calculateWeek"): (date: Date) => string; - /** - * Set the calculateWeek option, after initialization - * - * @param methodName 'option' - * @param optionName 'calculateWeek' - * @param calculateWeekValue A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year. - */ - datepicker(methodName: "option", optionName: "calculateWeek", calculateWeekValue: (date: Date) => string): JQuery; - - /** - * Get the changeMonth option, after initialization - * - * @param methodName 'option' - * @param optionName 'changeMonth' - */ - datepicker(methodName: "option", optionName: "changeMonth"): boolean; - /** - * Set the changeMonth option, after initialization - * - * @param methodName 'option' - * @param optionName 'changeMonth' - * @param changeMonthValue Whether the month should be rendered as a dropdown instead of text. - */ - datepicker(methodName: "option", optionName: "changeMonth", changeMonthValue: boolean): JQuery; - - /** - * Get the changeYear option, after initialization - * - * @param methodName 'option' - * @param optionName 'changeYear' - */ - datepicker(methodName: "option", optionName: "changeYear"): boolean; - /** - * Set the changeYear option, after initialization - * - * @param methodName 'option' - * @param optionName 'changeYear' - * @param changeYearValue Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection. - */ - datepicker(methodName: "option", optionName: "changeYear", changeYearValue: boolean): JQuery; - - /** - * Get the closeText option, after initialization - * - * @param methodName 'option' - * @param optionName 'closeText' - */ - datepicker(methodName: "option", optionName: "closeText"): string; - /** - * Set the closeText option, after initialization - * - * @param methodName 'option' - * @param optionName 'closeText' - * @param closeTextValue The text to display for the close link. Use the showButtonPanel option to display this button. - */ - datepicker(methodName: "option", optionName: "closeText", closeTextValue: string): JQuery; - - /** - * Get the constrainInput option, after initialization - * - * @param methodName 'option' - * @param optionName 'constrainInput' - */ - datepicker(methodName: "option", optionName: "constrainInput"): boolean; - /** - * Set the constrainInput option, after initialization - * - * @param methodName 'option' - * @param optionName 'constrainInput' - * @param constrainInputValue When true, entry in the input field is constrained to those characters allowed by the current dateFormat option. - */ - datepicker(methodName: "option", optionName: "constrainInput", constrainInputValue: boolean): JQuery; - - /** - * Get the currentText option, after initialization - * - * @param methodName 'option' - * @param optionName 'currentText' - */ - datepicker(methodName: "option", optionName: "currentText"): string; - /** - * Set the currentText option, after initialization - * - * @param methodName 'option' - * @param optionName 'currentText' - * @param currentTextValue The text to display for the current day link. Use the showButtonPanel option to display this button. - */ - datepicker(methodName: "option", optionName: "currentText", currentTextValue: string): JQuery; - - /** - * Get the dateFormat option, after initialization - * - * @param methodName 'option' - * @param optionName 'dateFormat' - */ - datepicker(methodName: "option", optionName: "dateFormat"): string; - /** - * Set the dateFormat option, after initialization - * - * @param methodName 'option' - * @param optionName 'dateFormat' - * @param dateFormatValue The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function. - */ - datepicker(methodName: "option", optionName: "dateFormat", dateFormatValue: string): JQuery; - - /** - * Get the dayNames option, after initialization - * - * @param methodName 'option' - * @param optionName 'dayNames' - */ - datepicker(methodName: "option", optionName: "dayNames"): string[]; - /** - * Set the dayNames option, after initialization - * - * @param methodName 'option' - * @param optionName 'dayNames' - * @param dayNamesValue The list of long day names, starting from Sunday, for use as requested via the dateFormat option. - */ - datepicker(methodName: "option", optionName: "dayNames", dayNamesValue: string[]): JQuery; - - /** - * Get the dayNamesMin option, after initialization - * - * @param methodName 'option' - * @param optionName 'dayNamesMin' - */ - datepicker(methodName: "option", optionName: "dayNamesMin"): string[]; - /** - * Set the dayNamesMin option, after initialization - * - * @param methodName 'option' - * @param optionName 'dayNamesMin' - * @param dayNamesMinValue The list of minimised day names, starting from Sunday, for use as column headers within the datepicker. - */ - datepicker(methodName: "option", optionName: "dayNamesMin", dayNamesMinValue: string[]): JQuery; - - /** - * Get the dayNamesShort option, after initialization - * - * @param methodName 'option' - * @param optionName 'dayNamesShort' - */ - datepicker(methodName: "option", optionName: "dayNamesShort"): string[]; - /** - * Set the dayNamesShort option, after initialization - * - * @param methodName 'option' - * @param optionName 'dayNamesShort' - * @param dayNamesShortValue The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option. - */ - datepicker(methodName: "option", optionName: "dayNamesShort", dayNamesShortValue: string[]): JQuery; - - /** - * Get the defaultDate option, after initialization - * - * @param methodName 'option' - * @param optionName 'defaultDate' - */ - datepicker(methodName: "option", optionName: "defaultDate"): any; - /** - * Set the defaultDate option, after initialization - * - * @param methodName 'option' - * @param optionName 'defaultDate' - * @param defaultDateValue A date object containing the default date. - */ - datepicker(methodName: "option", optionName: "defaultDate", defaultDateValue: Date): JQuery; - /** - * Set the defaultDate option, after initialization - * - * @param methodName 'option' - * @param optionName 'defaultDate' - * @param defaultDateValue A number of days from today. For example 2 represents two days from today and -1 represents yesterday. - */ - datepicker(methodName: "option", optionName: "defaultDate", defaultDateValue: number): JQuery; - /** - * Set the defaultDate option, after initialization - * - * @param methodName 'option' - * @param optionName 'defaultDate' - * @param defaultDateValue A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. - */ - datepicker(methodName: "option", optionName: "defaultDate", defaultDateValue: string): JQuery; - - /** - * Get the duration option, after initialization - * - * @param methodName 'option' - * @param optionName 'duration' - */ - datepicker(methodName: "option", optionName: "duration"): string; - /** - * Set the duration option, after initialization - * - * @param methodName 'option' - * @param optionName 'duration' - * @param durationValue Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast"). - */ - datepicker(methodName: "option", optionName: "duration", durationValue: string): JQuery; - - /** - * Get the firstDay option, after initialization - * - * @param methodName 'option' - * @param optionName 'firstDay' - */ - datepicker(methodName: "option", optionName: "firstDay"): number; - /** - * Set the firstDay option, after initialization - * - * @param methodName 'option' - * @param optionName 'firstDay' - * @param firstDayValue Set the first day of the week: Sunday is 0, Monday is 1, etc. - */ - datepicker(methodName: "option", optionName: "firstDay", firstDayValue: number): JQuery; - - /** - * Get the gotoCurrent option, after initialization - * - * @param methodName 'option' - * @param optionName 'gotoCurrent' - */ - datepicker(methodName: "option", optionName: "gotoCurrent"): boolean; - /** - * Set the gotoCurrent option, after initialization - * - * @param methodName 'option' - * @param optionName 'gotoCurrent' - * @param gotoCurrentValue When true, the current day link moves to the currently selected date instead of today. - */ - datepicker(methodName: "option", optionName: "gotoCurrent", gotoCurrentValue: boolean): JQuery; - - /** - * Gets the value currently associated with the specified optionName. - * - * @param methodName 'option' - * @param optionName The name of the option to get. - */ - datepicker(methodName: "option", optionName: string): any; - - datepicker(methodName: "option", optionName: string, ...otherParams: any[]): any; // Used for getting and setting options - - datepicker(methodName: string, ...otherParams: any[]): any; - - /** - * Initialize a datepicker with the given options - */ - datepicker(options: JQueryUI.DatepickerOptions): JQuery; - - dialog(): JQuery; - dialog(methodName: "close"): JQuery; - dialog(methodName: "destroy"): JQuery; - dialog(methodName: "isOpen"): boolean; - dialog(methodName: "moveToTop"): JQuery; - dialog(methodName: "open"): JQuery; - dialog(methodName: "widget"): JQuery; - dialog(methodName: string): JQuery; - dialog(options: JQueryUI.DialogOptions): JQuery; - dialog(optionLiteral: string, optionName: string): any; - dialog(optionLiteral: string, options: JQueryUI.DialogOptions): any; - dialog(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - draggable(): JQuery; - draggable(methodName: "destroy"): void; - draggable(methodName: "disable"): void; - draggable(methodName: "enable"): void; - draggable(methodName: "widget"): JQuery; - draggable(methodName: string): JQuery; - draggable(options: JQueryUI.DraggableOptions): JQuery; - draggable(optionLiteral: string, optionName: string): any; - draggable(optionLiteral: string, options: JQueryUI.DraggableOptions): any; - draggable(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - droppable(): JQuery; - droppable(methodName: "destroy"): void; - droppable(methodName: "disable"): void; - droppable(methodName: "enable"): void; - droppable(methodName: "widget"): JQuery; - droppable(methodName: string): JQuery; - droppable(options: JQueryUI.DroppableOptions): JQuery; - droppable(optionLiteral: string, optionName: string): any; - droppable(optionLiteral: string, options: JQueryUI.DraggableOptions): any; - droppable(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - menu(): JQuery; - menu(methodName: "blur"): void; - menu(methodName: "collapse", event?: JQueryEventObject): void; - menu(methodName: "collapseAll", event?: JQueryEventObject, all?: boolean): void; - menu(methodName: "destroy"): void; - menu(methodName: "disable"): void; - menu(methodName: "enable"): void; - menu(methodName: string, event: JQueryEventObject, item: JQuery): void; - menu(methodName: "focus", event: JQueryEventObject, item: JQuery): void; - menu(methodName: "isFirstItem"): boolean; - menu(methodName: "isLastItem"): boolean; - menu(methodName: "next", event?: JQueryEventObject): void; - menu(methodName: "nextPage", event?: JQueryEventObject): void; - menu(methodName: "previous", event?: JQueryEventObject): void; - menu(methodName: "previousPage", event?: JQueryEventObject): void; - menu(methodName: "refresh"): void; - menu(methodName: "select", event?: JQueryEventObject): void; - menu(methodName: "widget"): JQuery; - menu(methodName: string): JQuery; - menu(options: JQueryUI.MenuOptions): JQuery; - menu(optionLiteral: string, optionName: string): any; - menu(optionLiteral: string, options: JQueryUI.MenuOptions): any; - menu(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - progressbar(): JQuery; - progressbar(methodName: "destroy"): void; - progressbar(methodName: "disable"): void; - progressbar(methodName: "enable"): void; - progressbar(methodName: "refresh"): void; - progressbar(methodName: "value"): any; // number or boolean - progressbar(methodName: "value", value: number): void; - progressbar(methodName: "value", value: boolean): void; - progressbar(methodName: "widget"): JQuery; - progressbar(methodName: string): JQuery; - progressbar(options: JQueryUI.ProgressbarOptions): JQuery; - progressbar(optionLiteral: string, optionName: string): any; - progressbar(optionLiteral: string, options: JQueryUI.ProgressbarOptions): any; - progressbar(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - resizable(): JQuery; - resizable(methodName: "destroy"): void; - resizable(methodName: "disable"): void; - resizable(methodName: "enable"): void; - resizable(methodName: "widget"): JQuery; - resizable(methodName: string): JQuery; - resizable(options: JQueryUI.ResizableOptions): JQuery; - resizable(optionLiteral: string, optionName: string): any; - resizable(optionLiteral: string, options: JQueryUI.ResizableOptions): any; - resizable(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - selectable(): JQuery; - selectable(methodName: "destroy"): void; - selectable(methodName: "disable"): void; - selectable(methodName: "enable"): void; - selectable(methodName: "widget"): JQuery; - selectable(methodName: string): JQuery; - selectable(options: JQueryUI.SelectableOptions): JQuery; - selectable(optionLiteral: string, optionName: string): any; - selectable(optionLiteral: string, options: JQueryUI.SelectableOptions): any; - selectable(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - // TODO: VSTS 7733811 Define parameter types for jqueryUI selectmenu - selectmenu(option: any): JQuery; - - slider(): JQuery; - slider(methodName: "destroy"): void; - slider(methodName: "disable"): void; - slider(methodName: "enable"): void; - slider(methodName: "refresh"): void; - slider(methodName: "value"): number; - slider(methodName: "value", value: number): void; - slider(methodName: "values"): Array; - slider(methodName: "values", index: number): number; - slider(methodName: string, index: number, value: number): void; - slider(methodName: "values", index: number, value: number): void; - slider(methodName: string, values: Array): void; - slider(methodName: "values", values: Array): void; - slider(methodName: "widget"): JQuery; - slider(methodName: string): JQuery; - slider(options: JQueryUI.SliderOptions): JQuery; - slider(optionLiteral: string, optionName: string): any; - slider(optionLiteral: string, options: JQueryUI.SliderOptions): any; - slider(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - sortable(): JQuery; - sortable(methodName: "destroy"): void; - sortable(methodName: "disable"): void; - sortable(methodName: "enable"): void; - sortable(methodName: "widget"): JQuery; - sortable(methodName: "toArray"): string[]; - sortable(methodName: string): JQuery; - sortable(options: JQueryUI.SortableOptions): JQuery; - sortable(optionLiteral: string, optionName: string): any; - sortable(optionLiteral: string, options: JQueryUI.SortableOptions): any; - sortable(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - spinner(): JQuery; - spinner(methodName: "destroy"): void; - spinner(methodName: "disable"): void; - spinner(methodName: "enable"): void; - spinner(methodName: "pageDown", pages?: number): void; - spinner(methodName: "pageUp", pages?: number): void; - spinner(methodName: "stepDown", steps?: number): void; - spinner(methodName: "stepUp", steps?: number): void; - spinner(methodName: "value"): number; - spinner(methodName: "value", value: number): void; - spinner(methodName: "widget"): JQuery; - spinner(methodName: string): JQuery; - spinner(options: JQueryUI.SpinnerOptions): JQuery; - spinner(optionLiteral: string, optionName: string): any; - spinner(optionLiteral: string, options: JQueryUI.SpinnerOptions): any; - spinner(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - tabs(): JQuery; - tabs(methodName: "destroy"): void; - tabs(methodName: "disable"): void; - tabs(methodName: "enable"): void; - tabs(methodName: "load", index: number): void; - tabs(methodName: "refresh"): void; - tabs(methodName: "widget"): JQuery; - tabs(methodName: string): JQuery; - tabs(options: JQueryUI.TabsOptions): JQuery; - tabs(optionLiteral: string, optionName: string): any; - tabs(optionLiteral: string, options: JQueryUI.TabsOptions): any; - tabs(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - tooltip(): JQuery; - tooltip(methodName: "destroy"): void; - tooltip(methodName: "disable"): void; - tooltip(methodName: "enable"): void; - tooltip(methodName: "open"): void; - tooltip(methodName: "close"): void; - tooltip(methodName: "widget"): JQuery; - tooltip(methodName: string): JQuery; - tooltip(options: JQueryUI.TooltipOptions): JQuery; - tooltip(optionLiteral: string, optionName: string): any; - tooltip(optionLiteral: string, options: JQueryUI.TooltipOptions): any; - tooltip(optionLiteral: string, optionName: string, optionValue: any): JQuery; - - addClass(classNames: string, speed?: number, callback?: Function): JQuery; - addClass(classNames: string, speed?: string, callback?: Function): JQuery; - addClass(classNames: string, speed?: number, easing?: string, callback?: Function): JQuery; - addClass(classNames: string, speed?: string, easing?: string, callback?: Function): JQuery; - - removeClass(classNames: string, speed?: number, callback?: Function): JQuery; - removeClass(classNames: string, speed?: string, callback?: Function): JQuery; - removeClass(classNames: string, speed?: number, easing?: string, callback?: Function): JQuery; - removeClass(classNames: string, speed?: string, easing?: string, callback?: Function): JQuery; - - switchClass( - removeClassName: string, - addClassName: string, - duration?: number, - easing?: string, - complete?: Function - ): JQuery; - switchClass( - removeClassName: string, - addClassName: string, - duration?: string, - easing?: string, - complete?: Function - ): JQuery; - - toggleClass(className: string, duration?: number, easing?: string, complete?: Function): JQuery; - toggleClass(className: string, duration?: string, easing?: string, complete?: Function): JQuery; - toggleClass(className: string, aswitch?: boolean, duration?: number, easing?: string, complete?: Function): JQuery; - toggleClass(className: string, aswitch?: boolean, duration?: string, easing?: string, complete?: Function): JQuery; - - effect(options: any): JQuery; - effect(effect: string, options?: any, duration?: number, complete?: Function): JQuery; - effect(effect: string, options?: any, duration?: string, complete?: Function): JQuery; - - hide(options: any): JQuery; - hide(effect: string, options?: any, duration?: number, complete?: Function): JQuery; - hide(effect: string, options?: any, duration?: string, complete?: Function): JQuery; - - show(options: any): JQuery; - show(effect: string, options?: any, duration?: number, complete?: Function): JQuery; - show(effect: string, options?: any, duration?: string, complete?: Function): JQuery; - - toggle(options: any): JQuery; - toggle(effect: string, options?: any, duration?: number, complete?: Function): JQuery; - toggle(effect: string, options?: any, duration?: string, complete?: Function): JQuery; - - position(options: JQueryUI.JQueryPositionOptions): JQuery; - - enableSelection(): JQuery; - disableSelection(): JQuery; - focus(delay: number, callback?: Function): JQuery; - uniqueId(): JQuery; - removeUniqueId(): JQuery; - scrollParent(): JQuery; - zIndex(): JQuery; - zIndex(zIndex: number): JQuery; - - widget: JQueryUI.Widget; - - jQuery: JQueryStatic; -} - -interface JQueryStatic { - ui: JQueryUI.UI; - datepicker: JQueryUI.Datepicker; - widget: JQueryUI.Widget; - Widget: JQueryUI.Widget; -} +// Type definitions for jQueryUI 1.9 +// Project: http://jqueryui.com/ +// Definitions by: Boris Yankov , John Reilly +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/// + +declare namespace JQueryUI { + // Accordion ////////////////////////////////////////////////// + + interface AccordionOptions { + active?: any; // boolean or number + animate?: any; // boolean, number, string or object + collapsible?: boolean; + disabled?: boolean; + event?: string; + header?: string; + heightStyle?: string; + icons?: any; + } + + interface AccordionUIParams { + newHeader: JQuery; + oldHeader: JQuery; + newPanel: JQuery; + oldPanel: JQuery; + } + + interface AccordionEvent { + (event: Event, ui: AccordionUIParams): void; + } + + interface AccordionEvents { + activate?: AccordionEvent; + beforeActivate?: AccordionEvent; + create?: AccordionEvent; + } + + interface Accordion extends Widget, AccordionOptions, AccordionEvents {} + + // Autocomplete ////////////////////////////////////////////////// + + interface AutocompleteOptions { + appendTo?: any; //Selector; + autoFocus?: boolean; + delay?: number; + disabled?: boolean; + minLength?: number; + position?: string; + source?: any; // [], string or () + } + + interface AutocompleteUIParams {} + + interface AutocompleteEvent { + (event: Event, ui: AutocompleteUIParams): void; + } + + interface AutocompleteEvents { + change?: AutocompleteEvent; + close?: AutocompleteEvent; + create?: AutocompleteEvent; + focus?: AutocompleteEvent; + open?: AutocompleteEvent; + response?: AutocompleteEvent; + search?: AutocompleteEvent; + select?: AutocompleteEvent; + } + + interface Autocomplete extends Widget, AutocompleteOptions, AutocompleteEvents { + escapeRegex: (value: string) => string; + } + + // Button ////////////////////////////////////////////////// + + interface ButtonOptions { + disabled?: boolean; + icons?: any; + label?: string; + text?: boolean; + } + + interface Button extends Widget, ButtonOptions {} + + // Datepicker ////////////////////////////////////////////////// + + interface DatepickerOptions { + /** + * An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + altField?: any; // Selector, jQuery or Element + /** + * The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function + */ + altFormat?: string; + /** + * The text to display after each date field, e.g., to show the required format. + */ + appendText?: string; + /** + * Set to true to automatically resize the input field to accommodate dates in the current dateFormat. + */ + autoSize?: boolean; + /** + * A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed. + */ + beforeShow?: (input: Element, inst: any) => JQueryUI.DatepickerOptions; + /** + * A function that takes a date as a parameter and must return an array with: + * [0]: true/false indicating whether or not this date is selectable + * [1]: a CSS class name to add to the date's cell or "" for the default presentation + * [2]: an optional popup tooltip for this date + * The function is called for each day in the datepicker before it is displayed. + */ + beforeShowDay?: (date: Date) => any[]; + /** + * A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed. + */ + buttonImage?: string; + /** + * Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set. + */ + buttonImageOnly?: boolean; + /** + * The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both". + */ + buttonText?: string; + /** + * A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year. + */ + calculateWeek?: (date: Date) => string; + /** + * Whether the month should be rendered as a dropdown instead of text. + */ + changeMonth?: boolean; + /** + * Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection. + */ + changeYear?: boolean; + /** + * The text to display for the close link. Use the showButtonPanel option to display this button. + */ + closeText?: string; + /** + * When true, entry in the input field is constrained to those characters allowed by the current dateFormat option. + */ + constrainInput?: boolean; + /** + * The text to display for the current day link. Use the showButtonPanel option to display this button. + */ + currentText?: string; + /** + * The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function. + */ + dateFormat?: string; + /** + * The list of long day names, starting from Sunday, for use as requested via the dateFormat option. + */ + dayNames?: string[]; + /** + * The list of minimised day names, starting from Sunday, for use as column headers within the datepicker. + */ + dayNamesMin?: string[]; + /** + * The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option. + */ + dayNamesShort?: string[]; + /** + * Set the date to highlight on first opening if the field is blank. Specify either an actual date via a Date object or as a string in the current dateFormat, or a number of days from today (e.g. +7) or a string of values and periods ('y' for years, 'm' for months, 'w' for weeks, 'd' for days, e.g. '+1m +7d'), or null for today. + * Multiple types supported: + * Date: A date object containing the default date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + defaultDate?: any; // Date, number or string + /** + * Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast"). + */ + duration?: string; + /** + * Set the first day of the week: Sunday is 0, Monday is 1, etc. + */ + firstDay?: number; + /** + * When true, the current day link moves to the currently selected date instead of today. + */ + gotoCurrent?: boolean; + /** + * Normally the previous and next links are disabled when not applicable (see the minDate and maxDate options). You can hide them altogether by setting this attribute to true. + */ + hideIfNoPrevNext?: boolean; + /** + * Whether the current language is drawn from right to left. + */ + isRTL?: boolean; + /** + * The maximum selectable date. When set to null, there is no maximum. + * Multiple types supported: + * Date: A date object containing the maximum date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + maxDate?: any; // Date, number or string + /** + * The minimum selectable date. When set to null, there is no minimum. + * Multiple types supported: + * Date: A date object containing the minimum date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + minDate?: any; // Date, number or string + /** + * The list of full month names, for use as requested via the dateFormat option. + */ + monthNames?: string[]; + /** + * The list of abbreviated month names, as used in the month header on each datepicker and as requested via the dateFormat option. + */ + monthNamesShort?: string[]; + /** + * Whether the prevText and nextText options should be parsed as dates by the formatDate function, allowing them to display the target month names for example. + */ + navigationAsDateFormat?: boolean; + /** + * The text to display for the next month link. With the standard ThemeRoller styling, this value is replaced by an icon. + */ + nextText?: string; + /** + * The number of months to show at once. + * Multiple types supported: + * Number: The number of months to display in a single row. + * Array: An array defining the number of rows and columns to display. + */ + numberOfMonths?: any; // number or number[] + /** + * Called when the datepicker moves to a new month and/or year. The function receives the selected year, month (1-12), and the datepicker instance as parameters. this refers to the associated input field. + */ + onChangeMonthYear?: (year: number, month: number, inst: any) => void; + /** + * Called when the datepicker is closed, whether or not a date is selected. The function receives the selected date as text ("" if none) and the datepicker instance as parameters. this refers to the associated input field. + */ + onClose?: (dateText: string, inst: any) => void; + /** + * Called when the datepicker is selected. The function receives the selected date as text and the datepicker instance as parameters. this refers to the associated input field. + */ + onSelect?: (dateText: string, inst: any) => void; + /** + * The text to display for the previous month link. With the standard ThemeRoller styling, this value is replaced by an icon. + */ + prevText?: string; + /** + * Whether days in other months shown before or after the current month are selectable. This only applies if the showOtherMonths option is set to true. + */ + selectOtherMonths?: boolean; + /** + * The cutoff year for determining the century for a date (used in conjunction with dateFormat 'y'). Any dates entered with a year value less than or equal to the cutoff year are considered to be in the current century, while those greater than it are deemed to be in the previous century. + * Multiple types supported: + * Number: A value between 0 and 99 indicating the cutoff year. + * String: A relative number of years from the current year, e.g., "+3" or "-5". + */ + shortYearCutoff?: any; // number or string + /** + * The name of the animation used to show and hide the datepicker. Use "show" (the default), "slideDown", "fadeIn", any of the jQuery UI effects. Set to an empty string to disable animation. + */ + showAnim?: string; + /** + * Whether to display a button pane underneath the calendar. The button pane contains two buttons, a Today button that links to the current day, and a Done button that closes the datepicker. The buttons' text can be customized using the currentText and closeText options respectively. + */ + showButtonPanel?: boolean; + /** + * When displaying multiple months via the numberOfMonths option, the showCurrentAtPos option defines which position to display the current month in. + */ + showCurrentAtPos?: number; + /** + * Whether to show the month after the year in the header. + */ + showMonthAfterYear?: boolean; + /** + * When the datepicker should appear. The datepicker can appear when the field receives focus ("focus"), when a button is clicked ("button"), or when either event occurs ("both"). + */ + showOn?: string; + /** + * If using one of the jQuery UI effects for the showAnim option, you can provide additional settings for that animation via this option. + */ + showOptions?: any; // TODO + /** + * Whether to display dates in other months (non-selectable) at the start or end of the current month. To make these days selectable use the selectOtherMonths option. + */ + showOtherMonths?: boolean; + /** + * When true, a column is added to show the week of the year. The calculateWeek option determines how the week of the year is calculated. You may also want to change the firstDay option. + */ + showWeek?: boolean; + /** + * Set how many months to move when clicking the previous/next links. + */ + stepMonths?: number; + /** + * The text to display for the week of the year column heading. Use the showWeek option to display this column. + */ + weekHeader?: string; + /** + * The range of years displayed in the year drop-down: either relative to today's year ("-nn:+nn"), relative to the currently selected year ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats ("nnnn:-nn"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the minDate and/or maxDate options. + */ + yearRange?: string; + /** + * Additional text to display after the year in the month headers. + */ + yearSuffix?: string; + } + + interface DatepickerFormatDateOptions { + dayNamesShort?: string[]; + dayNames?: string[]; + monthNamesShort?: string[]; + monthNames?: string[]; + } + + interface Datepicker extends Widget, DatepickerOptions { + regional: { [languageCod3: string]: any }; + setDefaults(defaults: DatepickerOptions): void; + formatDate(format: string, date: Date, settings?: DatepickerFormatDateOptions): string; + parseDate(format: string, date: string, settings?: DatepickerFormatDateOptions): Date; + iso8601Week(date: Date): number; + noWeekends(date: Date): any[]; + } + + // Dialog ////////////////////////////////////////////////// + + interface DialogOptions { + autoOpen?: boolean; + buttons?: any; // object or [] + closeOnEscape?: boolean; + closeText?: string; + dialogClass?: string; + disabled?: boolean; + draggable?: boolean; + height?: any; // number or string + maxHeight?: number; + maxWidth?: number; + minHeight?: number; + minWidth?: number; + modal?: boolean; + position?: any; // object, string or [] + resizable?: boolean; + show?: any; // number, string or object + stack?: boolean; + title?: string; + width?: any; // number or string + zIndex?: number; + + close?: DialogEvent; + } + + interface DialogUIParams {} + + interface DialogEvent { + (event: Event, ui: DialogUIParams): void; + } + + interface DialogEvents { + beforeClose?: DialogEvent; + close?: DialogEvent; + create?: DialogEvent; + drag?: DialogEvent; + dragStart?: DialogEvent; + dragStop?: DialogEvent; + focus?: DialogEvent; + open?: DialogEvent; + resize?: DialogEvent; + resizeStart?: DialogEvent; + resizeStop?: DialogEvent; + } + + interface Dialog extends Widget, DialogOptions, DialogEvents {} + + // Draggable ////////////////////////////////////////////////// + + interface DraggableEventUIParams { + helper: JQuery; + position: { top: number; left: number }; + offset: { top: number; left: number }; + } + + interface DraggableEvent { + (event: Event, ui: DraggableEventUIParams): void; + } + + interface DraggableOptions { + disabled?: boolean; + addClasses?: boolean; + appendTo?: any; + axis?: string; + cancel?: string; + connectToSortable?: string; + containment?: any; + cursor?: string; + cursorAt?: any; + delay?: number; + distance?: number; + grid?: number[]; + handle?: any; + helper?: any; + iframeFix?: any; + opacity?: number; + refreshPositions?: boolean; + revert?: any; + revertDuration?: number; + scope?: string; + scroll?: boolean; + scrollSensitivity?: number; + scrollSpeed?: number; + snap?: any; + snapMode?: string; + snapTolerance?: number; + stack?: string; + zIndex?: number; + } + + interface DraggableEvents { + create?: DraggableEvent; + start?: DraggableEvent; + drag?: DraggableEvent; + stop?: DraggableEvent; + } + + interface Draggable extends Widget, DraggableOptions, DraggableEvent {} + + // Droppable ////////////////////////////////////////////////// + + interface DroppableEventUIParam { + draggable: JQuery; + helper: JQuery; + position: { top: number; left: number }; + offset: { top: number; left: number }; + } + + interface DroppableEvent { + (event: Event, ui: DroppableEventUIParam): void; + } + + interface DroppableOptions { + disabled?: boolean; + accept?: any; + activeClass?: string; + greedy?: boolean; + hoverClass?: string; + scope?: string; + tolerance?: string; + } + + interface DroppableEvents { + create?: DroppableEvent; + activate?: DroppableEvent; + deactivate?: DroppableEvent; + over?: DroppableEvent; + out?: DroppableEvent; + drop?: DroppableEvent; + } + + interface Droppable extends Widget, DroppableOptions, DroppableEvents {} + + // Menu ////////////////////////////////////////////////// + + interface MenuOptions { + disabled?: boolean; + icons?: any; + menus?: string; + position?: any; // TODO + role?: string; + } + + interface MenuUIParams {} + + interface MenuEvent { + (event: Event, ui: MenuUIParams): void; + } + + interface MenuEvents { + blur?: MenuEvent; + create?: MenuEvent; + focus?: MenuEvent; + select?: MenuEvent; + } + + interface Menu extends Widget, MenuOptions, MenuEvents {} + + // Progressbar ////////////////////////////////////////////////// + + interface ProgressbarOptions { + disabled?: boolean; + value?: number; + } + + interface ProgressbarUIParams {} + + interface ProgressbarEvent { + (event: Event, ui: ProgressbarUIParams): void; + } + + interface ProgressbarEvents { + change?: ProgressbarEvent; + complete?: ProgressbarEvent; + create?: ProgressbarEvent; + } + + interface Progressbar extends Widget, ProgressbarOptions, ProgressbarEvents {} + + // Resizable ////////////////////////////////////////////////// + + interface ResizableOptions { + alsoResize?: any; // Selector, JQuery or Element + animate?: boolean; + animateDuration?: any; // number or string + animateEasing?: string; + aspectRatio?: any; // boolean or number + autoHide?: boolean; + cancel?: string; + containment?: any; // Selector, Element or string + delay?: number; + disabled?: boolean; + distance?: number; + ghost?: boolean; + grid?: any; + handles?: any; // string or object + helper?: string; + maxHeight?: number; + maxWidth?: number; + minHeight?: number; + minWidth?: number; + resizeHeight?: boolean; + create?: ResizableEvent; + resize?: ResizableEvent; + start?: ResizableEvent; + stop?: ResizableEvent; + } + + interface ResizableUIParams { + element: JQuery; + helper: JQuery; + originalElement: JQuery; + originalPosition: any; + originalSize: any; + position: any; + size: any; + } + + interface ResizableEvent { + (event: Event, ui: ResizableUIParams): void; + } + + interface ResizableEvents { + resize?: ResizableEvent; + start?: ResizableEvent; + stop?: ResizableEvent; + } + + interface Resizable extends Widget, ResizableOptions, ResizableEvents {} + + // Selectable ////////////////////////////////////////////////// + + interface SelectableOptions { + autoRefresh?: boolean; + cancel?: string; + delay?: number; + disabled?: boolean; + distance?: number; + filter?: string; + tolerance?: string; + } + + interface SelectableEvents { + selected?(event: Event, ui: { selected?: Element }): void; + selecting?(event: Event, ui: { selecting?: Element }): void; + start?(event: Event, ui: any): void; + stop?(event: Event, ui: any): void; + unselected?(event: Event, ui: { unselected: Element }): void; + unselecting?(event: Event, ui: { unselecting: Element }): void; + } + + interface Selectable extends Widget, SelectableOptions, SelectableEvents {} + + // Slider ////////////////////////////////////////////////// + + interface SliderOptions { + animate?: any; // boolean, string or number + disabled?: boolean; + max?: number; + min?: number; + orientation?: string; + range?: any; // boolean or string + step?: number; + value?: number; + values?: number[]; + } + + interface SliderUIParams { + handle?: JQuery; + value?: number; + values?: number[]; + } + + interface SliderEvent { + (event: Event, ui: SliderUIParams): void; + } + + interface SliderEvents { + change?: SliderEvent; + create?: SliderEvent; + slide?: SliderEvent; + start?: SliderEvent; + stop?: SliderEvent; + } + + interface Slider extends Widget, SliderOptions, SliderEvents {} + + // Sortable ////////////////////////////////////////////////// + + interface SortableOptions extends SortableEvents { + appendTo?: any; // jQuery, Element, Selector or string + axis?: string; + cancel?: any; // Selector + connectWith?: any; // Selector + containment?: any; // Element, Selector or string + cursor?: string; + cursorAt?: any; + delay?: number; + disabled?: boolean; + distance?: number; + dropOnEmpty?: boolean; + forceHelperSize?: boolean; + forcePlaceholderSize?: boolean; + grid?: number[]; + handle?: any; // Selector or Element + items?: any; // Selector + opacity?: number; + placeholder?: string; + revert?: any; // boolean or number + scroll?: boolean; + scrollSensitivity?: number; + scrollSpeed?: number; + tolerance?: string; + zIndex?: number; + } + + interface SortableUIParams { + helper: JQuery; + item: JQuery; + offset: any; + position: any; + originalPosition: any; + sender: JQuery; + placeholder: JQuery; + } + + interface SortableEvent { + (event: JQueryEventObject, ui: SortableUIParams): void; + } + + interface SortableEvents { + activate?: SortableEvent; + beforeStop?: SortableEvent; + change?: SortableEvent; + deactivate?: SortableEvent; + out?: SortableEvent; + over?: SortableEvent; + receive?: SortableEvent; + remove?: SortableEvent; + sort?: SortableEvent; + start?: SortableEvent; + stop?: SortableEvent; + update?: SortableEvent; + } + + interface Sortable extends Widget, SortableOptions, SortableEvents {} + + // Spinner ////////////////////////////////////////////////// + + interface SpinnerOptions { + culture?: string; + disabled?: boolean; + icons?: any; + incremental?: any; // boolean or () + max?: any; // number or string + min?: any; // number or string + numberFormat?: string; + page?: number; + step?: any; // number or string + } + + interface SpinnerUIParams {} + + interface SpinnerEvent { + (event: Event, ui: SpinnerUIParams): void; + } + + interface SpinnerEvents { + spin?: SpinnerEvent; + start?: SpinnerEvent; + stop?: SpinnerEvent; + } + + interface Spinner extends Widget, SpinnerOptions, SpinnerEvents {} + + // Tabs ////////////////////////////////////////////////// + + interface TabsOptions { + active?: any; // boolean or number + collapsible?: boolean; + disabled?: any; // boolean or [] + event?: string; + heightStyle?: string; + hide?: any; // boolean, number, string or object + show?: any; // boolean, number, string or object + + activate?: TabsEvent; + } + + interface TabsUIParams { + newTab: JQuery; + oldTab: JQuery; + newPanel: JQuery; + oldPanel: JQuery; + } + + interface TabsEvent { + (event: Event, ui: TabsUIParams): void; + } + + interface TabsEvents { + activate?: TabsEvent; + beforeActivate?: TabsEvent; + beforeLoad?: TabsEvent; + load?: TabsEvent; + } + + interface Tabs extends Widget, TabsOptions, TabsEvents {} + + // Tooltip ////////////////////////////////////////////////// + + interface TooltipOptions { + content?: any; // () or string + disabled?: boolean; + hide?: any; // boolean, number, string or object + items?: string; + position?: any; // TODO + show?: any; // boolean, number, string or object + tooltipClass?: string; + track?: boolean; + } + + interface TooltipUIParams {} + + interface TooltipEvent { + (event: Event, ui: TooltipUIParams): void; + } + + interface TooltipEvents { + close?: TooltipEvent; + open?: TooltipEvent; + } + + interface Tooltip extends Widget, TooltipOptions, TooltipEvents {} + + // Effects ////////////////////////////////////////////////// + + interface EffectOptions { + effect: string; + easing?: string; + duration: any; + complete: Function; + } + + interface BlindEffect { + direction?: string; + } + + interface BounceEffect { + distance?: number; + times?: number; + } + + interface ClipEffect { + direction?: number; + } + + interface DropEffect { + direction?: number; + } + + interface ExplodeEffect { + pieces?: number; + } + + interface FadeEffect {} + + interface FoldEffect { + size?: any; + horizFirst?: boolean; + } + + interface HighlightEffect { + color?: string; + } + + interface PuffEffect { + percent?: number; + } + + interface PulsateEffect { + times?: number; + } + + interface ScaleEffect { + direction?: string; + origin?: string[]; + percent?: number; + scale?: string; + } + + interface ShakeEffect { + direction?: string; + distance?: number; + times?: number; + } + + interface SizeEffect { + to?: any; + origin?: string[]; + scale?: string; + } + + interface SlideEffect { + direction?: string; + distance?: number; + } + + interface TransferEffect { + className?: string; + to?: string; + } + + interface JQueryPositionOptions { + my?: string; + at?: string; + of?: any; + collision?: string; + using?: Function; + within?: any; + } + + // UI ////////////////////////////////////////////////// + + interface MouseOptions { + cancel?: string; + delay?: number; + distance?: number; + } + + interface KeyCode { + BACKSPACE: number; + COMMA: number; + DELETE: number; + DOWN: number; + END: number; + ENTER: number; + ESCAPE: number; + HOME: number; + LEFT: number; + NUMPAD_ADD: number; + NUMPAD_DECIMAL: number; + NUMPAD_DIVIDE: number; + NUMPAD_ENTER: number; + NUMPAD_MULTIPLY: number; + NUMPAD_SUBTRACT: number; + PAGE_DOWN: number; + PAGE_UP: number; + PERIOD: number; + RIGHT: number; + SPACE: number; + TAB: number; + UP: number; + } + + interface UI { + mouse(method: string): JQuery; + mouse(options: MouseOptions): JQuery; + mouse(optionLiteral: string, optionName: string, optionValue: any): JQuery; + mouse(optionLiteral: string, optionValue: any): any; + + accordion: Accordion; + autocomplete: Autocomplete; + button: Button; + buttonset: Button; + datepicker: Datepicker; + dialog: Dialog; + keyCode: KeyCode; + menu: Menu; + progressbar: Progressbar; + slider: Slider; + spinner: Spinner; + tabs: Tabs; + tooltip: Tooltip; + version: string; + } + + // Widget ////////////////////////////////////////////////// + + interface WidgetOptions { + disabled?: boolean; + hide?: any; + show?: any; + } + + interface Widget { + (methodName: string): JQuery; + (options: WidgetOptions): JQuery; + (options: AccordionOptions): JQuery; + (optionLiteral: string, optionName: string): any; + (optionLiteral: string, options: WidgetOptions): any; + (optionLiteral: string, optionName: string, optionValue: any): JQuery; + + (name: string, prototype: any): JQuery; + (name: string, base: Function, prototype: any): JQuery; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////// +} + +interface JQuery { + accordion(): JQuery; + accordion(methodName: "destroy"): void; + accordion(methodName: "disable"): void; + accordion(methodName: "enable"): void; + accordion(methodName: "refresh"): void; + accordion(methodName: "widget"): JQuery; + accordion(methodName: string): JQuery; + accordion(options: JQueryUI.AccordionOptions): JQuery; + accordion(optionLiteral: string, optionName: string): any; + accordion(optionLiteral: string, options: JQueryUI.AccordionOptions): any; + accordion(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + autocomplete(): JQuery; + autocomplete(methodName: "close"): void; + autocomplete(methodName: "destroy"): void; + autocomplete(methodName: "disable"): void; + autocomplete(methodName: "enable"): void; + autocomplete(methodName: "search", value?: string): void; + autocomplete(methodName: "widget"): JQuery; + autocomplete(methodName: string): JQuery; + autocomplete(options: JQueryUI.AutocompleteOptions): JQuery; + autocomplete(optionLiteral: string, optionName: string): any; + autocomplete(optionLiteral: string, options: JQueryUI.AutocompleteOptions): any; + autocomplete(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + button(): JQuery; + button(methodName: "destroy"): void; + button(methodName: "disable"): void; + button(methodName: "enable"): void; + button(methodName: "refresh"): void; + button(methodName: "widget"): JQuery; + button(methodName: string): JQuery; + button(options: JQueryUI.ButtonOptions): JQuery; + button(optionLiteral: string, optionName: string): any; + button(optionLiteral: string, options: JQueryUI.ButtonOptions): any; + button(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + buttonset(): JQuery; + buttonset(methodName: "destroy"): void; + buttonset(methodName: "disable"): void; + buttonset(methodName: "enable"): void; + buttonset(methodName: "refresh"): void; + buttonset(methodName: "widget"): JQuery; + buttonset(methodName: string): JQuery; + buttonset(options: JQueryUI.ButtonOptions): JQuery; + buttonset(optionLiteral: string, optionName: string): any; + buttonset(optionLiteral: string, options: JQueryUI.ButtonOptions): any; + buttonset(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + /** + * Initialize a datepicker + */ + datepicker(): JQuery; + /** + * Removes the datepicker functionality completely. This will return the element back to its pre-init state. + * + * @param methodName 'destroy' + */ + datepicker(methodName: "destroy"): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker( + methodName: "dialog", + date: Date, + onSelect?: () => void, + settings?: JQueryUI.DatepickerOptions, + pos?: number[] + ): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker( + methodName: "dialog", + date: Date, + onSelect?: () => void, + settings?: JQueryUI.DatepickerOptions, + pos?: MouseEvent + ): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker( + methodName: "dialog", + date: string, + onSelect?: () => void, + settings?: JQueryUI.DatepickerOptions, + pos?: number[] + ): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker( + methodName: "dialog", + date: string, + onSelect?: () => void, + settings?: JQueryUI.DatepickerOptions, + pos?: MouseEvent + ): JQuery; + /** + * Returns the current date for the datepicker or null if no date has been selected. + * + * @param methodName 'getDate' + */ + datepicker(methodName: "getDate"): Date; + /** + * Close a previously opened date picker. + * + * @param methodName 'hide' + */ + datepicker(methodName: "hide"): JQuery; + /** + * Determine whether a date picker has been disabled. + * + * @param methodName 'isDisabled' + */ + datepicker(methodName: "isDisabled"): boolean; + /** + * Redraw the date picker, after having made some external modifications. + * + * @param methodName 'refresh' + */ + datepicker(methodName: "refresh"): JQuery; + /** + * Sets the date for the datepicker. The new date may be a Date object or a string in the current date format (e.g., "01/26/2009"), a number of days from today (e.g., +7) or a string of values and periods ("y" for years, "m" for months, "w" for weeks, "d" for days, e.g., "+1m +7d"), or null to clear the selected date. + * + * @param methodName 'setDate' + * @param date The new date. + */ + datepicker(methodName: "setDate", date: Date): JQuery; + /** + * Sets the date for the datepicker. The new date may be a Date object or a string in the current date format (e.g., "01/26/2009"), a number of days from today (e.g., +7) or a string of values and periods ("y" for years, "m" for months, "w" for weeks, "d" for days, e.g., "+1m +7d"), or null to clear the selected date. + * + * @param methodName 'setDate' + * @param date The new date. + */ + datepicker(methodName: "setDate", date: string): JQuery; + /** + * Open the date picker. If the datepicker is attached to an input, the input must be visible for the datepicker to be shown. + * + * @param methodName 'show' + */ + datepicker(methodName: "show"): JQuery; + /** + * Returns a jQuery object containing the datepicker. + * + * @param methodName 'widget' + */ + datepicker(methodName: "widget"): JQuery; + + /** + * Get the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + */ + datepicker(methodName: "option", optionName: "altField"): any; + /** + * Set the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + datepicker(methodName: "option", optionName: "altField", altFieldValue: string): JQuery; + /** + * Set the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + datepicker(methodName: "option", optionName: "altField", altFieldValue: JQuery): JQuery; + /** + * Set the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + datepicker(methodName: "option", optionName: "altField", altFieldValue: Element): JQuery; + + /** + * Get the altFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'altFormat' + */ + datepicker(methodName: "option", optionName: "altFormat"): string; + /** + * Set the altFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'altFormat' + * @param altFormatValue The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function + */ + datepicker(methodName: "option", optionName: "altFormat", altFormatValue: string): JQuery; + + /** + * Get the appendText option, after initialization + * + * @param methodName 'option' + * @param optionName 'appendText' + */ + datepicker(methodName: "option", optionName: "appendText"): string; + /** + * Set the appendText option, after initialization + * + * @param methodName 'option' + * @param optionName 'appendText' + * @param appendTextValue The text to display after each date field, e.g., to show the required format. + */ + datepicker(methodName: "option", optionName: "appendText", appendTextValue: string): JQuery; + + /** + * Get the autoSize option, after initialization + * + * @param methodName 'option' + * @param optionName 'autoSize' + */ + datepicker(methodName: "option", optionName: "autoSize"): boolean; + /** + * Set the autoSize option, after initialization + * + * @param methodName 'option' + * @param optionName 'autoSize' + * @param autoSizeValue Set to true to automatically resize the input field to accommodate dates in the current dateFormat. + */ + datepicker(methodName: "option", optionName: "autoSize", autoSizeValue: boolean): JQuery; + + /** + * Get the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShow' + */ + datepicker(methodName: "option", optionName: "beforeShow"): (input: Element, inst: any) => JQueryUI.DatepickerOptions; + /** + * Set the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShow' + * @param beforeShowValue A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed. + */ + datepicker( + methodName: "option", + optionName: "beforeShow", + beforeShowValue: (input: Element, inst: any) => JQueryUI.DatepickerOptions + ): JQuery; + + /** + * Get the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShowDay' + */ + datepicker(methodName: "option", optionName: "beforeShowDay"): (date: Date) => any[]; + /** + * Set the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShowDay' + * @param beforeShowDayValue A function that takes a date as a parameter and must return an array with: + * [0]: true/false indicating whether or not this date is selectable + * [1]: a CSS class name to add to the date's cell or "" for the default presentation + * [2]: an optional popup tooltip for this date + * The function is called for each day in the datepicker before it is displayed. + */ + datepicker(methodName: "option", optionName: "beforeShowDay", beforeShowDayValue: (date: Date) => any[]): JQuery; + + /** + * Get the buttonImage option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImage' + */ + datepicker(methodName: "option", optionName: "buttonImage"): string; + /** + * Set the buttonImage option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImage' + * @param buttonImageValue A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed. + */ + datepicker(methodName: "option", optionName: "buttonImage", buttonImageValue: string): JQuery; + + /** + * Get the buttonImageOnly option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImageOnly' + */ + datepicker(methodName: "option", optionName: "buttonImageOnly"): boolean; + /** + * Set the buttonImageOnly option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImageOnly' + * @param buttonImageOnlyValue Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set. + */ + datepicker(methodName: "option", optionName: "buttonImageOnly", buttonImageOnlyValue: boolean): JQuery; + + /** + * Get the buttonText option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonText' + */ + datepicker(methodName: "option", optionName: "buttonText"): string; + /** + * Set the buttonText option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonText' + * @param buttonTextValue The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both". + */ + datepicker(methodName: "option", optionName: "buttonText", buttonTextValue: string): JQuery; + + /** + * Get the calculateWeek option, after initialization + * + * @param methodName 'option' + * @param optionName 'calculateWeek' + */ + datepicker(methodName: "option", optionName: "calculateWeek"): (date: Date) => string; + /** + * Set the calculateWeek option, after initialization + * + * @param methodName 'option' + * @param optionName 'calculateWeek' + * @param calculateWeekValue A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year. + */ + datepicker(methodName: "option", optionName: "calculateWeek", calculateWeekValue: (date: Date) => string): JQuery; + + /** + * Get the changeMonth option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeMonth' + */ + datepicker(methodName: "option", optionName: "changeMonth"): boolean; + /** + * Set the changeMonth option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeMonth' + * @param changeMonthValue Whether the month should be rendered as a dropdown instead of text. + */ + datepicker(methodName: "option", optionName: "changeMonth", changeMonthValue: boolean): JQuery; + + /** + * Get the changeYear option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeYear' + */ + datepicker(methodName: "option", optionName: "changeYear"): boolean; + /** + * Set the changeYear option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeYear' + * @param changeYearValue Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection. + */ + datepicker(methodName: "option", optionName: "changeYear", changeYearValue: boolean): JQuery; + + /** + * Get the closeText option, after initialization + * + * @param methodName 'option' + * @param optionName 'closeText' + */ + datepicker(methodName: "option", optionName: "closeText"): string; + /** + * Set the closeText option, after initialization + * + * @param methodName 'option' + * @param optionName 'closeText' + * @param closeTextValue The text to display for the close link. Use the showButtonPanel option to display this button. + */ + datepicker(methodName: "option", optionName: "closeText", closeTextValue: string): JQuery; + + /** + * Get the constrainInput option, after initialization + * + * @param methodName 'option' + * @param optionName 'constrainInput' + */ + datepicker(methodName: "option", optionName: "constrainInput"): boolean; + /** + * Set the constrainInput option, after initialization + * + * @param methodName 'option' + * @param optionName 'constrainInput' + * @param constrainInputValue When true, entry in the input field is constrained to those characters allowed by the current dateFormat option. + */ + datepicker(methodName: "option", optionName: "constrainInput", constrainInputValue: boolean): JQuery; + + /** + * Get the currentText option, after initialization + * + * @param methodName 'option' + * @param optionName 'currentText' + */ + datepicker(methodName: "option", optionName: "currentText"): string; + /** + * Set the currentText option, after initialization + * + * @param methodName 'option' + * @param optionName 'currentText' + * @param currentTextValue The text to display for the current day link. Use the showButtonPanel option to display this button. + */ + datepicker(methodName: "option", optionName: "currentText", currentTextValue: string): JQuery; + + /** + * Get the dateFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'dateFormat' + */ + datepicker(methodName: "option", optionName: "dateFormat"): string; + /** + * Set the dateFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'dateFormat' + * @param dateFormatValue The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function. + */ + datepicker(methodName: "option", optionName: "dateFormat", dateFormatValue: string): JQuery; + + /** + * Get the dayNames option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNames' + */ + datepicker(methodName: "option", optionName: "dayNames"): string[]; + /** + * Set the dayNames option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNames' + * @param dayNamesValue The list of long day names, starting from Sunday, for use as requested via the dateFormat option. + */ + datepicker(methodName: "option", optionName: "dayNames", dayNamesValue: string[]): JQuery; + + /** + * Get the dayNamesMin option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesMin' + */ + datepicker(methodName: "option", optionName: "dayNamesMin"): string[]; + /** + * Set the dayNamesMin option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesMin' + * @param dayNamesMinValue The list of minimised day names, starting from Sunday, for use as column headers within the datepicker. + */ + datepicker(methodName: "option", optionName: "dayNamesMin", dayNamesMinValue: string[]): JQuery; + + /** + * Get the dayNamesShort option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesShort' + */ + datepicker(methodName: "option", optionName: "dayNamesShort"): string[]; + /** + * Set the dayNamesShort option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesShort' + * @param dayNamesShortValue The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option. + */ + datepicker(methodName: "option", optionName: "dayNamesShort", dayNamesShortValue: string[]): JQuery; + + /** + * Get the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + */ + datepicker(methodName: "option", optionName: "defaultDate"): any; + /** + * Set the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + * @param defaultDateValue A date object containing the default date. + */ + datepicker(methodName: "option", optionName: "defaultDate", defaultDateValue: Date): JQuery; + /** + * Set the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + * @param defaultDateValue A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + */ + datepicker(methodName: "option", optionName: "defaultDate", defaultDateValue: number): JQuery; + /** + * Set the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + * @param defaultDateValue A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + datepicker(methodName: "option", optionName: "defaultDate", defaultDateValue: string): JQuery; + + /** + * Get the duration option, after initialization + * + * @param methodName 'option' + * @param optionName 'duration' + */ + datepicker(methodName: "option", optionName: "duration"): string; + /** + * Set the duration option, after initialization + * + * @param methodName 'option' + * @param optionName 'duration' + * @param durationValue Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast"). + */ + datepicker(methodName: "option", optionName: "duration", durationValue: string): JQuery; + + /** + * Get the firstDay option, after initialization + * + * @param methodName 'option' + * @param optionName 'firstDay' + */ + datepicker(methodName: "option", optionName: "firstDay"): number; + /** + * Set the firstDay option, after initialization + * + * @param methodName 'option' + * @param optionName 'firstDay' + * @param firstDayValue Set the first day of the week: Sunday is 0, Monday is 1, etc. + */ + datepicker(methodName: "option", optionName: "firstDay", firstDayValue: number): JQuery; + + /** + * Get the gotoCurrent option, after initialization + * + * @param methodName 'option' + * @param optionName 'gotoCurrent' + */ + datepicker(methodName: "option", optionName: "gotoCurrent"): boolean; + /** + * Set the gotoCurrent option, after initialization + * + * @param methodName 'option' + * @param optionName 'gotoCurrent' + * @param gotoCurrentValue When true, the current day link moves to the currently selected date instead of today. + */ + datepicker(methodName: "option", optionName: "gotoCurrent", gotoCurrentValue: boolean): JQuery; + + /** + * Gets the value currently associated with the specified optionName. + * + * @param methodName 'option' + * @param optionName The name of the option to get. + */ + datepicker(methodName: "option", optionName: string): any; + + datepicker(methodName: "option", optionName: string, ...otherParams: any[]): any; // Used for getting and setting options + + datepicker(methodName: string, ...otherParams: any[]): any; + + /** + * Initialize a datepicker with the given options + */ + datepicker(options: JQueryUI.DatepickerOptions): JQuery; + + dialog(): JQuery; + dialog(methodName: "close"): JQuery; + dialog(methodName: "destroy"): JQuery; + dialog(methodName: "isOpen"): boolean; + dialog(methodName: "moveToTop"): JQuery; + dialog(methodName: "open"): JQuery; + dialog(methodName: "widget"): JQuery; + dialog(methodName: string): JQuery; + dialog(options: JQueryUI.DialogOptions): JQuery; + dialog(optionLiteral: string, optionName: string): any; + dialog(optionLiteral: string, options: JQueryUI.DialogOptions): any; + dialog(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + draggable(): JQuery; + draggable(methodName: "destroy"): void; + draggable(methodName: "disable"): void; + draggable(methodName: "enable"): void; + draggable(methodName: "widget"): JQuery; + draggable(methodName: string): JQuery; + draggable(options: JQueryUI.DraggableOptions): JQuery; + draggable(optionLiteral: string, optionName: string): any; + draggable(optionLiteral: string, options: JQueryUI.DraggableOptions): any; + draggable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + droppable(): JQuery; + droppable(methodName: "destroy"): void; + droppable(methodName: "disable"): void; + droppable(methodName: "enable"): void; + droppable(methodName: "widget"): JQuery; + droppable(methodName: string): JQuery; + droppable(options: JQueryUI.DroppableOptions): JQuery; + droppable(optionLiteral: string, optionName: string): any; + droppable(optionLiteral: string, options: JQueryUI.DraggableOptions): any; + droppable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + menu(): JQuery; + menu(methodName: "blur"): void; + menu(methodName: "collapse", event?: JQueryEventObject): void; + menu(methodName: "collapseAll", event?: JQueryEventObject, all?: boolean): void; + menu(methodName: "destroy"): void; + menu(methodName: "disable"): void; + menu(methodName: "enable"): void; + menu(methodName: string, event: JQueryEventObject, item: JQuery): void; + menu(methodName: "focus", event: JQueryEventObject, item: JQuery): void; + menu(methodName: "isFirstItem"): boolean; + menu(methodName: "isLastItem"): boolean; + menu(methodName: "next", event?: JQueryEventObject): void; + menu(methodName: "nextPage", event?: JQueryEventObject): void; + menu(methodName: "previous", event?: JQueryEventObject): void; + menu(methodName: "previousPage", event?: JQueryEventObject): void; + menu(methodName: "refresh"): void; + menu(methodName: "select", event?: JQueryEventObject): void; + menu(methodName: "widget"): JQuery; + menu(methodName: string): JQuery; + menu(options: JQueryUI.MenuOptions): JQuery; + menu(optionLiteral: string, optionName: string): any; + menu(optionLiteral: string, options: JQueryUI.MenuOptions): any; + menu(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + progressbar(): JQuery; + progressbar(methodName: "destroy"): void; + progressbar(methodName: "disable"): void; + progressbar(methodName: "enable"): void; + progressbar(methodName: "refresh"): void; + progressbar(methodName: "value"): any; // number or boolean + progressbar(methodName: "value", value: number): void; + progressbar(methodName: "value", value: boolean): void; + progressbar(methodName: "widget"): JQuery; + progressbar(methodName: string): JQuery; + progressbar(options: JQueryUI.ProgressbarOptions): JQuery; + progressbar(optionLiteral: string, optionName: string): any; + progressbar(optionLiteral: string, options: JQueryUI.ProgressbarOptions): any; + progressbar(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + resizable(): JQuery; + resizable(methodName: "destroy"): void; + resizable(methodName: "disable"): void; + resizable(methodName: "enable"): void; + resizable(methodName: "widget"): JQuery; + resizable(methodName: string): JQuery; + resizable(options: JQueryUI.ResizableOptions): JQuery; + resizable(optionLiteral: string, optionName: string): any; + resizable(optionLiteral: string, options: JQueryUI.ResizableOptions): any; + resizable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + selectable(): JQuery; + selectable(methodName: "destroy"): void; + selectable(methodName: "disable"): void; + selectable(methodName: "enable"): void; + selectable(methodName: "widget"): JQuery; + selectable(methodName: string): JQuery; + selectable(options: JQueryUI.SelectableOptions): JQuery; + selectable(optionLiteral: string, optionName: string): any; + selectable(optionLiteral: string, options: JQueryUI.SelectableOptions): any; + selectable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + // TODO: VSTS 7733811 Define parameter types for jqueryUI selectmenu + selectmenu(option: any): JQuery; + + slider(): JQuery; + slider(methodName: "destroy"): void; + slider(methodName: "disable"): void; + slider(methodName: "enable"): void; + slider(methodName: "refresh"): void; + slider(methodName: "value"): number; + slider(methodName: "value", value: number): void; + slider(methodName: "values"): Array; + slider(methodName: "values", index: number): number; + slider(methodName: string, index: number, value: number): void; + slider(methodName: "values", index: number, value: number): void; + slider(methodName: string, values: Array): void; + slider(methodName: "values", values: Array): void; + slider(methodName: "widget"): JQuery; + slider(methodName: string): JQuery; + slider(options: JQueryUI.SliderOptions): JQuery; + slider(optionLiteral: string, optionName: string): any; + slider(optionLiteral: string, options: JQueryUI.SliderOptions): any; + slider(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + sortable(): JQuery; + sortable(methodName: "destroy"): void; + sortable(methodName: "disable"): void; + sortable(methodName: "enable"): void; + sortable(methodName: "widget"): JQuery; + sortable(methodName: "toArray"): string[]; + sortable(methodName: string): JQuery; + sortable(options: JQueryUI.SortableOptions): JQuery; + sortable(optionLiteral: string, optionName: string): any; + sortable(optionLiteral: string, options: JQueryUI.SortableOptions): any; + sortable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + spinner(): JQuery; + spinner(methodName: "destroy"): void; + spinner(methodName: "disable"): void; + spinner(methodName: "enable"): void; + spinner(methodName: "pageDown", pages?: number): void; + spinner(methodName: "pageUp", pages?: number): void; + spinner(methodName: "stepDown", steps?: number): void; + spinner(methodName: "stepUp", steps?: number): void; + spinner(methodName: "value"): number; + spinner(methodName: "value", value: number): void; + spinner(methodName: "widget"): JQuery; + spinner(methodName: string): JQuery; + spinner(options: JQueryUI.SpinnerOptions): JQuery; + spinner(optionLiteral: string, optionName: string): any; + spinner(optionLiteral: string, options: JQueryUI.SpinnerOptions): any; + spinner(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + tabs(): JQuery; + tabs(methodName: "destroy"): void; + tabs(methodName: "disable"): void; + tabs(methodName: "enable"): void; + tabs(methodName: "load", index: number): void; + tabs(methodName: "refresh"): void; + tabs(methodName: "widget"): JQuery; + tabs(methodName: string): JQuery; + tabs(options: JQueryUI.TabsOptions): JQuery; + tabs(optionLiteral: string, optionName: string): any; + tabs(optionLiteral: string, options: JQueryUI.TabsOptions): any; + tabs(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + tooltip(): JQuery; + tooltip(methodName: "destroy"): void; + tooltip(methodName: "disable"): void; + tooltip(methodName: "enable"): void; + tooltip(methodName: "open"): void; + tooltip(methodName: "close"): void; + tooltip(methodName: "widget"): JQuery; + tooltip(methodName: string): JQuery; + tooltip(options: JQueryUI.TooltipOptions): JQuery; + tooltip(optionLiteral: string, optionName: string): any; + tooltip(optionLiteral: string, options: JQueryUI.TooltipOptions): any; + tooltip(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + addClass(classNames: string, speed?: number, callback?: Function): JQuery; + addClass(classNames: string, speed?: string, callback?: Function): JQuery; + addClass(classNames: string, speed?: number, easing?: string, callback?: Function): JQuery; + addClass(classNames: string, speed?: string, easing?: string, callback?: Function): JQuery; + + removeClass(classNames: string, speed?: number, callback?: Function): JQuery; + removeClass(classNames: string, speed?: string, callback?: Function): JQuery; + removeClass(classNames: string, speed?: number, easing?: string, callback?: Function): JQuery; + removeClass(classNames: string, speed?: string, easing?: string, callback?: Function): JQuery; + + switchClass( + removeClassName: string, + addClassName: string, + duration?: number, + easing?: string, + complete?: Function + ): JQuery; + switchClass( + removeClassName: string, + addClassName: string, + duration?: string, + easing?: string, + complete?: Function + ): JQuery; + + toggleClass(className: string, duration?: number, easing?: string, complete?: Function): JQuery; + toggleClass(className: string, duration?: string, easing?: string, complete?: Function): JQuery; + toggleClass(className: string, aswitch?: boolean, duration?: number, easing?: string, complete?: Function): JQuery; + toggleClass(className: string, aswitch?: boolean, duration?: string, easing?: string, complete?: Function): JQuery; + + effect(options: any): JQuery; + effect(effect: string, options?: any, duration?: number, complete?: Function): JQuery; + effect(effect: string, options?: any, duration?: string, complete?: Function): JQuery; + + hide(options: any): JQuery; + hide(effect: string, options?: any, duration?: number, complete?: Function): JQuery; + hide(effect: string, options?: any, duration?: string, complete?: Function): JQuery; + + show(options: any): JQuery; + show(effect: string, options?: any, duration?: number, complete?: Function): JQuery; + show(effect: string, options?: any, duration?: string, complete?: Function): JQuery; + + toggle(options: any): JQuery; + toggle(effect: string, options?: any, duration?: number, complete?: Function): JQuery; + toggle(effect: string, options?: any, duration?: string, complete?: Function): JQuery; + + position(options: JQueryUI.JQueryPositionOptions): JQuery; + + enableSelection(): JQuery; + disableSelection(): JQuery; + focus(delay: number, callback?: Function): JQuery; + uniqueId(): JQuery; + removeUniqueId(): JQuery; + scrollParent(): JQuery; + zIndex(): JQuery; + zIndex(zIndex: number): JQuery; + + widget: JQueryUI.Widget; + + jQuery: JQueryStatic; +} + +interface JQueryStatic { + ui: JQueryUI.UI; + datepicker: JQueryUI.Datepicker; + widget: JQueryUI.Widget; + Widget: JQueryUI.Widget; +} diff --git a/src/Definitions/jquery.d.ts b/src/Definitions/jquery.d.ts index 498a498b0..3ee86d58a 100644 --- a/src/Definitions/jquery.d.ts +++ b/src/Definitions/jquery.d.ts @@ -1,1890 +1,1890 @@ -/* ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABLITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ - -// Typing for the jQuery library - -/* - Interface for the AJAX setting that will configure the AJAX request -*/ -interface JQueryAjaxSettings { - accepts?: any; - async?: boolean; - beforeSend?(jqXHR: JQueryXHR, settings: JQueryAjaxSettings): boolean; - cache?: boolean; - complete?(jqXHR: JQueryXHR, textStatus: string): any; - contents?: { [key: string]: any }; - // JQuery in the code compares contentType with a boolean value false - // to check, whether to add default "Content-Type" or not. - // Correct use: - // contentType: "text/plain" - // contentType: false - contentType?: any; - context?: any; - converters?: { [key: string]: any }; - crossDomain?: boolean; - data?: any; - dataFilter?(data: any, ty: any): any; - dataType?: string; - error?(jqXHR: JQueryXHR, textStatus: string, errorThrow: string): any; - global?: boolean; - headers?: { [key: string]: any }; - ifModified?: boolean; - isLocal?: boolean; - jsonp?: string; - jsonpCallback?: any; - mimeType?: string; - password?: string; - processData?: boolean; - scriptCharset?: string; - statusCode?: { [key: string]: any }; - success?(data: any, textStatus: string, jqXHR: JQueryXHR): void; - timeout?: number; - traditional?: boolean; - type?: string; - url?: string; - username?: string; - xhr?: any; - xhrFields?: { [key: string]: any }; -} - -interface JQueryPromiseXHRDoneCallback { - (data: T, textStatus: string, jqXHR: JQueryXHR): void; -} - -interface JQueryPromiseXHRFailCallback { - (jqXHR: JQueryXHR, textStatus: string, errorThrown: any): void; -} - -/* - Interface for the jqXHR object -*/ -interface JQueryXHR extends XMLHttpRequest { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryXHR; - done(...doneCallbacks: Array>): JQueryXHR; - fail(...failCallbacks: Array>): JQueryXHR; - progress(...progressCallbacks: Array<{ (): void }>): JQueryXHR; - state(): string; - promise(target?: any): JQueryXHR; - then( - doneCallbacks: JQueryPromiseXHRDoneCallback, - failCallbacks?: JQueryPromiseXHRFailCallback, - progressCallbacks?: { (): void } - ): JQueryPromise; - - then( - doneCallbacks: { (data: T, textStatus: string, jqXHR: JQueryXHR): UValue }, - failCallbacks?: JQueryPromiseXHRFailCallback, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (data: T, textStatus: string, jqXHR: JQueryXHR): UValue }, - failCallbacks?: { (data: T, textStatus: string, jqXHR: JQueryXHR): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - then( - doneCallbacks: JQueryPromiseXHRDoneCallback, - failCallbacks?: { (data: T, textStatus: string, jqXHR: JQueryXHR): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - overrideMimeType(mimeType: string): void; - abort(statusText?: string): void; -} - -/* - Interface for the JQuery callback -*/ -interface JQueryCallback { - add(...callbacks: Array<{ (): void }>): JQueryCallback; - add(callbacks: Array<{ (): void }>): JQueryCallback; - disable(): JQueryCallback; - disabled(): boolean; - empty(): JQueryCallback; - fire(): JQueryCallback; - fired(): boolean; - fireWith(context: any): JQueryCallback; - has(callback: { (): void }): boolean; - lock(): JQueryCallback; - locked(): boolean; - remove(...callbacks: Array<{ (): void }>): JQueryCallback; - remove(callbacks: Array<{ (): void }>): JQueryCallback; -} - -interface JQueryCallback1 { - add(...callbacks: Array<{ (arg: T): void }>): JQueryCallback1; - add(callbacks: Array<{ (arg: T): void }>): JQueryCallback1; - disable(): JQueryCallback1; - disabled(): boolean; - empty(): JQueryCallback1; - fire(arg: T): JQueryCallback1; - fired(): boolean; - fireWith(context: any, args: any[]): JQueryCallback1; - has(callback: { (arg: T): void }): boolean; - lock(): JQueryCallback1; - locked(): boolean; - remove(...callbacks: Array<{ (arg: T): void }>): JQueryCallback1; - remove(callbacks: Array<{ (arg: T): void }>): JQueryCallback1; -} - -interface JQueryCallback2 { - add(...callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; - add(callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; - disable(): JQueryCallback2; - disabled(): boolean; - empty(): JQueryCallback2; - fire(arg1: T1, arg2: T2): JQueryCallback2; - fired(): boolean; - fireWith(context: any, args: any[]): JQueryCallback2; - has(callback: { (arg1: T1, arg2: T2): void }): boolean; - lock(): JQueryCallback2; - locked(): boolean; - remove(...callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; - remove(callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; -} - -interface JQueryCallback3 { - add(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; - add(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; - disable(): JQueryCallback3; - disabled(): boolean; - empty(): JQueryCallback3; - fire(arg1: T1, arg2: T2, arg3: T3): JQueryCallback3; - fired(): boolean; - fireWith(context: any, args: any[]): JQueryCallback3; - has(callback: { (arg1: T1, arg2: T2, arg3: T3): void }): boolean; - lock(): JQueryCallback3; - locked(): boolean; - remove(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; - remove(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; -} - -interface JQueryCallback4 { - add(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; - add(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; - disable(): JQueryCallback4; - disabled(): boolean; - empty(): JQueryCallback4; - fire(arg1: T1, arg2: T2, arg3: T3, arg4: T4): JQueryCallback4; - fired(): boolean; - fireWith(context: any, args: any[]): JQueryCallback4; - has(callback: { (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }): boolean; - lock(): JQueryCallback4; - locked(): boolean; - remove(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; - remove(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; -} - -/* - Interface for the JQuery promise, part of callbacks -*/ -interface JQueryPromiseAny { - always(...alwaysCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; - done(...doneCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; - fail(...failCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; - progress(...progressCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; - state(): string; - promise(target?: any): JQueryPromiseAny; - then( - doneCallbacks: { (...args: any[]): any }, - failCallbacks: { (...args: any[]): any }, - progressCallbacks?: { (...args: any[]): any } - ): JQueryPromiseAny; -} - -interface JQueryPromise { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromise; - done(...doneCallbacks: Array<{ (): void }>): JQueryPromise; - fail(...failCallbacks: Array<{ (): void }>): JQueryPromise; - progress(...progressCallbacks: Array<{ (): void }>): JQueryPromise; - state(): string; - promise(target?: any): JQueryPromise; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (): JQueryPromiseV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (): JQueryDeferred }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; - - then( - doneCallbacks: { (): JQueryPromise }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; - - // U Value - then( - doneCallbacks: { (): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then(doneCallbacks: { (): void }, failCallbacks?: { (): void }, progressCallbacks?: { (): void }): JQueryPromise; -} - -interface JQueryPromiseV { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseV; - done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryPromiseV; - fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseV; - progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseV; - state(): string; - promise(target?: any): JQueryPromiseV; - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg: TValue): JQueryDeferredV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): JQueryPromiseV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - // U Value - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryPromiseN { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseN; - done(...doneCallbacks: Array<{ (): void }>): JQueryPromiseN; - fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseN; - progress(...progressCallbacks: Array<{ (arg: TNotify): void }>): JQueryPromiseN; - state(): string; - promise(target?: any): JQueryPromiseN; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (): JQueryDeferredN }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseN; - - then( - doneCallbacks: { (): JQueryPromiseN }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseN; - - // U Value - then( - doneCallbacks: { (): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromise; -} - -interface JQueryPromiseNNNN { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseNNNN; - done(...doneCallbacks: Array<{ (): void }>): JQueryPromiseNNNN; - fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseNNNN; - progress( - ...progressCallbacks: Array<{ (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void }> - ): JQueryPromiseNNNN; - state(): string; - promise(target?: any): JQueryPromiseNNNN; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromiseVR; - - then( - doneCallbacks: { (): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromise; -} - -interface JQueryPromiseVV { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVV; - done(...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2): void }>): JQueryPromiseVV; - fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseVV; - progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseVV; - state(): string; - promise(target?: any): JQueryPromiseVV; - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryDeferredVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryPromiseVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVV; - - // U Value - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryPromiseVVV { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVVV; - done( - ...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }> - ): JQueryPromiseVVV; - fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseVVV; - progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseVVV; - state(): string; - promise(target?: any): JQueryPromiseVVV; - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryDeferredVVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVVV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryPromiseVVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVVV; - - // U Value - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryPromiseVR { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVR; - done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryPromiseVR; - fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryPromiseVR; - progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseVR; - state(): string; - promise(target?: any): JQueryPromiseVR; - then( - doneCallbacks: { (arg: TValue): JQueryPromiseVR }, - failCallbacks?: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks?: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg: TValue): JQueryDeferredVR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - then( - doneCallbacks: { (arg: TValue): JQueryPromiseVR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Value - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks?: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryPromiseVRN { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVRN; - done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryPromiseVRN; - fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryPromiseVRN; - progress(...progressCallbacks: Array<{ (arg: TProgress): void }>): JQueryPromiseVRN; - state(): string; - promise(target?: any): JQueryPromiseVRN; - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (arg: TProgress): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg: TValue): JQueryDeferredVRN }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVRN; - - then( - doneCallbacks: { (arg: TValue): JQueryPromiseVRN }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVRN; - - // U Value - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (arg: TProgress): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (arg: TProgress): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (arg: TProgress): void } - ): JQueryPromise; -} - -interface JQueryPromiseR { - always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseR; - done(...doneCallbacks: Array<{ (): void }>): JQueryPromiseR; - fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryPromiseR; - progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseR; - state(): string; - promise(target?: any): JQueryPromiseR; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (): JQueryDeferredR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): JQueryPromiseR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks?: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -/* - Interface for the JQuery deferred, part of callbacks -*/ -interface JQueryDeferredAny { - always(...alwaysCallbacks: { (...args: any[]): void }[]): JQueryDeferredAny; - done(...doneCallbacks: { (...args: any[]): void }[]): JQueryDeferredAny; - fail(...failCallbacks: { (...args: any[]): void }[]): JQueryDeferredAny; - progress(...progressCallbacks: { (): void }[]): JQueryDeferredAny; - notify(...args: any[]): JQueryDeferredAny; - notifyWith(context: any, args: any[]): JQueryDeferredAny; - promise(target?: any): JQueryPromiseAny; - reject(...args: any[]): JQueryDeferredAny; - rejectWith(context: any, args: any[]): JQueryDeferredAny; - resolve(...args: any[]): JQueryDeferredAny; - resolveWith(context: any, args: any[]): JQueryDeferredAny; - state(): string; - then( - doneCallbacks: { (...args: any[]): any }, - failCallbacks: { (...args: any[]): any }, - progressCallbacks?: { (...args: any[]): any } - ): JQueryDeferredAny; -} - -interface JQueryDeferred { - notify(): JQueryDeferred; - notifyWith(context: any): JQueryDeferred; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferred; - done(...doneCallbacks: Array<{ (): void }>): JQueryDeferred; - fail(...failCallbacks: Array<{ (): void }>): JQueryDeferred; - progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferred; - promise(target?: any): JQueryPromise; - reject(...args: Array): JQueryDeferred; - rejectWith(context: any): JQueryDeferred; - resolve(): JQueryDeferred; - resolveWith(context: any): JQueryDeferred; - state(): string; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (): JQueryDeferred }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; - - then( - doneCallbacks: { (): JQueryPromise }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; - - // U Value - then( - doneCallbacks: { (): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then(doneCallbacks: { (): void }, failCallbacks?: { (): void }, progressCallbacks?: { (): void }): JQueryPromise; -} - -interface JQueryDeferredV { - notify(): JQueryDeferredV; - notifyWith(context: any): JQueryDeferredV; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredV; - done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryDeferredV; - fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredV; - progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredV; - promise(target?: any): JQueryPromiseV; - reject(...args: Array): JQueryDeferredV; - rejectWith(context: any): JQueryDeferredV; - resolve(arg: TValue): JQueryDeferredV; - resolveWith(context: any, args: TValue[]): JQueryDeferredV; - state(): string; - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg: TValue): JQueryDeferredV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): JQueryPromiseV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - // U Value - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryDeferredN { - notify(arg: TNotify): JQueryDeferredN; - notifyWith(context: any, arg: TNotify): JQueryDeferredN; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredN; - done(...doneCallbacks: Array<{ (): void }>): JQueryDeferredN; - fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredN; - progress(...progressCallbacks: Array<{ (arg: TNotify): void }>): JQueryDeferredN; - promise(target?: any): JQueryPromiseN; - reject(...args: Array): JQueryDeferredN; - rejectWith(context: any): JQueryDeferredN; - resolve(): JQueryDeferredN; - resolveWith(context: any): JQueryDeferredN; - state(): string; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (): JQueryDeferredN }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseN; - - then( - doneCallbacks: { (): JQueryPromiseN }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseN; - - // U Value - then( - doneCallbacks: { (): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromise; -} - -interface JQueryDeferredNNNN { - notify( - arg1: TNotify1, - arg2: TNotify2, - arg3: TNotify3, - arg4: TNotify4 - ): JQueryDeferredNNNN; - notifyWith( - context: any, - arg1: TNotify1, - arg2: TNotify2, - arg3: TNotify3, - arg4: TNotify4 - ): JQueryDeferredNNNN; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredNNNN; - done(...doneCallbacks: Array<{ (): void }>): JQueryDeferredNNNN; - fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredNNNN; - progress( - ...progressCallbacks: Array<{ (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void }> - ): JQueryDeferredNNNN; - promise(target?: any): JQueryPromiseNNNN; - reject(...args: Array): JQueryDeferredNNNN; - rejectWith(context: any): JQueryDeferredNNNN; - resolve(): JQueryDeferredNNNN; - resolveWith(context: any): JQueryDeferredNNNN; - state(): string; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromiseVR; - - then( - doneCallbacks: { (): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } - ): JQueryPromise; -} - -interface JQueryDeferredVV { - notify(): JQueryDeferredVV; - notifyWith(context: any): JQueryDeferredVV; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVV; - done(...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2): void }>): JQueryDeferredVV; - fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredVV; - progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredVV; - promise(target?: any): JQueryPromiseVV; - reject(...args: Array): JQueryDeferredVV; - rejectWith(context: any): JQueryDeferredVV; - resolve(arg1: TValue1, arg2: TValue2): JQueryDeferredVV; - resolveWith(context: any, args: any[]): JQueryDeferredVV; - state(): string; - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryDeferredVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryPromiseVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVV; - - // U Value - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, - failCallbacks: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryDeferredVVV { - notify(): JQueryDeferredVVV; - notifyWith(context: any): JQueryDeferredVVV; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVVV; - done( - ...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }> - ): JQueryDeferredVVV; - fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredVVV; - progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredVVV; - promise(target?: any): JQueryPromiseVVV; - reject(...args: Array): JQueryDeferredVVV; - rejectWith(context: any): JQueryDeferredVVV; - resolve(arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryDeferredVVV; - resolveWith(context: any, args: any[]): JQueryDeferredVVV; - state(): string; - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, - failCallbacks?: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryDeferredVVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVVV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryPromiseVVV }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVVV; - - // U Value - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, - failCallbacks?: { (): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, - failCallbacks?: { (): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryDeferredVR { - notify(): JQueryDeferredVR; - notifyWith(context: any): JQueryDeferredVR; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVR; - done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryDeferredVR; - fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryDeferredVR; - progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredVR; - promise(target?: any): JQueryPromiseVR; - reject(arg: TReject): JQueryDeferredVR; - rejectWith(context: any, arg: TReject[]): JQueryDeferredVR; - resolve(arg: TValue): JQueryDeferredVR; - resolveWith(context: any, args: TValue[]): JQueryDeferredVR; - state(): string; - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg: TValue): JQueryDeferredVR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - then( - doneCallbacks: { (arg: TValue): JQueryPromiseVR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Value - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -interface JQueryDeferredVRN { - notify(arg: TNotify): JQueryDeferredVR; - notifyWith(context: any, arg: TNotify): JQueryDeferredVR; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVR; - done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryDeferredVR; - fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryDeferredVR; - progress(...progressCallbacks: Array<{ (arg: TNotify): void }>): JQueryDeferredVR; - promise(target?: any): JQueryPromiseVRN; - reject(arg: TReject): JQueryDeferredVR; - rejectWith(context: any, args: TReject[]): JQueryDeferredVR; - resolve(arg: TValue): JQueryDeferredVR; - resolveWith(context: any, args: TValue[]): JQueryDeferredVR; - state(): string; - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (arg: TValue): JQueryDeferredVRN }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVRN; - - then( - doneCallbacks: { (arg: TValue): JQueryPromiseVRN }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseVRN; - - // U Value - then( - doneCallbacks: { (arg: TValue): UValue }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseV; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (arg: TValue): void }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (arg: TNotify): void } - ): JQueryPromise; -} - -interface JQueryDeferredR { - notify(): JQueryDeferredR; - notifyWith(context: any): JQueryDeferredR; - - always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredR; - done(...doneCallbacks: Array<{ (): void }>): JQueryDeferredR; - fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryDeferredR; - progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredR; - promise(target?: any): JQueryPromiseR; - reject(arg: TReject): JQueryDeferredR; - rejectWith(context: any, args: TReject[]): JQueryDeferredR; - resolve(): JQueryDeferredR; - resolveWith(context: any): JQueryDeferredR; - state(): string; - then( - doneCallbacks: { (): UValue }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseVR; - - // U Pipe - then( - doneCallbacks: { (): JQueryDeferredR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): JQueryPromiseR }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks: { (arg: TReject): UReject }, - progressCallbacks?: { (): void } - ): JQueryPromiseR; - - then( - doneCallbacks: { (): void }, - failCallbacks?: { (arg: TReject): void }, - progressCallbacks?: { (): void } - ): JQueryPromise; -} - -/* - Interface of the JQuery extension of the W3C event object -*/ -interface BaseJQueryEventObject extends Event { - data: any; - delegateTarget: Element; - isDefaultPrevented(): boolean; - isImmediatePropagationStopped(): boolean; - isPropagationStopped(): boolean; - originalEvent: Event; - namespace: string; - preventDefault(): any; - relatedTarget: Element; - result: any; - stopImmediatePropagation(): void; - stopPropagation(): void; - pageX: number; - pageY: number; - which: number; - - // Other possible values - cancellable?: boolean; - // detail ?? - prevValue?: any; - view?: Window; -} - -interface JQueryInputEventObject extends BaseJQueryEventObject { - altKey: boolean; - ctrlKey: boolean; - metaKey: boolean; - shiftKey: boolean; -} - -interface JQueryMouseEventObject extends JQueryInputEventObject { - button: number; - clientX: number; - clientY: number; - offsetX: number; - offsetY: number; - pageX: number; - pageY: number; - screenX: number; - screenY: number; -} - -interface JQueryKeyEventObject extends JQueryInputEventObject { - char: any; - charCode: number; - key: any; - keyCode: number; -} - -interface JQueryEventObject - extends BaseJQueryEventObject, - JQueryInputEventObject, - JQueryMouseEventObject, - JQueryKeyEventObject {} - -interface JQueryEventHandler { - (eventObject: JQueryEventObject, args?: any): any; -} - -interface JQuerySupport { - ajax?: boolean; - boxModel?: boolean; - changeBubbles?: boolean; - checkClone?: boolean; - checkOn?: boolean; - cors?: boolean; - cssFloat?: boolean; - hrefNormalized?: boolean; - htmlSerialize?: boolean; - leadingWhitespace?: boolean; - noCloneChecked?: boolean; - noCloneEvent?: boolean; - opacity?: boolean; - optDisabled?: boolean; - optSelected?: boolean; - scriptEval?(): boolean; - style?: boolean; - submitBubbles?: boolean; - tbody?: boolean; -} - -// TODO jsgoupil fix signature -interface JQueryEventStatic { - fix(evt: any): any; -} - -interface JQueryParam { - (obj: any): string; - (obj: any, traditional: boolean): string; -} - -/** - * This is a private type. It exists for type checking. Do not explicitly declare an identifier with this type. - */ -interface _JQueryDeferred { - resolve: Function; - resolveWith: Function; - reject: Function; - rejectWith: Function; -} - -interface JQueryWhen { - (promise1: JQueryPromiseV, promise2: JQueryPromiseV): JQueryPromiseVV; - ( - promise1: JQueryPromiseV, - promise2: JQueryPromiseV, - promise3: JQueryPromiseV - ): JQueryPromiseVVV; - (...deferreds: JQueryPromise[]): JQueryPromise; - apply($: JQueryStatic, deferreds: JQueryPromise[]): JQueryPromise; -} - -/* - Static members of jQuery (those on $ and jQuery themselves) -*/ -interface JQueryStatic { - /**** - AJAX - *****/ - ajax(settings: JQueryAjaxSettings): JQueryXHR; - ajax(url: string, settings?: JQueryAjaxSettings): JQueryXHR; - - ajaxPrefilter(dataTypes: string, handler: (opts: any, originalOpts: any, jqXHR: JQueryXHR) => any): any; - ajaxPrefilter(handler: (opts: any, originalOpts: any, jqXHR: JQueryXHR) => any): any; - - ajaxSettings: JQueryAjaxSettings; - - ajaxSetup(options: JQueryAjaxSettings): void; - - ajaxTransport( - dataType: string, - handler: ( - options: JQueryAjaxSettings, - originalOptions: JQueryAjaxSettings, - jqXHR: JQueryXHR - ) => JQueryTransport - ): any; - - get(url: string, data?: any, success?: any, dataType?: any): JQueryXHR; - getJSON(url: string, data?: any, success?: any): JQueryXHR; - getScript(url: string, success?: any): JQueryXHR; - - param: JQueryParam; - - post(url: string, data?: any, success?: any, dataType?: any): JQueryXHR; - - /********* - CALLBACKS - **********/ - Callbacks(flags?: string): JQueryCallback; - Callbacks(flags?: string): JQueryCallback1; - Callbacks(flags?: string): JQueryCallback2; - Callbacks(flags?: string): JQueryCallback3; - Callbacks(flags?: string): JQueryCallback4; - - /**** - CORE - *****/ - holdReady(hold: boolean): any; - - (selector: string, context?: any): JQuery; - (element: Element): JQuery; - (object: {}): JQuery; - (elementArray: Element[]): JQuery; - (object: JQuery): JQuery; - (func: Function): JQuery; - (array: any[]): JQuery; - (): JQuery; - - noConflict(removeAll?: boolean): Object; - - when: JQueryWhen; - - /*** - CSS - ****/ - css(e: any, propertyName: string, value?: any): JQuery; - css(e: any, propertyName: any, value?: any): JQuery; - cssHooks: { [key: string]: any }; - cssNumber: any; - - /**** - DATA - *****/ - data(element: Document, key?: string, value?: any): any; - data(element: Element, key: string, value: any): any; - data(element: Element, key: string): any; - data(element: Element): any; - - dequeue(element: Element, queueName?: string): any; - - hasData(element: Element): boolean; - - queue(element: Element, queueName?: string): any[]; - queue(element: Element, queueName: string, newQueueOrCallback: any): JQuery; - - removeData(element: Document, name?: string): JQuery; - removeData(element: Element, name?: string): JQuery; - - /******* - EFFECTS - ********/ - fx: { - tick: () => void; - interval: number; - stop: () => void; - speeds: { slow: number; fast: number }; - off: boolean; - step: any; - }; - - /****** - EVENTS - *******/ - proxy(fn: (...args: any[]) => any, context: any, ...args: any[]): any; - proxy(context: any, name: string, ...args: any[]): any; - Deferred: { - (fn?: (d: JQueryDeferred) => void): JQueryDeferred; - new (fn?: (d: JQueryDeferred) => void): JQueryDeferred; - - // Can't use a constraint against JQueryDeferred because the non-generic JQueryDeferred.resolve is not a base type of - // the generic JQueryDeferred.resolve methods. - (fn?: (d: TDeferred) => void): TDeferred; - new (fn?: (d: TDeferred) => void): TDeferred; - }; - Event(name: string, eventProperties?: any): JQueryEventObject; - Event(evt: JQueryEventObject, eventProperties?: any): JQueryEventObject; - - event: JQueryEventStatic; - - /********* - INTERNALS - **********/ - error(message: any): JQuery; - - /************* - MISCELLANEOUS - **************/ - expr: any; - fn: JQuery; - isReady: boolean; - - /********** - PROPERTIES - ***********/ - support: JQuerySupport; - - /********* - UTILITIES - **********/ - contains(container: Element, contained: Element): boolean; - - each(collection: any, callback: (indexInArray: any, valueOfElement: any) => any): any; - each(collection: JQuery, callback: (indexInArray: number, valueOfElement: HTMLElement) => any): JQuery; - each(collection: T[], callback: (indexInArray: number, valueOfElement: T) => void): T[]; - - extend(deep: boolean, target: any, ...objs: any[]): any; - extend(target: any, ...objs: any[]): any; - - globalEval(code: string): any; - - grep(array: T[], func: (elementOfArray: T, indexInArray: number) => boolean, invert?: boolean): T[]; - - inArray(value: T, array: T[], fromIndex?: number): number; - - isArray(obj: any): boolean; - isEmptyObject(obj: any): boolean; - isFunction(obj: any): boolean; - isNumeric(value: any): boolean; - isPlainObject(obj: any): boolean; - isWindow(obj: any): boolean; - isXMLDoc(node: Node): boolean; - - makeArray(obj: any): any[]; - - map(array: T[], callback: (elementOfArray: T, indexInArray: number) => U): U[]; - map(object: { [item: string]: T }, callback: (elementOfArray: T, indexInArray: string) => U): U[]; - map(array: any, callback: (elementOfArray: any, indexInArray: any) => any): any; - - merge(first: T[], second: T[]): T[]; - - noop(): any; - - now(): number; - - parseHTML(data: string, context?: Element, keepScripts?: boolean): Element[]; - - parseJSON(json: string): Object; - - //FIXME: This should return an XMLDocument - parseXML(data: string): any; - - trim(str: string): string; - - type(obj: any): string; - - unique(arr: T[]): T[]; -} - -interface JQueryTransport { - send( - headers: { [index: string]: string }, - completeCallback: ( - status: number, - statusText: string, - responses?: { [dataType: string]: any }, - headers?: string - ) => any - ): any; - abort(): any; -} - -/* - The jQuery instance members -*/ -interface JQuery { - /**** - AJAX - *****/ - ajaxComplete(handler: any): JQuery; - ajaxError(handler: (event: any, jqXHR: any, settings: any, exception: any) => any): JQuery; - ajaxSend(handler: (event: any, jqXHR: any, settings: any, exception: any) => any): JQuery; - ajaxStart(handler: () => any): JQuery; - ajaxStop(handler: () => any): JQuery; - ajaxSuccess(handler: (event: any, jqXHR: any, settings: any, exception: any) => any): JQuery; - - load(url: string, data?: any, complete?: any): JQuery; - - serialize(): string; - serializeArray(): any[]; - - /********** - ATTRIBUTES - ***********/ - addClass(classNames: string): JQuery; - addClass(func: (index: any, currentClass: any) => string): JQuery; - - // http://api.jquery.com/addBack/ - addBack(selector?: string): JQuery; - - attr(attributeName: string): string; - attr(attributeName: string, value: any): JQuery; - attr(map: { [key: string]: any }): JQuery; - attr(attributeName: string, func: (index: any, attr: any) => any): JQuery; - - hasClass(className: string): boolean; - - html(): string; - html(htmlString: number): JQuery; - html(htmlString: string): JQuery; - html(htmlContent: (index: number, oldhtml: string) => string): JQuery; - - prop(propertyName: string): any; - prop(propertyName: string, value: any): JQuery; - prop(map: any): JQuery; - prop(propertyName: string, func: (index: any, oldPropertyValue: any) => any): JQuery; - - removeAttr(attributeName: any): JQuery; - - removeClass(className?: any): JQuery; - removeClass(func: (index: any, cls: any) => any): JQuery; - - removeProp(propertyName: any): JQuery; - - toggleClass(className: any, swtch?: boolean): JQuery; - toggleClass(swtch?: boolean): JQuery; - toggleClass(func: (index: any, cls: any, swtch: any) => any): JQuery; - - val(): any; - val(value: string[]): JQuery; - val(value: string): JQuery; - val(value: number): JQuery; - val(func: (index: any, value: any) => any): JQuery; - - /*** - CSS - ****/ - css(propertyName: string): string; - css(propertyNames: string[]): string; - css(properties: any): JQuery; - css(propertyName: string, value: any): JQuery; - css(propertyName: any, value: any): JQuery; - - height(): number; - height(value: number): JQuery; - height(value: string): JQuery; - height(func: (index: any, height: any) => any): JQuery; - - innerHeight(): number; - innerWidth(): number; - - offset(): { left: number; top: number }; - offset(coordinates: any): JQuery; - offset(func: (index: any, coords: any) => any): JQuery; - - outerHeight(includeMargin?: boolean): number; - outerWidth(includeMargin?: boolean): number; - - position(): { top: number; left: number }; - - scrollLeft(): number; - scrollLeft(value: number): JQuery; - - scrollTop(): number; - scrollTop(value: number): JQuery; - - width(): number; - width(value: number): JQuery; - width(value: string): JQuery; - width(func: (index: any, height: any) => any): JQuery; - - /**** - DATA - *****/ - clearQueue(queueName?: string): JQuery; - - data(key: string, value: any): JQuery; - data(obj: { [key: string]: any }): JQuery; - data(key?: string): any; - - dequeue(queueName?: string): JQuery; - - removeData(nameOrList?: any): JQuery; - - /******** - DEFERRED - *********/ - promise(type?: any, target?: any): JQueryPromise; - - /******* - EFFECTS - ********/ - animate(properties: any, duration?: any, complete?: Function): JQuery; - animate(properties: any, duration?: any, easing?: string, complete?: Function): JQuery; - animate( - properties: any, - options: { - duration?: any; - easing?: string; - complete?: Function; - step?: Function; - queue?: boolean; - specialEasing?: any; - } - ): JQuery; - - delay(duration: number, queueName?: string): JQuery; - - fadeIn(duration?: any, callback?: any): JQuery; - fadeIn(duration?: any, easing?: string, callback?: any): JQuery; - - fadeOut(duration?: any, callback?: any): JQuery; - fadeOut(duration?: any, easing?: string, callback?: any): JQuery; - - fadeTo(duration: any, opacity: number, callback?: any): JQuery; - fadeTo(duration: any, opacity: number, easing?: string, callback?: any): JQuery; - - fadeToggle(duration?: any, callback?: any): JQuery; - fadeToggle(duration?: any, easing?: string, callback?: any): JQuery; - - finish(): JQuery; - - hide(duration?: any, callback?: any): JQuery; - hide(duration?: any, easing?: string, callback?: any): JQuery; - - show(duration?: any, callback?: any): JQuery; - show(duration?: any, easing?: string, callback?: any): JQuery; - - slideDown(duration?: any, callback?: any): JQuery; - slideDown(duration?: any, easing?: string, callback?: any): JQuery; - - slideToggle(duration?: any, callback?: any): JQuery; - slideToggle(duration?: any, easing?: string, callback?: any): JQuery; - - slideUp(duration?: any, callback?: any): JQuery; - slideUp(duration?: any, easing?: string, callback?: any): JQuery; - - stop(clearQueue?: boolean, jumpToEnd?: boolean): JQuery; - stop(queue?: any, clearQueue?: boolean, jumpToEnd?: boolean): JQuery; - - toggle(duration?: any, callback?: any): JQuery; - toggle(duration?: any, easing?: string, callback?: any): JQuery; - toggle(showOrHide: boolean): JQuery; - - /****** - EVENTS - *******/ - bind(eventType: string, eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - bind(eventType: string, eventData: any, preventBubble: boolean): JQuery; - bind(eventType: string, preventBubble: boolean): JQuery; - bind(...events: any[]): JQuery; - - blur(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - blur(handler: (eventObject: JQueryEventObject) => any): JQuery; - - change(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - change(handler: (eventObject: JQueryEventObject) => any): JQuery; - - click(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - click(handler: (eventObject: JQueryEventObject) => any): JQuery; - - dblclick(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - dblclick(handler: (eventObject: JQueryEventObject) => any): JQuery; - - delegate(selector: any, eventType: string, handler: (eventObject: JQueryEventObject) => any): JQuery; - - focus(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - focus(handler: (eventObject: JQueryEventObject) => any): JQuery; - - focusin(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; - focusin(handler: (eventObject: JQueryEventObject) => any): JQuery; - - focusout(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; - focusout(handler: (eventObject: JQueryEventObject) => any): JQuery; - - hover( - handlerIn: (eventObject: JQueryEventObject) => any, - handlerOut: (eventObject: JQueryEventObject) => any - ): JQuery; - hover(handlerInOut: (eventObject: JQueryEventObject) => any): JQuery; - - keydown(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; - keydown(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; - - keypress(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; - keypress(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; - - keyup(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; - keyup(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; - - load(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - load(handler: (eventObject: JQueryEventObject) => any): JQuery; - - mousedown(): JQuery; - mousedown(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mousedown(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - mouseevent(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mouseevent(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - mouseenter(): JQuery; - mouseenter(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mouseenter(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - mouseleave(): JQuery; - mouseleave(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mouseleave(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - mousemove(): JQuery; - mousemove(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mousemove(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - mouseout(): JQuery; - mouseout(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mouseout(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - mouseover(): JQuery; - mouseover(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mouseover(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - mouseup(): JQuery; - mouseup(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - mouseup(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; - - off(events?: string, selector?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - off(eventsMap: { [key: string]: any }, selector?: any): JQuery; - - on(events: string, selector: any, data: any, handler: (eventObject: JQueryEventObject, args: any) => any): JQuery; - on(events: string, selector: any, handler: (eventObject: JQueryEventObject) => any): JQuery; - on(events: string, handler: (eventObject: JQueryEventObject, args: any) => any): JQuery; - on(eventsMap: { [key: string]: any }, selector?: any, data?: any): JQuery; - - one(events: string, selector?: any, data?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - one(eventsMap: { [key: string]: any }, selector?: any, data?: any): JQuery; - - ready(handler: any): JQuery; - - resize(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - resize(handler: (eventObject: JQueryEventObject) => any): JQuery; - - scroll(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - scroll(handler: (eventObject: JQueryEventObject) => any): JQuery; - - select(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - select(handler: (eventObject: JQueryEventObject) => any): JQuery; - - submit(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - submit(handler: (eventObject: JQueryEventObject) => any): JQuery; - - trigger(eventType: string, ...extraParameters: any[]): JQuery; - trigger(event: JQueryEventObject, ...extraParameters: any[]): JQuery; - - triggerHandler(eventType: string, ...extraParameters: any[]): Object; - // JSGOUPIL: triggerHandler uses trigger, not documented though - triggerHandler(evt: JQueryEventObject): Object; - - unbind(eventType?: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; - unbind(eventType: string, fls: boolean): JQuery; - unbind(evt: any): JQuery; - - undelegate(): JQuery; - undelegate(selector: any, eventType: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; - undelegate(selector: any, events: any): JQuery; - undelegate(namespace: string): JQuery; - - unload(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; - unload(handler: (eventObject: JQueryEventObject) => any): JQuery; - - /********* - INTERNALS - **********/ - - context: Element; - jquery: string; - - error(handler: (eventObject: JQueryEventObject) => any): JQuery; - error(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; - - pushStack(elements: any[]): JQuery; - pushStack(elements: any[], name: any, arguments: any): JQuery; - - /************ - MANIPULATION - *************/ - after(...content: any[]): JQuery; - after(func: (index: any) => any): JQuery; - - append(...content: any[]): JQuery; - append(func: (index: any, html: any) => any): JQuery; - - appendTo(target: any): JQuery; - - before(...content: any[]): JQuery; - before(func: (index: any) => any): JQuery; - - clone(withDataAndEvents?: boolean, deepWithDataAndEvents?: boolean): JQuery; - - detach(selector?: any): JQuery; - - empty(): JQuery; - - insertAfter(target: any): JQuery; - insertBefore(target: any): JQuery; - - prepend(...content: any[]): JQuery; - prepend(func: (index: any, html: any) => any): JQuery; - - prependTo(target: any): JQuery; - - remove(selector?: any): JQuery; - - replaceAll(target: any): JQuery; - - replaceWith(func: any): JQuery; - - text(): string; - text(textString: any): JQuery; - text(textString: (index: number, text: string) => string): JQuery; - - toArray(): any[]; - - unwrap(): JQuery; - - wrap(wrappingElement: any): JQuery; - wrap(func: (index: any) => any): JQuery; - - wrapAll(wrappingElement: any): JQuery; - - wrapInner(wrappingElement: any): JQuery; - wrapInner(func: (index: any) => any): JQuery; - - /************* - MISCELLANEOUS - **************/ - each(func: (index: any, elem: Element) => any): JQuery; - - get(index?: number): any; - - index(): number; - index(selector: string): number; - index(element: any): number; - - /********** - PROPERTIES - ***********/ - length: number; - selector: string; - [x: string]: any; - [x: number]: HTMLElement; - - /********** - TRAVERSING - ***********/ - add(selector: string, context?: any): JQuery; - add(...elements: any[]): JQuery; - add(html: string): JQuery; - add(obj: JQuery): JQuery; - - children(selector?: any): JQuery; - - closest(selector: string): JQuery; - closest(selector: string, context?: Element): JQuery; - closest(obj: JQuery): JQuery; - closest(element: any): JQuery; - closest(selectors: any, context?: Element): any[]; - - contents(): JQuery; - - end(): JQuery; - - eq(index: number): JQuery; - - filter(selector: string): JQuery; - filter(func: (index: any) => any): JQuery; - filter(element: any): JQuery; - filter(obj: JQuery): JQuery; - - find(selector: string): JQuery; - find(element: any): JQuery; - find(obj: JQuery): JQuery; - - first(): JQuery; - - has(selector: string): JQuery; - has(contained: Element): JQuery; - - is(selector: string): boolean; - is(func: (index: any) => any): boolean; - is(element: any): boolean; - is(obj: JQuery): boolean; - - last(): JQuery; - - map(callback: (index: any, domElement: Element) => any): JQuery; - - next(selector?: string): JQuery; - - nextAll(selector?: string): JQuery; - - nextUntil(selector?: string, filter?: string): JQuery; - nextUntil(element?: Element, filter?: string): JQuery; - - not(selector: string): JQuery; - not(func: (index: any) => any): JQuery; - not(element: any): JQuery; - not(obj: JQuery): JQuery; - - offsetParent(): JQuery; - - parent(selector?: string): JQuery; - - parents(selector?: string): JQuery; - - parentsUntil(selector?: string, filter?: string): JQuery; - parentsUntil(element?: Element, filter?: string): JQuery; - - prev(selector?: string): JQuery; - - prevAll(selector?: string): JQuery; - - prevUntil(selector?: string, filter?: string): JQuery; - prevUntil(element?: Element, filter?: string): JQuery; - - siblings(selector?: string): JQuery; - - slice(start: number, end?: number): JQuery; - - /********* - UTILITIES - **********/ - - queue(queueName?: string): any[]; - queue(queueName: string, newQueueOrCallback: any): JQuery; - queue(newQueueOrCallback: any): JQuery; -} - -interface EventTarget { - //nodeName: string; //bugfix, duplicate identifier. see: http://stackoverflow.com/questions/14824143/duplicate-identifier-nodename-in-jquery-d-ts -} - -// TODO Rmove these and make jquery a proper module -declare module "jquery"; -declare var jQuery: JQueryStatic; -declare var $: JQueryStatic; +/* ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + +// Typing for the jQuery library + +/* + Interface for the AJAX setting that will configure the AJAX request +*/ +interface JQueryAjaxSettings { + accepts?: any; + async?: boolean; + beforeSend?(jqXHR: JQueryXHR, settings: JQueryAjaxSettings): boolean; + cache?: boolean; + complete?(jqXHR: JQueryXHR, textStatus: string): any; + contents?: { [key: string]: any }; + // JQuery in the code compares contentType with a boolean value false + // to check, whether to add default "Content-Type" or not. + // Correct use: + // contentType: "text/plain" + // contentType: false + contentType?: any; + context?: any; + converters?: { [key: string]: any }; + crossDomain?: boolean; + data?: any; + dataFilter?(data: any, ty: any): any; + dataType?: string; + error?(jqXHR: JQueryXHR, textStatus: string, errorThrow: string): any; + global?: boolean; + headers?: { [key: string]: any }; + ifModified?: boolean; + isLocal?: boolean; + jsonp?: string; + jsonpCallback?: any; + mimeType?: string; + password?: string; + processData?: boolean; + scriptCharset?: string; + statusCode?: { [key: string]: any }; + success?(data: any, textStatus: string, jqXHR: JQueryXHR): void; + timeout?: number; + traditional?: boolean; + type?: string; + url?: string; + username?: string; + xhr?: any; + xhrFields?: { [key: string]: any }; +} + +interface JQueryPromiseXHRDoneCallback { + (data: T, textStatus: string, jqXHR: JQueryXHR): void; +} + +interface JQueryPromiseXHRFailCallback { + (jqXHR: JQueryXHR, textStatus: string, errorThrown: any): void; +} + +/* + Interface for the jqXHR object +*/ +interface JQueryXHR extends XMLHttpRequest { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryXHR; + done(...doneCallbacks: Array>): JQueryXHR; + fail(...failCallbacks: Array>): JQueryXHR; + progress(...progressCallbacks: Array<{ (): void }>): JQueryXHR; + state(): string; + promise(target?: any): JQueryXHR; + then( + doneCallbacks: JQueryPromiseXHRDoneCallback, + failCallbacks?: JQueryPromiseXHRFailCallback, + progressCallbacks?: { (): void } + ): JQueryPromise; + + then( + doneCallbacks: { (data: T, textStatus: string, jqXHR: JQueryXHR): UValue }, + failCallbacks?: JQueryPromiseXHRFailCallback, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (data: T, textStatus: string, jqXHR: JQueryXHR): UValue }, + failCallbacks?: { (data: T, textStatus: string, jqXHR: JQueryXHR): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + then( + doneCallbacks: JQueryPromiseXHRDoneCallback, + failCallbacks?: { (data: T, textStatus: string, jqXHR: JQueryXHR): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + overrideMimeType(mimeType: string): void; + abort(statusText?: string): void; +} + +/* + Interface for the JQuery callback +*/ +interface JQueryCallback { + add(...callbacks: Array<{ (): void }>): JQueryCallback; + add(callbacks: Array<{ (): void }>): JQueryCallback; + disable(): JQueryCallback; + disabled(): boolean; + empty(): JQueryCallback; + fire(): JQueryCallback; + fired(): boolean; + fireWith(context: any): JQueryCallback; + has(callback: { (): void }): boolean; + lock(): JQueryCallback; + locked(): boolean; + remove(...callbacks: Array<{ (): void }>): JQueryCallback; + remove(callbacks: Array<{ (): void }>): JQueryCallback; +} + +interface JQueryCallback1 { + add(...callbacks: Array<{ (arg: T): void }>): JQueryCallback1; + add(callbacks: Array<{ (arg: T): void }>): JQueryCallback1; + disable(): JQueryCallback1; + disabled(): boolean; + empty(): JQueryCallback1; + fire(arg: T): JQueryCallback1; + fired(): boolean; + fireWith(context: any, args: any[]): JQueryCallback1; + has(callback: { (arg: T): void }): boolean; + lock(): JQueryCallback1; + locked(): boolean; + remove(...callbacks: Array<{ (arg: T): void }>): JQueryCallback1; + remove(callbacks: Array<{ (arg: T): void }>): JQueryCallback1; +} + +interface JQueryCallback2 { + add(...callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; + add(callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; + disable(): JQueryCallback2; + disabled(): boolean; + empty(): JQueryCallback2; + fire(arg1: T1, arg2: T2): JQueryCallback2; + fired(): boolean; + fireWith(context: any, args: any[]): JQueryCallback2; + has(callback: { (arg1: T1, arg2: T2): void }): boolean; + lock(): JQueryCallback2; + locked(): boolean; + remove(...callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; + remove(callbacks: Array<{ (arg1: T1, arg2: T2): void }>): JQueryCallback2; +} + +interface JQueryCallback3 { + add(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; + add(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; + disable(): JQueryCallback3; + disabled(): boolean; + empty(): JQueryCallback3; + fire(arg1: T1, arg2: T2, arg3: T3): JQueryCallback3; + fired(): boolean; + fireWith(context: any, args: any[]): JQueryCallback3; + has(callback: { (arg1: T1, arg2: T2, arg3: T3): void }): boolean; + lock(): JQueryCallback3; + locked(): boolean; + remove(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; + remove(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3): void }>): JQueryCallback3; +} + +interface JQueryCallback4 { + add(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; + add(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; + disable(): JQueryCallback4; + disabled(): boolean; + empty(): JQueryCallback4; + fire(arg1: T1, arg2: T2, arg3: T3, arg4: T4): JQueryCallback4; + fired(): boolean; + fireWith(context: any, args: any[]): JQueryCallback4; + has(callback: { (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }): boolean; + lock(): JQueryCallback4; + locked(): boolean; + remove(...callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; + remove(callbacks: Array<{ (arg1: T1, arg2: T2, arg3: T3, arg4: T4): void }>): JQueryCallback4; +} + +/* + Interface for the JQuery promise, part of callbacks +*/ +interface JQueryPromiseAny { + always(...alwaysCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; + done(...doneCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; + fail(...failCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; + progress(...progressCallbacks: { (...args: any[]): void }[]): JQueryPromiseAny; + state(): string; + promise(target?: any): JQueryPromiseAny; + then( + doneCallbacks: { (...args: any[]): any }, + failCallbacks: { (...args: any[]): any }, + progressCallbacks?: { (...args: any[]): any } + ): JQueryPromiseAny; +} + +interface JQueryPromise { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromise; + done(...doneCallbacks: Array<{ (): void }>): JQueryPromise; + fail(...failCallbacks: Array<{ (): void }>): JQueryPromise; + progress(...progressCallbacks: Array<{ (): void }>): JQueryPromise; + state(): string; + promise(target?: any): JQueryPromise; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (): JQueryPromiseV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (): JQueryDeferred }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; + + then( + doneCallbacks: { (): JQueryPromise }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; + + // U Value + then( + doneCallbacks: { (): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then(doneCallbacks: { (): void }, failCallbacks?: { (): void }, progressCallbacks?: { (): void }): JQueryPromise; +} + +interface JQueryPromiseV { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseV; + done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryPromiseV; + fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseV; + progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseV; + state(): string; + promise(target?: any): JQueryPromiseV; + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg: TValue): JQueryDeferredV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): JQueryPromiseV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + // U Value + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryPromiseN { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseN; + done(...doneCallbacks: Array<{ (): void }>): JQueryPromiseN; + fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseN; + progress(...progressCallbacks: Array<{ (arg: TNotify): void }>): JQueryPromiseN; + state(): string; + promise(target?: any): JQueryPromiseN; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (): JQueryDeferredN }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseN; + + then( + doneCallbacks: { (): JQueryPromiseN }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseN; + + // U Value + then( + doneCallbacks: { (): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromise; +} + +interface JQueryPromiseNNNN { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseNNNN; + done(...doneCallbacks: Array<{ (): void }>): JQueryPromiseNNNN; + fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseNNNN; + progress( + ...progressCallbacks: Array<{ (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void }> + ): JQueryPromiseNNNN; + state(): string; + promise(target?: any): JQueryPromiseNNNN; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromiseVR; + + then( + doneCallbacks: { (): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromise; +} + +interface JQueryPromiseVV { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVV; + done(...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2): void }>): JQueryPromiseVV; + fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseVV; + progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseVV; + state(): string; + promise(target?: any): JQueryPromiseVV; + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryDeferredVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryPromiseVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVV; + + // U Value + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryPromiseVVV { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVVV; + done( + ...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }> + ): JQueryPromiseVVV; + fail(...failCallbacks: Array<{ (): void }>): JQueryPromiseVVV; + progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseVVV; + state(): string; + promise(target?: any): JQueryPromiseVVV; + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryDeferredVVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVVV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryPromiseVVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVVV; + + // U Value + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryPromiseVR { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVR; + done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryPromiseVR; + fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryPromiseVR; + progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseVR; + state(): string; + promise(target?: any): JQueryPromiseVR; + then( + doneCallbacks: { (arg: TValue): JQueryPromiseVR }, + failCallbacks?: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks?: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg: TValue): JQueryDeferredVR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + then( + doneCallbacks: { (arg: TValue): JQueryPromiseVR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Value + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks?: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryPromiseVRN { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseVRN; + done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryPromiseVRN; + fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryPromiseVRN; + progress(...progressCallbacks: Array<{ (arg: TProgress): void }>): JQueryPromiseVRN; + state(): string; + promise(target?: any): JQueryPromiseVRN; + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (arg: TProgress): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg: TValue): JQueryDeferredVRN }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVRN; + + then( + doneCallbacks: { (arg: TValue): JQueryPromiseVRN }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVRN; + + // U Value + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (arg: TProgress): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (arg: TProgress): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (arg: TProgress): void } + ): JQueryPromise; +} + +interface JQueryPromiseR { + always(...alwaysCallbacks: Array<{ (): void }>): JQueryPromiseR; + done(...doneCallbacks: Array<{ (): void }>): JQueryPromiseR; + fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryPromiseR; + progress(...progressCallbacks: Array<{ (): void }>): JQueryPromiseR; + state(): string; + promise(target?: any): JQueryPromiseR; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (): JQueryDeferredR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): JQueryPromiseR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks?: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +/* + Interface for the JQuery deferred, part of callbacks +*/ +interface JQueryDeferredAny { + always(...alwaysCallbacks: { (...args: any[]): void }[]): JQueryDeferredAny; + done(...doneCallbacks: { (...args: any[]): void }[]): JQueryDeferredAny; + fail(...failCallbacks: { (...args: any[]): void }[]): JQueryDeferredAny; + progress(...progressCallbacks: { (): void }[]): JQueryDeferredAny; + notify(...args: any[]): JQueryDeferredAny; + notifyWith(context: any, args: any[]): JQueryDeferredAny; + promise(target?: any): JQueryPromiseAny; + reject(...args: any[]): JQueryDeferredAny; + rejectWith(context: any, args: any[]): JQueryDeferredAny; + resolve(...args: any[]): JQueryDeferredAny; + resolveWith(context: any, args: any[]): JQueryDeferredAny; + state(): string; + then( + doneCallbacks: { (...args: any[]): any }, + failCallbacks: { (...args: any[]): any }, + progressCallbacks?: { (...args: any[]): any } + ): JQueryDeferredAny; +} + +interface JQueryDeferred { + notify(): JQueryDeferred; + notifyWith(context: any): JQueryDeferred; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferred; + done(...doneCallbacks: Array<{ (): void }>): JQueryDeferred; + fail(...failCallbacks: Array<{ (): void }>): JQueryDeferred; + progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferred; + promise(target?: any): JQueryPromise; + reject(...args: Array): JQueryDeferred; + rejectWith(context: any): JQueryDeferred; + resolve(): JQueryDeferred; + resolveWith(context: any): JQueryDeferred; + state(): string; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (): JQueryDeferred }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; + + then( + doneCallbacks: { (): JQueryPromise }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; + + // U Value + then( + doneCallbacks: { (): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then(doneCallbacks: { (): void }, failCallbacks?: { (): void }, progressCallbacks?: { (): void }): JQueryPromise; +} + +interface JQueryDeferredV { + notify(): JQueryDeferredV; + notifyWith(context: any): JQueryDeferredV; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredV; + done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryDeferredV; + fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredV; + progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredV; + promise(target?: any): JQueryPromiseV; + reject(...args: Array): JQueryDeferredV; + rejectWith(context: any): JQueryDeferredV; + resolve(arg: TValue): JQueryDeferredV; + resolveWith(context: any, args: TValue[]): JQueryDeferredV; + state(): string; + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg: TValue): JQueryDeferredV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): JQueryPromiseV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + // U Value + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryDeferredN { + notify(arg: TNotify): JQueryDeferredN; + notifyWith(context: any, arg: TNotify): JQueryDeferredN; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredN; + done(...doneCallbacks: Array<{ (): void }>): JQueryDeferredN; + fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredN; + progress(...progressCallbacks: Array<{ (arg: TNotify): void }>): JQueryDeferredN; + promise(target?: any): JQueryPromiseN; + reject(...args: Array): JQueryDeferredN; + rejectWith(context: any): JQueryDeferredN; + resolve(): JQueryDeferredN; + resolveWith(context: any): JQueryDeferredN; + state(): string; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (): JQueryDeferredN }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseN; + + then( + doneCallbacks: { (): JQueryPromiseN }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseN; + + // U Value + then( + doneCallbacks: { (): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromise; +} + +interface JQueryDeferredNNNN { + notify( + arg1: TNotify1, + arg2: TNotify2, + arg3: TNotify3, + arg4: TNotify4 + ): JQueryDeferredNNNN; + notifyWith( + context: any, + arg1: TNotify1, + arg2: TNotify2, + arg3: TNotify3, + arg4: TNotify4 + ): JQueryDeferredNNNN; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredNNNN; + done(...doneCallbacks: Array<{ (): void }>): JQueryDeferredNNNN; + fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredNNNN; + progress( + ...progressCallbacks: Array<{ (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void }> + ): JQueryDeferredNNNN; + promise(target?: any): JQueryPromiseNNNN; + reject(...args: Array): JQueryDeferredNNNN; + rejectWith(context: any): JQueryDeferredNNNN; + resolve(): JQueryDeferredNNNN; + resolveWith(context: any): JQueryDeferredNNNN; + state(): string; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromiseVR; + + then( + doneCallbacks: { (): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (arg1: TNotify1, arg2: TNotify2, arg3: TNotify3, arg4: TNotify4): void } + ): JQueryPromise; +} + +interface JQueryDeferredVV { + notify(): JQueryDeferredVV; + notifyWith(context: any): JQueryDeferredVV; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVV; + done(...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2): void }>): JQueryDeferredVV; + fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredVV; + progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredVV; + promise(target?: any): JQueryPromiseVV; + reject(...args: Array): JQueryDeferredVV; + rejectWith(context: any): JQueryDeferredVV; + resolve(arg1: TValue1, arg2: TValue2): JQueryDeferredVV; + resolveWith(context: any, args: any[]): JQueryDeferredVV; + state(): string; + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryDeferredVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): JQueryPromiseVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVV; + + // U Value + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, + failCallbacks: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryDeferredVVV { + notify(): JQueryDeferredVVV; + notifyWith(context: any): JQueryDeferredVVV; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVVV; + done( + ...doneCallbacks: Array<{ (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }> + ): JQueryDeferredVVV; + fail(...failCallbacks: Array<{ (): void }>): JQueryDeferredVVV; + progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredVVV; + promise(target?: any): JQueryPromiseVVV; + reject(...args: Array): JQueryDeferredVVV; + rejectWith(context: any): JQueryDeferredVVV; + resolve(arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryDeferredVVV; + resolveWith(context: any, args: any[]): JQueryDeferredVVV; + state(): string; + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, + failCallbacks?: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryDeferredVVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVVV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): JQueryPromiseVVV }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVVV; + + // U Value + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): UValue }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, + failCallbacks?: { (): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg1: TValue1, arg2: TValue2, arg3: TValue3): void }, + failCallbacks?: { (): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryDeferredVR { + notify(): JQueryDeferredVR; + notifyWith(context: any): JQueryDeferredVR; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVR; + done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryDeferredVR; + fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryDeferredVR; + progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredVR; + promise(target?: any): JQueryPromiseVR; + reject(arg: TReject): JQueryDeferredVR; + rejectWith(context: any, arg: TReject[]): JQueryDeferredVR; + resolve(arg: TValue): JQueryDeferredVR; + resolveWith(context: any, args: TValue[]): JQueryDeferredVR; + state(): string; + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg: TValue): JQueryDeferredVR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + then( + doneCallbacks: { (arg: TValue): JQueryPromiseVR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Value + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +interface JQueryDeferredVRN { + notify(arg: TNotify): JQueryDeferredVR; + notifyWith(context: any, arg: TNotify): JQueryDeferredVR; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredVR; + done(...doneCallbacks: Array<{ (arg: TValue): void }>): JQueryDeferredVR; + fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryDeferredVR; + progress(...progressCallbacks: Array<{ (arg: TNotify): void }>): JQueryDeferredVR; + promise(target?: any): JQueryPromiseVRN; + reject(arg: TReject): JQueryDeferredVR; + rejectWith(context: any, args: TReject[]): JQueryDeferredVR; + resolve(arg: TValue): JQueryDeferredVR; + resolveWith(context: any, args: TValue[]): JQueryDeferredVR; + state(): string; + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (arg: TValue): JQueryDeferredVRN }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVRN; + + then( + doneCallbacks: { (arg: TValue): JQueryPromiseVRN }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseVRN; + + // U Value + then( + doneCallbacks: { (arg: TValue): UValue }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseV; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (arg: TValue): void }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (arg: TNotify): void } + ): JQueryPromise; +} + +interface JQueryDeferredR { + notify(): JQueryDeferredR; + notifyWith(context: any): JQueryDeferredR; + + always(...alwaysCallbacks: Array<{ (): void }>): JQueryDeferredR; + done(...doneCallbacks: Array<{ (): void }>): JQueryDeferredR; + fail(...failCallbacks: Array<{ (arg: TReject): void }>): JQueryDeferredR; + progress(...progressCallbacks: Array<{ (): void }>): JQueryDeferredR; + promise(target?: any): JQueryPromiseR; + reject(arg: TReject): JQueryDeferredR; + rejectWith(context: any, args: TReject[]): JQueryDeferredR; + resolve(): JQueryDeferredR; + resolveWith(context: any): JQueryDeferredR; + state(): string; + then( + doneCallbacks: { (): UValue }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseVR; + + // U Pipe + then( + doneCallbacks: { (): JQueryDeferredR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): JQueryPromiseR }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks: { (arg: TReject): UReject }, + progressCallbacks?: { (): void } + ): JQueryPromiseR; + + then( + doneCallbacks: { (): void }, + failCallbacks?: { (arg: TReject): void }, + progressCallbacks?: { (): void } + ): JQueryPromise; +} + +/* + Interface of the JQuery extension of the W3C event object +*/ +interface BaseJQueryEventObject extends Event { + data: any; + delegateTarget: Element; + isDefaultPrevented(): boolean; + isImmediatePropagationStopped(): boolean; + isPropagationStopped(): boolean; + originalEvent: Event; + namespace: string; + preventDefault(): any; + relatedTarget: Element; + result: any; + stopImmediatePropagation(): void; + stopPropagation(): void; + pageX: number; + pageY: number; + which: number; + + // Other possible values + cancellable?: boolean; + // detail ?? + prevValue?: any; + view?: Window; +} + +interface JQueryInputEventObject extends BaseJQueryEventObject { + altKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + shiftKey: boolean; +} + +interface JQueryMouseEventObject extends JQueryInputEventObject { + button: number; + clientX: number; + clientY: number; + offsetX: number; + offsetY: number; + pageX: number; + pageY: number; + screenX: number; + screenY: number; +} + +interface JQueryKeyEventObject extends JQueryInputEventObject { + char: any; + charCode: number; + key: any; + keyCode: number; +} + +interface JQueryEventObject + extends BaseJQueryEventObject, + JQueryInputEventObject, + JQueryMouseEventObject, + JQueryKeyEventObject {} + +interface JQueryEventHandler { + (eventObject: JQueryEventObject, args?: any): any; +} + +interface JQuerySupport { + ajax?: boolean; + boxModel?: boolean; + changeBubbles?: boolean; + checkClone?: boolean; + checkOn?: boolean; + cors?: boolean; + cssFloat?: boolean; + hrefNormalized?: boolean; + htmlSerialize?: boolean; + leadingWhitespace?: boolean; + noCloneChecked?: boolean; + noCloneEvent?: boolean; + opacity?: boolean; + optDisabled?: boolean; + optSelected?: boolean; + scriptEval?(): boolean; + style?: boolean; + submitBubbles?: boolean; + tbody?: boolean; +} + +// TODO jsgoupil fix signature +interface JQueryEventStatic { + fix(evt: any): any; +} + +interface JQueryParam { + (obj: any): string; + (obj: any, traditional: boolean): string; +} + +/** + * This is a private type. It exists for type checking. Do not explicitly declare an identifier with this type. + */ +interface _JQueryDeferred { + resolve: Function; + resolveWith: Function; + reject: Function; + rejectWith: Function; +} + +interface JQueryWhen { + (promise1: JQueryPromiseV, promise2: JQueryPromiseV): JQueryPromiseVV; + ( + promise1: JQueryPromiseV, + promise2: JQueryPromiseV, + promise3: JQueryPromiseV + ): JQueryPromiseVVV; + (...deferreds: JQueryPromise[]): JQueryPromise; + apply($: JQueryStatic, deferreds: JQueryPromise[]): JQueryPromise; +} + +/* + Static members of jQuery (those on $ and jQuery themselves) +*/ +interface JQueryStatic { + /**** + AJAX + *****/ + ajax(settings: JQueryAjaxSettings): JQueryXHR; + ajax(url: string, settings?: JQueryAjaxSettings): JQueryXHR; + + ajaxPrefilter(dataTypes: string, handler: (opts: any, originalOpts: any, jqXHR: JQueryXHR) => any): any; + ajaxPrefilter(handler: (opts: any, originalOpts: any, jqXHR: JQueryXHR) => any): any; + + ajaxSettings: JQueryAjaxSettings; + + ajaxSetup(options: JQueryAjaxSettings): void; + + ajaxTransport( + dataType: string, + handler: ( + options: JQueryAjaxSettings, + originalOptions: JQueryAjaxSettings, + jqXHR: JQueryXHR + ) => JQueryTransport + ): any; + + get(url: string, data?: any, success?: any, dataType?: any): JQueryXHR; + getJSON(url: string, data?: any, success?: any): JQueryXHR; + getScript(url: string, success?: any): JQueryXHR; + + param: JQueryParam; + + post(url: string, data?: any, success?: any, dataType?: any): JQueryXHR; + + /********* + CALLBACKS + **********/ + Callbacks(flags?: string): JQueryCallback; + Callbacks(flags?: string): JQueryCallback1; + Callbacks(flags?: string): JQueryCallback2; + Callbacks(flags?: string): JQueryCallback3; + Callbacks(flags?: string): JQueryCallback4; + + /**** + CORE + *****/ + holdReady(hold: boolean): any; + + (selector: string, context?: any): JQuery; + (element: Element): JQuery; + (object: {}): JQuery; + (elementArray: Element[]): JQuery; + (object: JQuery): JQuery; + (func: Function): JQuery; + (array: any[]): JQuery; + (): JQuery; + + noConflict(removeAll?: boolean): Object; + + when: JQueryWhen; + + /*** + CSS + ****/ + css(e: any, propertyName: string, value?: any): JQuery; + css(e: any, propertyName: any, value?: any): JQuery; + cssHooks: { [key: string]: any }; + cssNumber: any; + + /**** + DATA + *****/ + data(element: Document, key?: string, value?: any): any; + data(element: Element, key: string, value: any): any; + data(element: Element, key: string): any; + data(element: Element): any; + + dequeue(element: Element, queueName?: string): any; + + hasData(element: Element): boolean; + + queue(element: Element, queueName?: string): any[]; + queue(element: Element, queueName: string, newQueueOrCallback: any): JQuery; + + removeData(element: Document, name?: string): JQuery; + removeData(element: Element, name?: string): JQuery; + + /******* + EFFECTS + ********/ + fx: { + tick: () => void; + interval: number; + stop: () => void; + speeds: { slow: number; fast: number }; + off: boolean; + step: any; + }; + + /****** + EVENTS + *******/ + proxy(fn: (...args: any[]) => any, context: any, ...args: any[]): any; + proxy(context: any, name: string, ...args: any[]): any; + Deferred: { + (fn?: (d: JQueryDeferred) => void): JQueryDeferred; + new (fn?: (d: JQueryDeferred) => void): JQueryDeferred; + + // Can't use a constraint against JQueryDeferred because the non-generic JQueryDeferred.resolve is not a base type of + // the generic JQueryDeferred.resolve methods. + (fn?: (d: TDeferred) => void): TDeferred; + new (fn?: (d: TDeferred) => void): TDeferred; + }; + Event(name: string, eventProperties?: any): JQueryEventObject; + Event(evt: JQueryEventObject, eventProperties?: any): JQueryEventObject; + + event: JQueryEventStatic; + + /********* + INTERNALS + **********/ + error(message: any): JQuery; + + /************* + MISCELLANEOUS + **************/ + expr: any; + fn: JQuery; + isReady: boolean; + + /********** + PROPERTIES + ***********/ + support: JQuerySupport; + + /********* + UTILITIES + **********/ + contains(container: Element, contained: Element): boolean; + + each(collection: any, callback: (indexInArray: any, valueOfElement: any) => any): any; + each(collection: JQuery, callback: (indexInArray: number, valueOfElement: HTMLElement) => any): JQuery; + each(collection: T[], callback: (indexInArray: number, valueOfElement: T) => void): T[]; + + extend(deep: boolean, target: any, ...objs: any[]): any; + extend(target: any, ...objs: any[]): any; + + globalEval(code: string): any; + + grep(array: T[], func: (elementOfArray: T, indexInArray: number) => boolean, invert?: boolean): T[]; + + inArray(value: T, array: T[], fromIndex?: number): number; + + isArray(obj: any): boolean; + isEmptyObject(obj: any): boolean; + isFunction(obj: any): boolean; + isNumeric(value: any): boolean; + isPlainObject(obj: any): boolean; + isWindow(obj: any): boolean; + isXMLDoc(node: Node): boolean; + + makeArray(obj: any): any[]; + + map(array: T[], callback: (elementOfArray: T, indexInArray: number) => U): U[]; + map(object: { [item: string]: T }, callback: (elementOfArray: T, indexInArray: string) => U): U[]; + map(array: any, callback: (elementOfArray: any, indexInArray: any) => any): any; + + merge(first: T[], second: T[]): T[]; + + noop(): any; + + now(): number; + + parseHTML(data: string, context?: Element, keepScripts?: boolean): Element[]; + + parseJSON(json: string): Object; + + //FIXME: This should return an XMLDocument + parseXML(data: string): any; + + trim(str: string): string; + + type(obj: any): string; + + unique(arr: T[]): T[]; +} + +interface JQueryTransport { + send( + headers: { [index: string]: string }, + completeCallback: ( + status: number, + statusText: string, + responses?: { [dataType: string]: any }, + headers?: string + ) => any + ): any; + abort(): any; +} + +/* + The jQuery instance members +*/ +interface JQuery { + /**** + AJAX + *****/ + ajaxComplete(handler: any): JQuery; + ajaxError(handler: (event: any, jqXHR: any, settings: any, exception: any) => any): JQuery; + ajaxSend(handler: (event: any, jqXHR: any, settings: any, exception: any) => any): JQuery; + ajaxStart(handler: () => any): JQuery; + ajaxStop(handler: () => any): JQuery; + ajaxSuccess(handler: (event: any, jqXHR: any, settings: any, exception: any) => any): JQuery; + + load(url: string, data?: any, complete?: any): JQuery; + + serialize(): string; + serializeArray(): any[]; + + /********** + ATTRIBUTES + ***********/ + addClass(classNames: string): JQuery; + addClass(func: (index: any, currentClass: any) => string): JQuery; + + // http://api.jquery.com/addBack/ + addBack(selector?: string): JQuery; + + attr(attributeName: string): string; + attr(attributeName: string, value: any): JQuery; + attr(map: { [key: string]: any }): JQuery; + attr(attributeName: string, func: (index: any, attr: any) => any): JQuery; + + hasClass(className: string): boolean; + + html(): string; + html(htmlString: number): JQuery; + html(htmlString: string): JQuery; + html(htmlContent: (index: number, oldhtml: string) => string): JQuery; + + prop(propertyName: string): any; + prop(propertyName: string, value: any): JQuery; + prop(map: any): JQuery; + prop(propertyName: string, func: (index: any, oldPropertyValue: any) => any): JQuery; + + removeAttr(attributeName: any): JQuery; + + removeClass(className?: any): JQuery; + removeClass(func: (index: any, cls: any) => any): JQuery; + + removeProp(propertyName: any): JQuery; + + toggleClass(className: any, swtch?: boolean): JQuery; + toggleClass(swtch?: boolean): JQuery; + toggleClass(func: (index: any, cls: any, swtch: any) => any): JQuery; + + val(): any; + val(value: string[]): JQuery; + val(value: string): JQuery; + val(value: number): JQuery; + val(func: (index: any, value: any) => any): JQuery; + + /*** + CSS + ****/ + css(propertyName: string): string; + css(propertyNames: string[]): string; + css(properties: any): JQuery; + css(propertyName: string, value: any): JQuery; + css(propertyName: any, value: any): JQuery; + + height(): number; + height(value: number): JQuery; + height(value: string): JQuery; + height(func: (index: any, height: any) => any): JQuery; + + innerHeight(): number; + innerWidth(): number; + + offset(): { left: number; top: number }; + offset(coordinates: any): JQuery; + offset(func: (index: any, coords: any) => any): JQuery; + + outerHeight(includeMargin?: boolean): number; + outerWidth(includeMargin?: boolean): number; + + position(): { top: number; left: number }; + + scrollLeft(): number; + scrollLeft(value: number): JQuery; + + scrollTop(): number; + scrollTop(value: number): JQuery; + + width(): number; + width(value: number): JQuery; + width(value: string): JQuery; + width(func: (index: any, height: any) => any): JQuery; + + /**** + DATA + *****/ + clearQueue(queueName?: string): JQuery; + + data(key: string, value: any): JQuery; + data(obj: { [key: string]: any }): JQuery; + data(key?: string): any; + + dequeue(queueName?: string): JQuery; + + removeData(nameOrList?: any): JQuery; + + /******** + DEFERRED + *********/ + promise(type?: any, target?: any): JQueryPromise; + + /******* + EFFECTS + ********/ + animate(properties: any, duration?: any, complete?: Function): JQuery; + animate(properties: any, duration?: any, easing?: string, complete?: Function): JQuery; + animate( + properties: any, + options: { + duration?: any; + easing?: string; + complete?: Function; + step?: Function; + queue?: boolean; + specialEasing?: any; + } + ): JQuery; + + delay(duration: number, queueName?: string): JQuery; + + fadeIn(duration?: any, callback?: any): JQuery; + fadeIn(duration?: any, easing?: string, callback?: any): JQuery; + + fadeOut(duration?: any, callback?: any): JQuery; + fadeOut(duration?: any, easing?: string, callback?: any): JQuery; + + fadeTo(duration: any, opacity: number, callback?: any): JQuery; + fadeTo(duration: any, opacity: number, easing?: string, callback?: any): JQuery; + + fadeToggle(duration?: any, callback?: any): JQuery; + fadeToggle(duration?: any, easing?: string, callback?: any): JQuery; + + finish(): JQuery; + + hide(duration?: any, callback?: any): JQuery; + hide(duration?: any, easing?: string, callback?: any): JQuery; + + show(duration?: any, callback?: any): JQuery; + show(duration?: any, easing?: string, callback?: any): JQuery; + + slideDown(duration?: any, callback?: any): JQuery; + slideDown(duration?: any, easing?: string, callback?: any): JQuery; + + slideToggle(duration?: any, callback?: any): JQuery; + slideToggle(duration?: any, easing?: string, callback?: any): JQuery; + + slideUp(duration?: any, callback?: any): JQuery; + slideUp(duration?: any, easing?: string, callback?: any): JQuery; + + stop(clearQueue?: boolean, jumpToEnd?: boolean): JQuery; + stop(queue?: any, clearQueue?: boolean, jumpToEnd?: boolean): JQuery; + + toggle(duration?: any, callback?: any): JQuery; + toggle(duration?: any, easing?: string, callback?: any): JQuery; + toggle(showOrHide: boolean): JQuery; + + /****** + EVENTS + *******/ + bind(eventType: string, eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + bind(eventType: string, eventData: any, preventBubble: boolean): JQuery; + bind(eventType: string, preventBubble: boolean): JQuery; + bind(...events: any[]): JQuery; + + blur(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + blur(handler: (eventObject: JQueryEventObject) => any): JQuery; + + change(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + change(handler: (eventObject: JQueryEventObject) => any): JQuery; + + click(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + click(handler: (eventObject: JQueryEventObject) => any): JQuery; + + dblclick(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + dblclick(handler: (eventObject: JQueryEventObject) => any): JQuery; + + delegate(selector: any, eventType: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + + focus(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + focus(handler: (eventObject: JQueryEventObject) => any): JQuery; + + focusin(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + focusin(handler: (eventObject: JQueryEventObject) => any): JQuery; + + focusout(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + focusout(handler: (eventObject: JQueryEventObject) => any): JQuery; + + hover( + handlerIn: (eventObject: JQueryEventObject) => any, + handlerOut: (eventObject: JQueryEventObject) => any + ): JQuery; + hover(handlerInOut: (eventObject: JQueryEventObject) => any): JQuery; + + keydown(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + keydown(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + + keypress(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + keypress(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + + keyup(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + keyup(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + + load(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + load(handler: (eventObject: JQueryEventObject) => any): JQuery; + + mousedown(): JQuery; + mousedown(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mousedown(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + mouseevent(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mouseevent(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + mouseenter(): JQuery; + mouseenter(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mouseenter(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + mouseleave(): JQuery; + mouseleave(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mouseleave(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + mousemove(): JQuery; + mousemove(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mousemove(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + mouseout(): JQuery; + mouseout(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mouseout(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + mouseover(): JQuery; + mouseover(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mouseover(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + mouseup(): JQuery; + mouseup(eventData: any, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + mouseup(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + off(events?: string, selector?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + off(eventsMap: { [key: string]: any }, selector?: any): JQuery; + + on(events: string, selector: any, data: any, handler: (eventObject: JQueryEventObject, args: any) => any): JQuery; + on(events: string, selector: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + on(events: string, handler: (eventObject: JQueryEventObject, args: any) => any): JQuery; + on(eventsMap: { [key: string]: any }, selector?: any, data?: any): JQuery; + + one(events: string, selector?: any, data?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + one(eventsMap: { [key: string]: any }, selector?: any, data?: any): JQuery; + + ready(handler: any): JQuery; + + resize(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + resize(handler: (eventObject: JQueryEventObject) => any): JQuery; + + scroll(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + scroll(handler: (eventObject: JQueryEventObject) => any): JQuery; + + select(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + select(handler: (eventObject: JQueryEventObject) => any): JQuery; + + submit(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + submit(handler: (eventObject: JQueryEventObject) => any): JQuery; + + trigger(eventType: string, ...extraParameters: any[]): JQuery; + trigger(event: JQueryEventObject, ...extraParameters: any[]): JQuery; + + triggerHandler(eventType: string, ...extraParameters: any[]): Object; + // JSGOUPIL: triggerHandler uses trigger, not documented though + triggerHandler(evt: JQueryEventObject): Object; + + unbind(eventType?: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + unbind(eventType: string, fls: boolean): JQuery; + unbind(evt: any): JQuery; + + undelegate(): JQuery; + undelegate(selector: any, eventType: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + undelegate(selector: any, events: any): JQuery; + undelegate(namespace: string): JQuery; + + unload(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + unload(handler: (eventObject: JQueryEventObject) => any): JQuery; + + /********* + INTERNALS + **********/ + + context: Element; + jquery: string; + + error(handler: (eventObject: JQueryEventObject) => any): JQuery; + error(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + + pushStack(elements: any[]): JQuery; + pushStack(elements: any[], name: any, arguments: any): JQuery; + + /************ + MANIPULATION + *************/ + after(...content: any[]): JQuery; + after(func: (index: any) => any): JQuery; + + append(...content: any[]): JQuery; + append(func: (index: any, html: any) => any): JQuery; + + appendTo(target: any): JQuery; + + before(...content: any[]): JQuery; + before(func: (index: any) => any): JQuery; + + clone(withDataAndEvents?: boolean, deepWithDataAndEvents?: boolean): JQuery; + + detach(selector?: any): JQuery; + + empty(): JQuery; + + insertAfter(target: any): JQuery; + insertBefore(target: any): JQuery; + + prepend(...content: any[]): JQuery; + prepend(func: (index: any, html: any) => any): JQuery; + + prependTo(target: any): JQuery; + + remove(selector?: any): JQuery; + + replaceAll(target: any): JQuery; + + replaceWith(func: any): JQuery; + + text(): string; + text(textString: any): JQuery; + text(textString: (index: number, text: string) => string): JQuery; + + toArray(): any[]; + + unwrap(): JQuery; + + wrap(wrappingElement: any): JQuery; + wrap(func: (index: any) => any): JQuery; + + wrapAll(wrappingElement: any): JQuery; + + wrapInner(wrappingElement: any): JQuery; + wrapInner(func: (index: any) => any): JQuery; + + /************* + MISCELLANEOUS + **************/ + each(func: (index: any, elem: Element) => any): JQuery; + + get(index?: number): any; + + index(): number; + index(selector: string): number; + index(element: any): number; + + /********** + PROPERTIES + ***********/ + length: number; + selector: string; + [x: string]: any; + [x: number]: HTMLElement; + + /********** + TRAVERSING + ***********/ + add(selector: string, context?: any): JQuery; + add(...elements: any[]): JQuery; + add(html: string): JQuery; + add(obj: JQuery): JQuery; + + children(selector?: any): JQuery; + + closest(selector: string): JQuery; + closest(selector: string, context?: Element): JQuery; + closest(obj: JQuery): JQuery; + closest(element: any): JQuery; + closest(selectors: any, context?: Element): any[]; + + contents(): JQuery; + + end(): JQuery; + + eq(index: number): JQuery; + + filter(selector: string): JQuery; + filter(func: (index: any) => any): JQuery; + filter(element: any): JQuery; + filter(obj: JQuery): JQuery; + + find(selector: string): JQuery; + find(element: any): JQuery; + find(obj: JQuery): JQuery; + + first(): JQuery; + + has(selector: string): JQuery; + has(contained: Element): JQuery; + + is(selector: string): boolean; + is(func: (index: any) => any): boolean; + is(element: any): boolean; + is(obj: JQuery): boolean; + + last(): JQuery; + + map(callback: (index: any, domElement: Element) => any): JQuery; + + next(selector?: string): JQuery; + + nextAll(selector?: string): JQuery; + + nextUntil(selector?: string, filter?: string): JQuery; + nextUntil(element?: Element, filter?: string): JQuery; + + not(selector: string): JQuery; + not(func: (index: any) => any): JQuery; + not(element: any): JQuery; + not(obj: JQuery): JQuery; + + offsetParent(): JQuery; + + parent(selector?: string): JQuery; + + parents(selector?: string): JQuery; + + parentsUntil(selector?: string, filter?: string): JQuery; + parentsUntil(element?: Element, filter?: string): JQuery; + + prev(selector?: string): JQuery; + + prevAll(selector?: string): JQuery; + + prevUntil(selector?: string, filter?: string): JQuery; + prevUntil(element?: Element, filter?: string): JQuery; + + siblings(selector?: string): JQuery; + + slice(start: number, end?: number): JQuery; + + /********* + UTILITIES + **********/ + + queue(queueName?: string): any[]; + queue(queueName: string, newQueueOrCallback: any): JQuery; + queue(newQueueOrCallback: any): JQuery; +} + +interface EventTarget { + //nodeName: string; //bugfix, duplicate identifier. see: http://stackoverflow.com/questions/14824143/duplicate-identifier-nodename-in-jquery-d-ts +} + +// TODO Rmove these and make jquery a proper module +declare module "jquery"; +declare var jQuery: JQueryStatic; +declare var $: JQueryStatic; diff --git a/src/Explorer/ComponentRegisterer.test.ts b/src/Explorer/ComponentRegisterer.test.ts index ed3596c0f..f55d41185 100644 --- a/src/Explorer/ComponentRegisterer.test.ts +++ b/src/Explorer/ComponentRegisterer.test.ts @@ -1,122 +1,122 @@ -jest.mock("monaco-editor"); - -import * as ko from "knockout"; -import "./ComponentRegisterer"; - -describe("Component Registerer", () => { - it("should register input-typeahead component", () => { - expect(ko.components.isRegistered("input-typeahead")).toBe(true); - }); - - it("should register new-vertex-form component", () => { - expect(ko.components.isRegistered("new-vertex-form")).toBe(true); - }); - - it("should register error-display component", () => { - expect(ko.components.isRegistered("error-display")).toBe(true); - }); - - it("should register graph-style component", () => { - expect(ko.components.isRegistered("graph-style")).toBe(true); - }); - - it("should register collapsible-panel component", () => { - expect(ko.components.isRegistered("collapsible-panel")).toBe(true); - }); - - it("should register json-editor component", () => { - expect(ko.components.isRegistered("json-editor")).toBe(true); - }); - - it("should register documents-tab component", () => { - expect(ko.components.isRegistered("documents-tab")).toBe(true); - }); - - it("should register stored-procedure-tab component", () => { - expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true); - }); - - it("should register trigger-tab component", () => { - expect(ko.components.isRegistered("trigger-tab")).toBe(true); - }); - - it("should register user-defined-function-tab component", () => { - expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true); - }); - - it("should register settings-tab-v2 component", () => { - expect(ko.components.isRegistered("settings-tab-v2")).toBe(true); - }); - - it("should register query-tab component", () => { - expect(ko.components.isRegistered("query-tab")).toBe(true); - }); - - it("should register tables-query-tab component", () => { - expect(ko.components.isRegistered("tables-query-tab")).toBe(true); - }); - - it("should register graph-tab component", () => { - expect(ko.components.isRegistered("graph-tab")).toBe(true); - }); - - it("should register notebookv2-tab component", () => { - expect(ko.components.isRegistered("notebookv2-tab")).toBe(true); - }); - - it("should register terminal-tab component", () => { - expect(ko.components.isRegistered("terminal-tab")).toBe(true); - }); - - it("should register spark-master-tab component", () => { - expect(ko.components.isRegistered("spark-master-tab")).toBe(true); - }); - - it("should register mongo-shell-tab component", () => { - expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true); - }); - - it("should registeradd-collection-pane component", () => { - expect(ko.components.isRegistered("add-collection-pane")).toBe(true); - }); - - it("should register delete-collection-confirmation-pane component", () => { - expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true); - }); - - it("should register delete-database-confirmation-pane component", () => { - expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true); - }); - - it("should register save-query-pane component", () => { - expect(ko.components.isRegistered("save-query-pane")).toBe(true); - }); - - it("should register browse-queries-pane component", () => { - expect(ko.components.isRegistered("browse-queries-pane")).toBe(true); - }); - - it("should register graph-new-vertex-pane component", () => { - expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true); - }); - - it("should register graph-styling-pane component", () => { - expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); - }); - - it("should register upload-file-pane component", () => { - expect(ko.components.isRegistered("upload-file-pane")).toBe(true); - }); - - it("should register string-input-pane component", () => { - expect(ko.components.isRegistered("string-input-pane")).toBe(true); - }); - - it("should register setup-notebooks-pane component", () => { - expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true); - }); - - it("should register dynamic-list component", () => { - expect(ko.components.isRegistered("dynamic-list")).toBe(true); - }); -}); +jest.mock("monaco-editor"); + +import * as ko from "knockout"; +import "./ComponentRegisterer"; + +describe("Component Registerer", () => { + it("should register input-typeahead component", () => { + expect(ko.components.isRegistered("input-typeahead")).toBe(true); + }); + + it("should register new-vertex-form component", () => { + expect(ko.components.isRegistered("new-vertex-form")).toBe(true); + }); + + it("should register error-display component", () => { + expect(ko.components.isRegistered("error-display")).toBe(true); + }); + + it("should register graph-style component", () => { + expect(ko.components.isRegistered("graph-style")).toBe(true); + }); + + it("should register collapsible-panel component", () => { + expect(ko.components.isRegistered("collapsible-panel")).toBe(true); + }); + + it("should register json-editor component", () => { + expect(ko.components.isRegistered("json-editor")).toBe(true); + }); + + it("should register documents-tab component", () => { + expect(ko.components.isRegistered("documents-tab")).toBe(true); + }); + + it("should register stored-procedure-tab component", () => { + expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true); + }); + + it("should register trigger-tab component", () => { + expect(ko.components.isRegistered("trigger-tab")).toBe(true); + }); + + it("should register user-defined-function-tab component", () => { + expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true); + }); + + it("should register settings-tab-v2 component", () => { + expect(ko.components.isRegistered("settings-tab-v2")).toBe(true); + }); + + it("should register query-tab component", () => { + expect(ko.components.isRegistered("query-tab")).toBe(true); + }); + + it("should register tables-query-tab component", () => { + expect(ko.components.isRegistered("tables-query-tab")).toBe(true); + }); + + it("should register graph-tab component", () => { + expect(ko.components.isRegistered("graph-tab")).toBe(true); + }); + + it("should register notebookv2-tab component", () => { + expect(ko.components.isRegistered("notebookv2-tab")).toBe(true); + }); + + it("should register terminal-tab component", () => { + expect(ko.components.isRegistered("terminal-tab")).toBe(true); + }); + + it("should register spark-master-tab component", () => { + expect(ko.components.isRegistered("spark-master-tab")).toBe(true); + }); + + it("should register mongo-shell-tab component", () => { + expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true); + }); + + it("should registeradd-collection-pane component", () => { + expect(ko.components.isRegistered("add-collection-pane")).toBe(true); + }); + + it("should register delete-collection-confirmation-pane component", () => { + expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true); + }); + + it("should register delete-database-confirmation-pane component", () => { + expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true); + }); + + it("should register save-query-pane component", () => { + expect(ko.components.isRegistered("save-query-pane")).toBe(true); + }); + + it("should register browse-queries-pane component", () => { + expect(ko.components.isRegistered("browse-queries-pane")).toBe(true); + }); + + it("should register graph-new-vertex-pane component", () => { + expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true); + }); + + it("should register graph-styling-pane component", () => { + expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); + }); + + it("should register upload-file-pane component", () => { + expect(ko.components.isRegistered("upload-file-pane")).toBe(true); + }); + + it("should register string-input-pane component", () => { + expect(ko.components.isRegistered("string-input-pane")).toBe(true); + }); + + it("should register setup-notebooks-pane component", () => { + expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true); + }); + + it("should register dynamic-list component", () => { + expect(ko.components.isRegistered("dynamic-list")).toBe(true); + }); +}); diff --git a/src/Explorer/ComponentRegisterer.ts b/src/Explorer/ComponentRegisterer.ts index bc7c47a7d..4d49a52bf 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -1,77 +1,77 @@ -import * as ko from "knockout"; -import * as PaneComponents from "./Panes/PaneComponents"; -import * as TabComponents from "./Tabs/TabComponents"; -import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent"; -import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; -import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; -import { EditorComponent } from "./Controls/Editor/EditorComponent"; -import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent"; -import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent"; -import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; -import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; -import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent"; -import { TabsManagerKOComponent } from "./Tabs/TabsManager"; -import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; - -ko.components.register("input-typeahead", new InputTypeaheadComponent()); -ko.components.register("new-vertex-form", NewVertexComponent); -ko.components.register("error-display", new ErrorDisplayComponent()); -ko.components.register("graph-style", GraphStyleComponent); -ko.components.register("collapsible-panel", new CollapsiblePanelComponent()); -ko.components.register("editor", new EditorComponent()); -ko.components.register("json-editor", new JsonEditorComponent()); -ko.components.register("diff-editor", new DiffEditorComponent()); -ko.components.register("dynamic-list", DynamicListComponent); -ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); -ko.components.register("tabs-manager", TabsManagerKOComponent()); - -// Collection Tabs -ko.components.register("documents-tab", new TabComponents.DocumentsTab()); -ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab()); -ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); -ko.components.register("trigger-tab", new TabComponents.TriggerTab()); -ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab()); -ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2()); -ko.components.register("query-tab", new TabComponents.QueryTab()); -ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab()); -ko.components.register("graph-tab", new TabComponents.GraphTab()); -ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab()); -ko.components.register("conflicts-tab", new TabComponents.ConflictsTab()); -ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab()); -ko.components.register("terminal-tab", new TabComponents.TerminalTab()); -ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab()); -ko.components.register("gallery-tab", new TabComponents.GalleryTab()); -ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab()); - -// Database Tabs -ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab()); - -// Panes -ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); -ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent()); -ko.components.register( - "delete-collection-confirmation-pane", - new PaneComponents.DeleteCollectionConfirmationPaneComponent() -); -ko.components.register( - "delete-database-confirmation-pane", - new PaneComponents.DeleteDatabaseConfirmationPaneComponent() -); -ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent()); -ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); -ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); -ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); -ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent()); -ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent()); -ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); -ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent()); -ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent()); -ko.components.register("renew-adhoc-access-pane", new PaneComponents.RenewAdHocAccessPane()); -ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent()); -ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent()); -ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent()); -ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent()); -ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent()); -ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); -ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); -ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); +import * as ko from "knockout"; +import * as PaneComponents from "./Panes/PaneComponents"; +import * as TabComponents from "./Tabs/TabComponents"; +import { CollapsiblePanelComponent } from "./Controls/CollapsiblePanel/CollapsiblePanelComponent"; +import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; +import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; +import { EditorComponent } from "./Controls/Editor/EditorComponent"; +import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent"; +import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent"; +import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; +import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; +import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent"; +import { TabsManagerKOComponent } from "./Tabs/TabsManager"; +import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; + +ko.components.register("input-typeahead", new InputTypeaheadComponent()); +ko.components.register("new-vertex-form", NewVertexComponent); +ko.components.register("error-display", new ErrorDisplayComponent()); +ko.components.register("graph-style", GraphStyleComponent); +ko.components.register("collapsible-panel", new CollapsiblePanelComponent()); +ko.components.register("editor", new EditorComponent()); +ko.components.register("json-editor", new JsonEditorComponent()); +ko.components.register("diff-editor", new DiffEditorComponent()); +ko.components.register("dynamic-list", DynamicListComponent); +ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); +ko.components.register("tabs-manager", TabsManagerKOComponent()); + +// Collection Tabs +ko.components.register("documents-tab", new TabComponents.DocumentsTab()); +ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab()); +ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); +ko.components.register("trigger-tab", new TabComponents.TriggerTab()); +ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab()); +ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2()); +ko.components.register("query-tab", new TabComponents.QueryTab()); +ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab()); +ko.components.register("graph-tab", new TabComponents.GraphTab()); +ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab()); +ko.components.register("conflicts-tab", new TabComponents.ConflictsTab()); +ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab()); +ko.components.register("terminal-tab", new TabComponents.TerminalTab()); +ko.components.register("spark-master-tab", new TabComponents.SparkMasterTab()); +ko.components.register("gallery-tab", new TabComponents.GalleryTab()); +ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab()); + +// Database Tabs +ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab()); + +// Panes +ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); +ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent()); +ko.components.register( + "delete-collection-confirmation-pane", + new PaneComponents.DeleteCollectionConfirmationPaneComponent() +); +ko.components.register( + "delete-database-confirmation-pane", + new PaneComponents.DeleteDatabaseConfirmationPaneComponent() +); +ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent()); +ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); +ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); +ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); +ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent()); +ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent()); +ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); +ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent()); +ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent()); +ko.components.register("renew-adhoc-access-pane", new PaneComponents.RenewAdHocAccessPane()); +ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent()); +ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent()); +ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent()); +ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent()); +ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent()); +ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); +ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); +ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts index b188c4500..8aaf8cce9 100644 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ b/src/Explorer/ContextMenuButtonFactory.ts @@ -36,8 +36,8 @@ export class ResourceTreeContextMenuButtonFactory { { iconSrc: AddCollectionIcon, onClick: () => container.onNewCollectionClicked(), - label: container.addCollectionText() - } + label: container.addCollectionText(), + }, ]; if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) { @@ -45,7 +45,7 @@ export class ResourceTreeContextMenuButtonFactory { iconSrc: DeleteDatabaseIcon, onClick: () => container.deleteDatabaseConfirmationPane.open(), label: container.deleteDatabaseText(), - styleClass: "deleteDatabaseMenuItem" + styleClass: "deleteDatabaseMenuItem", }); } return items; @@ -60,7 +60,7 @@ export class ResourceTreeContextMenuButtonFactory { items.push({ iconSrc: AddSqlQueryIcon, onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null), - label: "New SQL Query" + label: "New SQL Query", }); } @@ -68,7 +68,7 @@ export class ResourceTreeContextMenuButtonFactory { items.push({ iconSrc: AddSqlQueryIcon, onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null), - label: "New Query" + label: "New Query", }); items.push({ @@ -77,7 +77,7 @@ export class ResourceTreeContextMenuButtonFactory { const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); selectedCollection && selectedCollection.onNewMongoShellClick(); }, - label: "New Shell" + label: "New Shell", }); } @@ -88,7 +88,7 @@ export class ResourceTreeContextMenuButtonFactory { const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null); }, - label: "New Stored Procedure" + label: "New Stored Procedure", }); items.push({ @@ -97,7 +97,7 @@ export class ResourceTreeContextMenuButtonFactory { const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null); }, - label: "New UDF" + label: "New UDF", }); items.push({ @@ -106,7 +106,7 @@ export class ResourceTreeContextMenuButtonFactory { const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null); }, - label: "New Trigger" + label: "New Trigger", }); } @@ -117,7 +117,7 @@ export class ResourceTreeContextMenuButtonFactory { selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null); }, label: container.deleteCollectionText(), - styleClass: "deleteCollectionMenuItem" + styleClass: "deleteCollectionMenuItem", }); return items; @@ -135,8 +135,8 @@ export class ResourceTreeContextMenuButtonFactory { { iconSrc: DeleteSprocIcon, onClick: () => storedProcedure.delete(), - label: "Delete Store Procedure" - } + label: "Delete Store Procedure", + }, ]; } @@ -149,8 +149,8 @@ export class ResourceTreeContextMenuButtonFactory { { iconSrc: DeleteTriggerIcon, onClick: () => trigger.delete(), - label: "Delete Trigger" - } + label: "Delete Trigger", + }, ]; } @@ -166,8 +166,8 @@ export class ResourceTreeContextMenuButtonFactory { { iconSrc: DeleteUDFIcon, onClick: () => userDefinedFunction.delete(), - label: "Delete User Defined Function" - } + label: "Delete User Defined Function", + }, ]; } } diff --git a/src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx b/src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx index d365f8c7a..f46290bff 100644 --- a/src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx +++ b/src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx @@ -31,7 +31,7 @@ export class AccessibleElement extends React.Component { ...elementProps, onKeyPress: this.onKeyPress, onClick: this.props.onActivated, - tabIndex + tabIndex, }); } } diff --git a/src/Explorer/Controls/Accordion/AccordionComponent.tsx b/src/Explorer/Controls/Accordion/AccordionComponent.tsx index a8fada8c3..90685b6ba 100644 --- a/src/Explorer/Controls/Accordion/AccordionComponent.tsx +++ b/src/Explorer/Controls/Accordion/AccordionComponent.tsx @@ -38,7 +38,7 @@ export class AccordionItemComponent extends React.Component { + let workspaceMenuItems: IContextualMenuItem[] = workspaces.map((workspace) => { let sparkPoolsMenuProps: IContextualMenuProps = { items: workspace.sparkPools.map( (sparkpool): IContextualMenuItem => ({ key: sparkpool.id, text: sparkpool.name, - onClick: this._onSparkPoolClicked + onClick: this._onSparkPoolClicked, }) - ) + ), }; if (!sparkPoolsMenuProps.items.length) { sparkPoolsMenuProps.items.push({ key: workspace.id, text: "Create new spark pool", - onClick: this._onCreateNewSparkPoolClicked + onClick: this._onCreateNewSparkPoolClicked, }); } return { key: workspace.id, text: workspace.name, - subMenuProps: this.props.disableSubmenu ? undefined : sparkPoolsMenuProps + subMenuProps: this.props.disableSubmenu ? undefined : sparkPoolsMenuProps, }; }); @@ -94,7 +94,7 @@ export class ArcadiaMenuPicker extends React.Component diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts b/src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts index b0f398f1a..9297a0423 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts @@ -1,56 +1,56 @@ -import * as ko from "knockout"; -import template from "./collapsible-panel-component.html"; - -/** - * Helper class for ko component registration - */ -export class CollapsiblePanelComponent { - constructor() { - return { - viewModel: CollapsiblePanelViewModel, - template - }; - } -} - -/** - * Parameters for this component - */ -interface CollapsiblePanelParams { - collapsedTitle: ko.Observable; - expandedTitle: ko.Observable; - isCollapsed?: ko.Observable; - collapseToLeft?: boolean; -} - -/** - * Collapsible panel: - * Contains a header with [>] button to collapse and an title ("expandedTitle"). - * Collapsing the panel: - * - shrinks width to narrow amount - * - hides children - * - shows [<] - * - shows vertical title ("collapsedTitle") - * - the default behavior is to collapse to the right (ie, place this component on the right or use "collapseToLeft" parameter) - * - * How to use in your markup: - * - * - * - * - * Use the optional "isCollapsed" parameter to programmatically collapse/expand the pane from outside the component. - * Use the optional "collapseToLeft" parameter to collapse to the left. - */ -class CollapsiblePanelViewModel { - public params: CollapsiblePanelParams; - private isCollapsed: ko.Observable; - - public constructor(params: CollapsiblePanelParams) { - this.params = params; - this.isCollapsed = params.isCollapsed || ko.observable(false); - } - - public toggleCollapse(): void { - this.isCollapsed(!this.isCollapsed()); - } -} +import * as ko from "knockout"; +import template from "./collapsible-panel-component.html"; + +/** + * Helper class for ko component registration + */ +export class CollapsiblePanelComponent { + constructor() { + return { + viewModel: CollapsiblePanelViewModel, + template, + }; + } +} + +/** + * Parameters for this component + */ +interface CollapsiblePanelParams { + collapsedTitle: ko.Observable; + expandedTitle: ko.Observable; + isCollapsed?: ko.Observable; + collapseToLeft?: boolean; +} + +/** + * Collapsible panel: + * Contains a header with [>] button to collapse and an title ("expandedTitle"). + * Collapsing the panel: + * - shrinks width to narrow amount + * - hides children + * - shows [<] + * - shows vertical title ("collapsedTitle") + * - the default behavior is to collapse to the right (ie, place this component on the right or use "collapseToLeft" parameter) + * + * How to use in your markup: + * + * + * + * + * Use the optional "isCollapsed" parameter to programmatically collapse/expand the pane from outside the component. + * Use the optional "collapseToLeft" parameter to collapse to the left. + */ +class CollapsiblePanelViewModel { + public params: CollapsiblePanelParams; + private isCollapsed: ko.Observable; + + public constructor(params: CollapsiblePanelParams) { + this.params = params; + this.isCollapsed = params.isCollapsed || ko.observable(false); + } + + public toggleCollapse(): void { + this.isCollapsed(!this.isCollapsed()); + } +} diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.test.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.test.tsx index 5b9eb0fe5..6193ac23b 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.test.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.test.tsx @@ -5,7 +5,7 @@ import { CollapsibleSectionComponent, CollapsibleSectionProps } from "./Collapsi describe("CollapsibleSectionComponent", () => { it("renders", () => { const props: CollapsibleSectionProps = { - title: "Sample title" + title: "Sample title", }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx index b7dea8c36..ef59eab1c 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -14,7 +14,7 @@ export class CollapsibleSectionComponent extends React.Component -
- - Collapse - - -
- - - -
- - - - -
- +
+
+ + Collapse + + +
+ + + +
+ + + + +
+
diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index afe771bf4..93c53c56d 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -149,9 +149,7 @@ export class CommandButtonComponent extends React.Component): boolean { if (event.keyCode === KeyCodes.DownArrow) { $(this.dropdownElt).hide(); - $(this.dropdownElt) - .show() - .focus(); + $(this.dropdownElt).show().focus(); event.stopPropagation(); return false; } @@ -187,7 +185,7 @@ export class CommandButtonComponent extends React.Component, newValue?: string) => void; - defaultValue?: string; -} - -export interface LinkProps { - linkText: string; - linkUrl: string; -} - -export interface DialogProps { - title: string; - subText: string; - isModal: boolean; - visible: boolean; - choiceGroupProps?: IChoiceGroupProps; - textFieldProps?: TextFieldProps; - linkProps?: LinkProps; - primaryButtonText: string; - secondaryButtonText: string; - onPrimaryButtonClick: () => void; - onSecondaryButtonClick: () => void; - primaryButtonDisabled?: boolean; - type?: DialogType; - showCloseButton?: boolean; - onDismiss?: () => void; -} - -const DIALOG_MIN_WIDTH = "400px"; -const DIALOG_MAX_WIDTH = "600px"; -const DIALOG_TITLE_FONT_SIZE = "17px"; -const DIALOG_TITLE_FONT_WEIGHT = 400; -const DIALOG_SUBTEXT_FONT_SIZE = "15px"; - -export class DialogComponent extends React.Component { - constructor(props: DialogProps) { - super(props); - } - - public render(): JSX.Element { - const dialogProps: IDialogProps = { - hidden: !this.props.visible, - dialogContentProps: { - type: this.props.type || DialogType.normal, - title: this.props.title, - subText: this.props.subText, - styles: { - title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT }, - subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE } - }, - showCloseButton: this.props.showCloseButton || false, - onDismiss: this.props.onDismiss - }, - modalProps: { isBlocking: this.props.isModal }, - minWidth: DIALOG_MIN_WIDTH, - maxWidth: DIALOG_MAX_WIDTH - }; - const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps; - const textFieldProps: ITextFieldProps = this.props.textFieldProps; - const linkProps: LinkProps = this.props.linkProps; - const primaryButtonProps: IButtonProps = { - text: this.props.primaryButtonText, - disabled: this.props.primaryButtonDisabled || false, - onClick: this.props.onPrimaryButtonClick - }; - const secondaryButtonProps: IButtonProps = - this.props.secondaryButtonText && this.props.onSecondaryButtonClick - ? { - text: this.props.secondaryButtonText, - onClick: this.props.onSecondaryButtonClick - } - : undefined; - - return ( - - {choiceGroupProps && } - {textFieldProps && } - {linkProps && ( - - {linkProps.linkText} - - )} - - - {secondaryButtonProps && } - - - ); - } -} +import * as React from "react"; +import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog"; +import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button"; +import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField"; +import { Link } from "office-ui-fabric-react/lib/Link"; +import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react"; + +export interface TextFieldProps extends ITextFieldProps { + label: string; + multiline: boolean; + autoAdjustHeight: boolean; + rows: number; + onChange: (event: React.FormEvent, newValue?: string) => void; + defaultValue?: string; +} + +export interface LinkProps { + linkText: string; + linkUrl: string; +} + +export interface DialogProps { + title: string; + subText: string; + isModal: boolean; + visible: boolean; + choiceGroupProps?: IChoiceGroupProps; + textFieldProps?: TextFieldProps; + linkProps?: LinkProps; + primaryButtonText: string; + secondaryButtonText: string; + onPrimaryButtonClick: () => void; + onSecondaryButtonClick: () => void; + primaryButtonDisabled?: boolean; + type?: DialogType; + showCloseButton?: boolean; + onDismiss?: () => void; +} + +const DIALOG_MIN_WIDTH = "400px"; +const DIALOG_MAX_WIDTH = "600px"; +const DIALOG_TITLE_FONT_SIZE = "17px"; +const DIALOG_TITLE_FONT_WEIGHT = 400; +const DIALOG_SUBTEXT_FONT_SIZE = "15px"; + +export class DialogComponent extends React.Component { + constructor(props: DialogProps) { + super(props); + } + + public render(): JSX.Element { + const dialogProps: IDialogProps = { + hidden: !this.props.visible, + dialogContentProps: { + type: this.props.type || DialogType.normal, + title: this.props.title, + subText: this.props.subText, + styles: { + title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT }, + subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }, + }, + showCloseButton: this.props.showCloseButton || false, + onDismiss: this.props.onDismiss, + }, + modalProps: { isBlocking: this.props.isModal }, + minWidth: DIALOG_MIN_WIDTH, + maxWidth: DIALOG_MAX_WIDTH, + }; + const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps; + const textFieldProps: ITextFieldProps = this.props.textFieldProps; + const linkProps: LinkProps = this.props.linkProps; + const primaryButtonProps: IButtonProps = { + text: this.props.primaryButtonText, + disabled: this.props.primaryButtonDisabled || false, + onClick: this.props.onPrimaryButtonClick, + }; + const secondaryButtonProps: IButtonProps = + this.props.secondaryButtonText && this.props.onSecondaryButtonClick + ? { + text: this.props.secondaryButtonText, + onClick: this.props.onSecondaryButtonClick, + } + : undefined; + + return ( + + {choiceGroupProps && } + {textFieldProps && } + {linkProps && ( + + {linkProps.linkText} + + )} + + + {secondaryButtonProps && } + + + ); + } +} diff --git a/src/Explorer/Controls/DialogReactComponent/DialogComponentAdapter.tsx b/src/Explorer/Controls/DialogReactComponent/DialogComponentAdapter.tsx index 0364cbb91..8e1db3b10 100644 --- a/src/Explorer/Controls/DialogReactComponent/DialogComponentAdapter.tsx +++ b/src/Explorer/Controls/DialogReactComponent/DialogComponentAdapter.tsx @@ -1,16 +1,16 @@ -/** - * This adapter is responsible to render the Dialog React component - * If the component signals a change through the callback passed in the properties, it must render the React component when appropriate - * and update any knockout observables passed from the parent. - */ -import * as React from "react"; -import { DialogComponent, DialogProps } from "./DialogComponent"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; - -export class DialogComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - public renderComponent(): JSX.Element { - return ; - } -} +/** + * This adapter is responsible to render the Dialog React component + * If the component signals a change through the callback passed in the properties, it must render the React component when appropriate + * and update any knockout observables passed from the parent. + */ +import * as React from "react"; +import { DialogComponent, DialogProps } from "./DialogComponent"; +import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; + +export class DialogComponentAdapter implements ReactAdapter { + public parameters: ko.Observable; + + public renderComponent(): JSX.Element { + return ; + } +} diff --git a/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts b/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts index a37b16b14..854cb26f3 100644 --- a/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts +++ b/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts @@ -9,7 +9,7 @@ export class DiffEditorComponent { constructor() { return { viewModel: DiffEditorViewModel, - template + template, }; } } @@ -103,7 +103,7 @@ export class DiffEditorViewModel { lineNumbers: this.params.lineNumbers || "off", fontSize: 12, ariaLabel: this.params.ariaLabel, - theme: this.params.theme + theme: this.params.theme, }; if (this.params.renderSideBySide !== undefined) { @@ -120,7 +120,7 @@ export class DiffEditorViewModel { ); diffEditor.setModel({ original: originalModel, - modified: modifiedModel + modified: modifiedModel, }); createCallback(diffEditor); @@ -147,7 +147,7 @@ export class DiffEditorViewModel { this.observer.observe(document.body, { attributes: true, subtree: true, - childList: true + childList: true, }); this.editor.focus(); } diff --git a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx b/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx index 66620a818..1fca6262d 100644 --- a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx +++ b/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx @@ -7,7 +7,7 @@ const createBlankProps = (): DefaultDirectoryDropdownProps => { return { defaultDirectoryId: "", directories: [], - onDefaultDirectoryChange: jest.fn() + onDefaultDirectoryChange: jest.fn(), }; }; @@ -17,7 +17,7 @@ const createBlankDirectory = (): Tenant => { displayName: "", domains: [], id: "", - tenantId: "" + tenantId: "", }; }; @@ -90,27 +90,15 @@ describe("test function", () => { const wrapper = mount(); - wrapper - .find("div.defaultDirectoryDropdown") - .find("div.ms-Dropdown") - .simulate("click"); + wrapper.find("div.defaultDirectoryDropdown").find("div.ms-Dropdown").simulate("click"); expect(wrapper.exists("div.ms-Callout-main")).toBe(true); - wrapper - .find("button.ms-Dropdown-item") - .at(1) - .simulate("click"); + wrapper.find("button.ms-Dropdown-item").at(1).simulate("click"); expect(props.onDefaultDirectoryChange).toBeCalled(); expect(props.onDefaultDirectoryChange).toHaveBeenCalled(); - wrapper - .find("div.defaultDirectoryDropdown") - .find("div.ms-Dropdown") - .simulate("click"); + wrapper.find("div.defaultDirectoryDropdown").find("div.ms-Dropdown").simulate("click"); expect(wrapper.exists("div.ms-Callout-main")).toBe(true); - wrapper - .find("button.ms-Dropdown-item") - .at(0) - .simulate("click"); + wrapper.find("button.ms-Dropdown-item").at(0).simulate("click"); expect(props.onDefaultDirectoryChange).toBeCalled(); expect(props.onDefaultDirectoryChange).toHaveBeenCalled(); }); diff --git a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx b/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx index cd17aac9f..0511efdd4 100644 --- a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx +++ b/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx @@ -19,13 +19,13 @@ export class DefaultDirectoryDropdownComponent extends React.Component = this.props.directories.map( (dirc): IDropdownOption => { return { key: dirc.tenantId, - text: `${dirc.displayName}(${dirc.tenantId})` + text: `${dirc.displayName}(${dirc.tenantId})`, }; } ); @@ -35,7 +35,7 @@ export class DefaultDirectoryDropdownComponent extends React.Component; @@ -56,12 +56,12 @@ export class DefaultDirectoryDropdownComponent extends React.Component d.tenantId === option.key); + const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === option.key); if (!selectedDirectory) { return; } diff --git a/src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx b/src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx index 21280e9ac..2ca392ff9 100644 --- a/src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx +++ b/src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx @@ -7,7 +7,7 @@ const createBlankProps = (): DirectoryListProps => { return { selectedDirectoryId: undefined, directories: [], - onNewDirectorySelected: jest.fn() + onNewDirectorySelected: jest.fn(), }; }; @@ -17,7 +17,7 @@ const createBlankDirectory = (): Tenant => { displayName: undefined, domains: [], id: undefined, - tenantId: undefined + tenantId: undefined, }; }; diff --git a/src/Explorer/Controls/Directory/DirectoryListComponent.tsx b/src/Explorer/Controls/Directory/DirectoryListComponent.tsx index 633e16f60..d7356d541 100644 --- a/src/Explorer/Controls/Directory/DirectoryListComponent.tsx +++ b/src/Explorer/Controls/Directory/DirectoryListComponent.tsx @@ -28,7 +28,7 @@ export class DirectoryListComponent extends React.Component + (directory) => directory.displayName && directory.displayName.toLowerCase().indexOf(filterText && filterText.toLowerCase()) >= 0 ) : originalItems; - const filteredItemsSelected = filteredItems.map(t => { + const filteredItemsSelected = filteredItems.map((t) => { let tenant: ListTenant = t; tenant.selected = t.tenantId === selectedDirectoryId; return tenant; @@ -53,7 +53,7 @@ export class DirectoryListComponent extends React.Component, text?: string): void => { this.setState({ - filterText: text + filterText: text, }); }; @@ -84,19 +84,19 @@ export class DirectoryListComponent extends React.Component d.tenantId === selectedDirectoryId); + const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === selectedDirectoryId); this.props.onNewDirectorySelected(selectedDirectory); }; diff --git a/src/Explorer/Controls/DynamicList/DynamicList.test.ts b/src/Explorer/Controls/DynamicList/DynamicList.test.ts index ac190f6e5..22400de08 100644 --- a/src/Explorer/Controls/DynamicList/DynamicList.test.ts +++ b/src/Explorer/Controls/DynamicList/DynamicList.test.ts @@ -25,7 +25,7 @@ describe("Dynamic List Component", () => { placeholder: placeholder, listItems: items, buttonText: mockButton, - ariaLabel: mockAriaLabel + ariaLabel: mockAriaLabel, }; } diff --git a/src/Explorer/Controls/DynamicList/DynamicListComponent.ts b/src/Explorer/Controls/DynamicList/DynamicListComponent.ts index cc699b4a0..4407700d7 100644 --- a/src/Explorer/Controls/DynamicList/DynamicListComponent.ts +++ b/src/Explorer/Controls/DynamicList/DynamicListComponent.ts @@ -113,5 +113,5 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel { */ export const DynamicListComponent = { viewModel: DynamicListViewModel, - template + template, }; diff --git a/src/Explorer/Controls/Editor/EditorComponent.ts b/src/Explorer/Controls/Editor/EditorComponent.ts index ac24189a1..5f77c0a52 100644 --- a/src/Explorer/Controls/Editor/EditorComponent.ts +++ b/src/Explorer/Controls/Editor/EditorComponent.ts @@ -1,63 +1,63 @@ -import { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent"; -import template from "./editor-component.html"; -import * as monaco from "monaco-editor"; -import { SqlCompletionItemProvider, ErrorMarkProvider } from "@azure/cosmos-language-service"; - -/** - * Helper class for ko component registration - */ -export class EditorComponent { - constructor() { - return { - viewModel: EditorViewModel, - template - }; - } -} - -/** - * Parameters for this component - */ -export interface EditorParams extends JsonEditorParams { - contentType: string; -} - -/** - * This is a generic editor component that builds on top of the pre-existing JsonEditorComponent. - */ -// TODO: Ideally, JsonEditorViewModel should extend EditorViewModel and not the other way around -class EditorViewModel extends JsonEditorViewModel { - public params: EditorParams; - private static providerRegistered: string[] = []; - - public constructor(params: EditorParams) { - super(params); - this.params = params; - super.createEditor.bind(this); - - /** - * setTimeout is needed as creating the edtior manipulates the dom directly and expects - * Knockout to have completed all of the initial bindings for the component - */ - this.params.content() != null && - setTimeout(() => { - this.createEditor(this.params.content(), this.configureEditor.bind(this)); - }); - } - - protected getEditorLanguage(): string { - return this.params.contentType; - } - - protected registerCompletionItemProvider() { - let sqlCompletionItemProvider = new SqlCompletionItemProvider(); - if (EditorViewModel.providerRegistered.indexOf("sql") < 0) { - monaco.languages.registerCompletionItemProvider("sql", sqlCompletionItemProvider); - EditorViewModel.providerRegistered.push("sql"); - } - } - - protected getErrorMarkers(input: string): Q.Promise { - return ErrorMarkProvider.getErrorMark(input); - } -} +import { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent"; +import template from "./editor-component.html"; +import * as monaco from "monaco-editor"; +import { SqlCompletionItemProvider, ErrorMarkProvider } from "@azure/cosmos-language-service"; + +/** + * Helper class for ko component registration + */ +export class EditorComponent { + constructor() { + return { + viewModel: EditorViewModel, + template, + }; + } +} + +/** + * Parameters for this component + */ +export interface EditorParams extends JsonEditorParams { + contentType: string; +} + +/** + * This is a generic editor component that builds on top of the pre-existing JsonEditorComponent. + */ +// TODO: Ideally, JsonEditorViewModel should extend EditorViewModel and not the other way around +class EditorViewModel extends JsonEditorViewModel { + public params: EditorParams; + private static providerRegistered: string[] = []; + + public constructor(params: EditorParams) { + super(params); + this.params = params; + super.createEditor.bind(this); + + /** + * setTimeout is needed as creating the edtior manipulates the dom directly and expects + * Knockout to have completed all of the initial bindings for the component + */ + this.params.content() != null && + setTimeout(() => { + this.createEditor(this.params.content(), this.configureEditor.bind(this)); + }); + } + + protected getEditorLanguage(): string { + return this.params.contentType; + } + + protected registerCompletionItemProvider() { + let sqlCompletionItemProvider = new SqlCompletionItemProvider(); + if (EditorViewModel.providerRegistered.indexOf("sql") < 0) { + monaco.languages.registerCompletionItemProvider("sql", sqlCompletionItemProvider); + EditorViewModel.providerRegistered.push("sql"); + } + } + + protected getErrorMarkers(input: string): Q.Promise { + return ErrorMarkProvider.getErrorMark(input); + } +} diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 23c47960d..49cf2aca2 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -70,7 +70,7 @@ export class EditorReact extends React.Component { fontSize: 12, ariaLabel: this.props.ariaLabel, theme: this.props.theme, - automaticLayout: true + automaticLayout: true, }; this.rootNode.innerHTML = ""; diff --git a/src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts b/src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts index 7be570f6d..0d54838f6 100644 --- a/src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts +++ b/src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts @@ -1,27 +1,27 @@ -import template from "./error-display-component.html"; - -/** - * Helper class for ko component registration - * This component displays an error as designed in: - * https://microsoft.sharepoint.com/teams/DPX/Modern/DocDB/_layouts/15/WopiFrame.aspx?sourcedoc={66864d4a-f925-4cbe-9eb4-79f8d191a115}&action=edit&wd=target%28DocumentDB%20emulator%2Eone%7CE617D0A7-F77C-4968-B75A-1451049F4FEA%2FError%20notification%7CAA1E4BC9-4D72-472C-B40C-2437FA217226%2F%29 - * TODO: support "More details" - */ -export class ErrorDisplayComponent { - constructor() { - return { - viewModel: ErrorDisplayViewModel, - template - }; - } -} - -/** - * Parameters for this component - */ -interface ErrorDisplayParams { - errorMsg: ko.Observable; // Primary message -} - -class ErrorDisplayViewModel { - public constructor(public params: ErrorDisplayParams) {} -} +import template from "./error-display-component.html"; + +/** + * Helper class for ko component registration + * This component displays an error as designed in: + * https://microsoft.sharepoint.com/teams/DPX/Modern/DocDB/_layouts/15/WopiFrame.aspx?sourcedoc={66864d4a-f925-4cbe-9eb4-79f8d191a115}&action=edit&wd=target%28DocumentDB%20emulator%2Eone%7CE617D0A7-F77C-4968-B75A-1451049F4FEA%2FError%20notification%7CAA1E4BC9-4D72-472C-B40C-2437FA217226%2F%29 + * TODO: support "More details" + */ +export class ErrorDisplayComponent { + constructor() { + return { + viewModel: ErrorDisplayViewModel, + template, + }; + } +} + +/** + * Parameters for this component + */ +interface ErrorDisplayParams { + errorMsg: ko.Observable; // Primary message +} + +class ErrorDisplayViewModel { + public constructor(public params: ErrorDisplayParams) {} +} diff --git a/src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html b/src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html index 6f64b470d..ca4b301ce 100644 --- a/src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html +++ b/src/Explorer/Controls/ErrorDisplayComponent/error-display-component.html @@ -1,6 +1,6 @@ -
-
- Error - -
-
+
+
+ Error + +
+
diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx index 41f4ff001..2a1c38117 100644 --- a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx @@ -15,23 +15,23 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { const baseUrlOptions = [ { key: "https://localhost:1234/explorer.html", text: "localhost:1234" }, { key: "https://cosmos.azure.com/explorer.html", text: "cosmos.azure.com" }, - { key: "https://portal.azure.com", text: "portal" } + { key: "https://portal.azure.com", text: "portal" }, ]; const platformOptions = [ { key: "Hosted", text: "Hosted" }, { key: "Portal", text: "Portal" }, { key: "Emulator", text: "Emulator" }, - { key: "", text: "None" } + { key: "", text: "None" }, ]; // React hooks to keep state const [baseUrl, setBaseUrl] = React.useState( - baseUrlOptions.find(o => o.key === window.location.origin + window.location.pathname) || baseUrlOptions[0] + baseUrlOptions.find((o) => o.key === window.location.origin + window.location.pathname) || baseUrlOptions[0] ); const [platform, setPlatform] = React.useState( urlParams.has("platform") - ? platformOptions.find(o => o.key === urlParams.get("platform")) || platformOptions[0] + ? platformOptions.find((o) => o.key === urlParams.get("platform")) || platformOptions[0] : platformOptions[0] ); @@ -52,13 +52,13 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { { key: "feature.enableLinkInjection", label: "Enable Injecting Notebook Viewer Link into the first cell", - value: "true" + value: "true", }, { key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" }, { key: "feature.enablefixedcollectionwithsharedthroughput", label: "Enable fixed collection with shared throughput", - value: "true" + value: "true", }, { key: "feature.ttl90days", label: "TTL 90 days", value: "true" }, { key: "feature.enablenotebooks", label: "Enable notebooks", value: "true" }, @@ -66,10 +66,10 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { key: "feature.customportal", label: "Force Production portal (portal only)", value: "false", - disabled: (): boolean => baseUrl.key !== "https://portal.azure.com" + disabled: (): boolean => baseUrl.key !== "https://portal.azure.com", }, { key: "feature.enablespark", label: "Enable Synapse", value: "true" }, - { key: "feature.enableautopilotv2", label: "Enable Auto-pilot V2", value: "true" } + { key: "feature.enableautopilotv2", label: "Enable Auto-pilot V2", value: "true" }, ]; const stringFeatures: { @@ -88,23 +88,23 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { key: "dataExplorerSource", label: "Data Explorer Source (portal only)", placeholder: "https://localhost:1234/explorer.html", - disabled: (): boolean => baseUrl.key !== "https://portal.azure.com" + disabled: (): boolean => baseUrl.key !== "https://portal.azure.com", }, - { key: "feature.livyendpoint", label: "Livy endpoint", placeholder: "" } + { key: "feature.livyendpoint", label: "Livy endpoint", placeholder: "" }, ]; booleanFeatures.forEach( - f => (f.reactState = React.useState(urlParams.has(f.key) ? urlParams.get(f.key) === "true" : false)) + (f) => (f.reactState = React.useState(urlParams.has(f.key) ? urlParams.get(f.key) === "true" : false)) ); stringFeatures.forEach( - f => (f.reactState = React.useState(urlParams.has(f.key) ? urlParams.get(f.key) : undefined)) + (f) => (f.reactState = React.useState(urlParams.has(f.key) ? urlParams.get(f.key) : undefined)) ); const buildUrl = (): string => { const fragments = (platform.key === "" ? [] : [`platform=${platform.key}`]) - .concat(booleanFeatures.map(f => (f.reactState[0] ? `${f.key}=${f.value}` : ""))) - .concat(stringFeatures.map(f => (f.reactState[0] ? `${f.key}=${encodeURIComponent(f.reactState[0])}` : ""))) - .filter(v => v && v.length > 0); + .concat(booleanFeatures.map((f) => (f.reactState[0] ? `${f.key}=${f.value}` : ""))) + .concat(stringFeatures.map((f) => (f.reactState[0] ? `${f.key}=${encodeURIComponent(f.reactState[0])}` : ""))) + .filter((v) => v && v.length > 0); const paramString = fragments.length < 1 ? "" : `?${fragments.join("&")}`; return `${baseUrl.key}${paramString}`; @@ -119,38 +119,38 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { }; booleanFeatures.forEach( - f => + (f) => (f.onChange = (ev?: React.FormEvent, checked?: boolean): void => { f.reactState[1](checked); }) ); stringFeatures.forEach( - f => + (f) => (f.onChange = (event: React.FormEvent, newValue?: string): void => { f.reactState[1](newValue); }) ); const onNotebookShortcut = (): void => { - booleanFeatures.find(f => f.key === "feature.enablenotebooks").reactState[1](true); + booleanFeatures.find((f) => f.key === "feature.enablenotebooks").reactState[1](true); stringFeatures - .find(f => f.key === "feature.notebookserverurl") + .find((f) => f.key === "feature.notebookserverurl") .reactState[1]("https://localhost:10001/12345/notebook/"); - stringFeatures.find(f => f.key === "feature.notebookservertoken").reactState[1]("token"); - stringFeatures.find(f => f.key === "feature.notebookbasepath").reactState[1]("./notebooks"); - setPlatform(platformOptions.find(o => o.key === "Hosted")); + stringFeatures.find((f) => f.key === "feature.notebookservertoken").reactState[1]("token"); + stringFeatures.find((f) => f.key === "feature.notebookbasepath").reactState[1]("./notebooks"); + setPlatform(platformOptions.find((o) => o.key === "Hosted")); }; const onPortalLocalDEShortcut = (): void => { - setBaseUrl(baseUrlOptions.find(o => o.key === "https://portal.azure.com")); - setPlatform(platformOptions.find(o => o.key === "Portal")); - stringFeatures.find(f => f.key === "dataExplorerSource").reactState[1]("https://localhost:1234/explorer.html"); + setBaseUrl(baseUrlOptions.find((o) => o.key === "https://portal.azure.com")); + setPlatform(platformOptions.find((o) => o.key === "Portal")); + stringFeatures.find((f) => f.key === "dataExplorerSource").reactState[1]("https://localhost:1234/explorer.html"); }; const onReset = (): void => { - booleanFeatures.forEach(f => f.reactState[1](false)); - stringFeatures.forEach(f => f.reactState[1]("")); + booleanFeatures.forEach((f) => f.reactState[1](false)); + stringFeatures.forEach((f) => f.reactState[1]("")); }; const stackTokens = { childrenGap: 10 }; @@ -169,7 +169,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { const anchorOptions = { href: buildUrl(), target: "_blank", - rel: "noopener" + rel: "noopener", }; return ( @@ -201,7 +201,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => { - {leftBooleanFeatures.map(f => ( + {leftBooleanFeatures.map((f) => ( { ))} - {rightBooleanFeatures.map(f => ( + {rightBooleanFeatures.map((f) => ( { - {leftStringFeatures.map(f => ( + {leftStringFeatures.map((f) => ( { ))} - {rightStringFeatures.map(f => ( + {rightStringFeatures.map((f) => ( container: { display: "flex", flexFlow: "column nowrap", - alignItems: "stretch" + alignItems: "stretch", }, header: [ // tslint:disable-next-line:deprecation @@ -32,16 +32,16 @@ export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element => display: "flex", alignItems: "center", fontWeight: FontWeights.semibold, - padding: "12px 12px 14px 24px" - } + padding: "12px 12px 14px 24px", + }, ], body: { flex: "4 4 auto", overflowY: "hidden", marginBottom: 40, height: "100%", - display: "flex" - } + display: "flex", + }, }); const iconButtonStyles = { @@ -49,11 +49,11 @@ export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element => color: theme.palette.neutralPrimary, marginLeft: "auto", marginTop: "4px", - marginRight: "2px" + marginRight: "2px", }, rootHovered: { - color: theme.palette.neutralDark - } + color: theme.palette.neutralDark, + }, }; const cancelIcon: IIconProps = { iconName: "Cancel" }; const hideModal = (): void => showModal(false); diff --git a/src/Explorer/Controls/GitHub/AddRepoComponent.tsx b/src/Explorer/Controls/GitHub/AddRepoComponent.tsx index 1352a687c..5a44a3651 100644 --- a/src/Explorer/Controls/GitHub/AddRepoComponent.tsx +++ b/src/Explorer/Controls/GitHub/AddRepoComponent.tsx @@ -1,132 +1,132 @@ -import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "office-ui-fabric-react"; -import * as React from "react"; -import * as Constants from "../../../Common/Constants"; -import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; -import { RepoListItem } from "./GitHubReposComponent"; -import { ChildrenMargin } from "./GitHubStyleConstants"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import { IGitHubRepo } from "../../../GitHub/GitHubClient"; -import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import UrlUtility from "../../../Common/UrlUtility"; -import Explorer from "../../Explorer"; - -export interface AddRepoComponentProps { - container: Explorer; - getRepo: (owner: string, repo: string) => Promise; - pinRepo: (item: RepoListItem) => void; -} - -interface AddRepoComponentState { - textFieldValue: string; - textFieldErrorMessage: string; -} - -export class AddRepoComponent extends React.Component { - private static readonly DescriptionText = - "Don't see what you're looking for? Add your repo/branch, or any public repo (read-access only) by entering the URL: "; - private static readonly ButtonText = "Add"; - private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch"; - private static readonly TextFieldErrorMessage = "Invalid url"; - private static readonly DefaultBranchName = "master"; - - constructor(props: AddRepoComponentProps) { - super(props); - - this.state = { - textFieldValue: "", - textFieldErrorMessage: undefined - }; - } - - public render(): JSX.Element { - const textFieldProps: ITextFieldProps = { - placeholder: AddRepoComponent.TextFieldPlaceholder, - autoFocus: true, - value: this.state.textFieldValue, - errorMessage: this.state.textFieldErrorMessage, - onChange: this.onTextFieldChange - }; - - const buttonProps: IButtonProps = { - text: AddRepoComponent.ButtonText, - ariaLabel: AddRepoComponent.ButtonText, - onClick: this.onAddRepoButtonClick - }; - - return ( - <> -

{AddRepoComponent.DescriptionText}

- - - - ); - } - - private onTextFieldChange = ( - event: React.FormEvent, - newValue?: string - ): void => { - this.setState({ - textFieldValue: newValue || "", - textFieldErrorMessage: undefined - }); - }; - - private onAddRepoButtonClick = async (): Promise => { - const startKey: number = TelemetryProcessor.traceStart(Action.NotebooksGitHubManualRepoAdd, { - databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name, - defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }); - let enteredUrl = this.state.textFieldValue; - if (enteredUrl.indexOf("/tree/") === -1) { - enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`); - } - - const repoInfo = GitHubUtils.fromRepoUri(enteredUrl); - if (repoInfo) { - this.setState({ - textFieldValue: "", - textFieldErrorMessage: undefined - }); - - const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo); - if (repo) { - const item: RepoListItem = { - key: GitHubUtils.toRepoFullName(repo.owner, repo.name), - repo, - branches: [ - { - name: repoInfo.branch - } - ] - }; - - TelemetryProcessor.traceSuccess( - Action.NotebooksGitHubManualRepoAdd, - { - databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name, - defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }, - startKey - ); - return this.props.pinRepo(item); - } - } - - this.setState({ - textFieldErrorMessage: AddRepoComponent.TextFieldErrorMessage - }); - TelemetryProcessor.traceFailure( - Action.NotebooksGitHubManualRepoAdd, - { - databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name, - defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook, - error: AddRepoComponent.TextFieldErrorMessage - }, - startKey - ); - }; -} +import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "office-ui-fabric-react"; +import * as React from "react"; +import * as Constants from "../../../Common/Constants"; +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import { RepoListItem } from "./GitHubReposComponent"; +import { ChildrenMargin } from "./GitHubStyleConstants"; +import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { IGitHubRepo } from "../../../GitHub/GitHubClient"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import UrlUtility from "../../../Common/UrlUtility"; +import Explorer from "../../Explorer"; + +export interface AddRepoComponentProps { + container: Explorer; + getRepo: (owner: string, repo: string) => Promise; + pinRepo: (item: RepoListItem) => void; +} + +interface AddRepoComponentState { + textFieldValue: string; + textFieldErrorMessage: string; +} + +export class AddRepoComponent extends React.Component { + private static readonly DescriptionText = + "Don't see what you're looking for? Add your repo/branch, or any public repo (read-access only) by entering the URL: "; + private static readonly ButtonText = "Add"; + private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch"; + private static readonly TextFieldErrorMessage = "Invalid url"; + private static readonly DefaultBranchName = "master"; + + constructor(props: AddRepoComponentProps) { + super(props); + + this.state = { + textFieldValue: "", + textFieldErrorMessage: undefined, + }; + } + + public render(): JSX.Element { + const textFieldProps: ITextFieldProps = { + placeholder: AddRepoComponent.TextFieldPlaceholder, + autoFocus: true, + value: this.state.textFieldValue, + errorMessage: this.state.textFieldErrorMessage, + onChange: this.onTextFieldChange, + }; + + const buttonProps: IButtonProps = { + text: AddRepoComponent.ButtonText, + ariaLabel: AddRepoComponent.ButtonText, + onClick: this.onAddRepoButtonClick, + }; + + return ( + <> +

{AddRepoComponent.DescriptionText}

+ + + + ); + } + + private onTextFieldChange = ( + event: React.FormEvent, + newValue?: string + ): void => { + this.setState({ + textFieldValue: newValue || "", + textFieldErrorMessage: undefined, + }); + }; + + private onAddRepoButtonClick = async (): Promise => { + const startKey: number = TelemetryProcessor.traceStart(Action.NotebooksGitHubManualRepoAdd, { + databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name, + defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + }); + let enteredUrl = this.state.textFieldValue; + if (enteredUrl.indexOf("/tree/") === -1) { + enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`); + } + + const repoInfo = GitHubUtils.fromRepoUri(enteredUrl); + if (repoInfo) { + this.setState({ + textFieldValue: "", + textFieldErrorMessage: undefined, + }); + + const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo); + if (repo) { + const item: RepoListItem = { + key: GitHubUtils.toRepoFullName(repo.owner, repo.name), + repo, + branches: [ + { + name: repoInfo.branch, + }, + ], + }; + + TelemetryProcessor.traceSuccess( + Action.NotebooksGitHubManualRepoAdd, + { + databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name, + defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + }, + startKey + ); + return this.props.pinRepo(item); + } + } + + this.setState({ + textFieldErrorMessage: AddRepoComponent.TextFieldErrorMessage, + }); + TelemetryProcessor.traceFailure( + Action.NotebooksGitHubManualRepoAdd, + { + databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name, + defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + error: AddRepoComponent.TextFieldErrorMessage, + }, + startKey + ); + }; +} diff --git a/src/Explorer/Controls/GitHub/AuthorizeAccessComponent.tsx b/src/Explorer/Controls/GitHub/AuthorizeAccessComponent.tsx index acf35cc5e..66e62d0c6 100644 --- a/src/Explorer/Controls/GitHub/AuthorizeAccessComponent.tsx +++ b/src/Explorer/Controls/GitHub/AuthorizeAccessComponent.tsx @@ -1,91 +1,91 @@ -import { - ChoiceGroup, - IButtonProps, - IChoiceGroupProps, - PrimaryButton, - IChoiceGroupOption -} from "office-ui-fabric-react"; -import * as React from "react"; -import { ChildrenMargin } from "./GitHubStyleConstants"; - -export interface AuthorizeAccessComponentProps { - scope: string; - authorizeAccess: (scope: string) => void; -} - -export interface AuthorizeAccessComponentState { - scope: string; -} - -export class AuthorizeAccessComponent extends React.Component< - AuthorizeAccessComponentProps, - AuthorizeAccessComponentState -> { - // Scopes supported by GitHub OAuth. We're only interested in ones which allow us access to repos. - // https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ - public static readonly Scopes = { - Public: { - key: "public_repo", - text: "Public repos only" - }, - PublicAndPrivate: { - key: "repo", - text: "Public and private repos" - } - }; - - private static readonly DescriptionPara1 = - "Connect your notebooks workspace to GitHub. You'll be able to view, edit, and run notebooks stored in your GitHub repositories in Data Explorer."; - private static readonly DescriptionPara2 = - "Complete setup by authorizing Azure Cosmos DB to access the repositories in your GitHub account: "; - private static readonly AuthorizeButtonText = "Authorize access"; - - private onChoiceGroupChange = (event: React.SyntheticEvent, option: IChoiceGroupOption): void => - this.setState({ - scope: option.key - }); - - private onButtonClick = (): void => this.props.authorizeAccess(this.state.scope); - - constructor(props: AuthorizeAccessComponentProps) { - super(props); - - this.state = { - scope: this.props.scope - }; - } - - public render(): JSX.Element { - const choiceGroupProps: IChoiceGroupProps = { - options: [ - { - key: AuthorizeAccessComponent.Scopes.Public.key, - text: AuthorizeAccessComponent.Scopes.Public.text, - ariaLabel: AuthorizeAccessComponent.Scopes.Public.text - }, - { - key: AuthorizeAccessComponent.Scopes.PublicAndPrivate.key, - text: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text, - ariaLabel: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text - } - ], - selectedKey: this.state.scope, - onChange: this.onChoiceGroupChange - }; - - const buttonProps: IButtonProps = { - text: AuthorizeAccessComponent.AuthorizeButtonText, - ariaLabel: AuthorizeAccessComponent.AuthorizeButtonText, - onClick: this.onButtonClick - }; - - return ( - <> -

{AuthorizeAccessComponent.DescriptionPara1}

-

{AuthorizeAccessComponent.DescriptionPara2}

- - - - ); - } -} +import { + ChoiceGroup, + IButtonProps, + IChoiceGroupProps, + PrimaryButton, + IChoiceGroupOption, +} from "office-ui-fabric-react"; +import * as React from "react"; +import { ChildrenMargin } from "./GitHubStyleConstants"; + +export interface AuthorizeAccessComponentProps { + scope: string; + authorizeAccess: (scope: string) => void; +} + +export interface AuthorizeAccessComponentState { + scope: string; +} + +export class AuthorizeAccessComponent extends React.Component< + AuthorizeAccessComponentProps, + AuthorizeAccessComponentState +> { + // Scopes supported by GitHub OAuth. We're only interested in ones which allow us access to repos. + // https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ + public static readonly Scopes = { + Public: { + key: "public_repo", + text: "Public repos only", + }, + PublicAndPrivate: { + key: "repo", + text: "Public and private repos", + }, + }; + + private static readonly DescriptionPara1 = + "Connect your notebooks workspace to GitHub. You'll be able to view, edit, and run notebooks stored in your GitHub repositories in Data Explorer."; + private static readonly DescriptionPara2 = + "Complete setup by authorizing Azure Cosmos DB to access the repositories in your GitHub account: "; + private static readonly AuthorizeButtonText = "Authorize access"; + + private onChoiceGroupChange = (event: React.SyntheticEvent, option: IChoiceGroupOption): void => + this.setState({ + scope: option.key, + }); + + private onButtonClick = (): void => this.props.authorizeAccess(this.state.scope); + + constructor(props: AuthorizeAccessComponentProps) { + super(props); + + this.state = { + scope: this.props.scope, + }; + } + + public render(): JSX.Element { + const choiceGroupProps: IChoiceGroupProps = { + options: [ + { + key: AuthorizeAccessComponent.Scopes.Public.key, + text: AuthorizeAccessComponent.Scopes.Public.text, + ariaLabel: AuthorizeAccessComponent.Scopes.Public.text, + }, + { + key: AuthorizeAccessComponent.Scopes.PublicAndPrivate.key, + text: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text, + ariaLabel: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text, + }, + ], + selectedKey: this.state.scope, + onChange: this.onChoiceGroupChange, + }; + + const buttonProps: IButtonProps = { + text: AuthorizeAccessComponent.AuthorizeButtonText, + ariaLabel: AuthorizeAccessComponent.AuthorizeButtonText, + onClick: this.onButtonClick, + }; + + return ( + <> +

{AuthorizeAccessComponent.DescriptionPara1}

+

{AuthorizeAccessComponent.DescriptionPara2}

+ + + + ); + } +} diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx index bbd770976..7a87f4246 100644 --- a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx +++ b/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx @@ -1,87 +1,87 @@ -import { DefaultButton, IButtonProps, Link, PrimaryButton } from "office-ui-fabric-react"; -import * as React from "react"; -import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient"; -import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent"; -import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent"; -import { ButtonsFooterStyle, ChildrenMargin, ContentFooterStyle } from "./GitHubStyleConstants"; -import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent"; - -export interface GitHubReposComponentProps { - showAuthorizeAccess: boolean; - authorizeAccessProps: AuthorizeAccessComponentProps; - reposListProps: ReposListComponentProps; - addRepoProps: AddRepoComponentProps; - resetConnection: () => void; - onOkClick: () => void; - onCancelClick: () => void; -} - -export interface RepoListItem { - key: string; - repo: IGitHubRepo; - branches: IGitHubBranch[]; -} - -export class GitHubReposComponent extends React.Component { - public static readonly ConnectToGitHubTitle = "Connect to GitHub"; - public static readonly ManageGitHubRepoTitle = "Manage GitHub settings"; - private static readonly ManageGitHubRepoDescription = - "Select your GitHub repos and branch(es) to pin to your notebooks workspace."; - private static readonly ManageGitHubRepoResetConnection = "View or change your GitHub authorization settings."; - private static readonly OKButtonText = "OK"; - private static readonly CancelButtonText = "Cancel"; - - public render(): JSX.Element { - const header: JSX.Element = ( -

- {this.props.showAuthorizeAccess - ? GitHubReposComponent.ConnectToGitHubTitle - : GitHubReposComponent.ManageGitHubRepoTitle} -

- ); - - const content: JSX.Element = this.props.showAuthorizeAccess ? ( - - ) : ( - <> -

{GitHubReposComponent.ManageGitHubRepoDescription}

- - {GitHubReposComponent.ManageGitHubRepoResetConnection} - - - - ); - - const okProps: IButtonProps = { - text: GitHubReposComponent.OKButtonText, - ariaLabel: GitHubReposComponent.OKButtonText, - onClick: this.props.onOkClick - }; - - const cancelProps: IButtonProps = { - text: GitHubReposComponent.CancelButtonText, - ariaLabel: GitHubReposComponent.CancelButtonText, - onClick: this.props.onCancelClick - }; - - return ( - <> -
- {header} -
-
{content}
- {!this.props.showAuthorizeAccess && ( - <> -
- -
-
- - -
- - )} - - ); - } -} +import { DefaultButton, IButtonProps, Link, PrimaryButton } from "office-ui-fabric-react"; +import * as React from "react"; +import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient"; +import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent"; +import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent"; +import { ButtonsFooterStyle, ChildrenMargin, ContentFooterStyle } from "./GitHubStyleConstants"; +import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent"; + +export interface GitHubReposComponentProps { + showAuthorizeAccess: boolean; + authorizeAccessProps: AuthorizeAccessComponentProps; + reposListProps: ReposListComponentProps; + addRepoProps: AddRepoComponentProps; + resetConnection: () => void; + onOkClick: () => void; + onCancelClick: () => void; +} + +export interface RepoListItem { + key: string; + repo: IGitHubRepo; + branches: IGitHubBranch[]; +} + +export class GitHubReposComponent extends React.Component { + public static readonly ConnectToGitHubTitle = "Connect to GitHub"; + public static readonly ManageGitHubRepoTitle = "Manage GitHub settings"; + private static readonly ManageGitHubRepoDescription = + "Select your GitHub repos and branch(es) to pin to your notebooks workspace."; + private static readonly ManageGitHubRepoResetConnection = "View or change your GitHub authorization settings."; + private static readonly OKButtonText = "OK"; + private static readonly CancelButtonText = "Cancel"; + + public render(): JSX.Element { + const header: JSX.Element = ( +

+ {this.props.showAuthorizeAccess + ? GitHubReposComponent.ConnectToGitHubTitle + : GitHubReposComponent.ManageGitHubRepoTitle} +

+ ); + + const content: JSX.Element = this.props.showAuthorizeAccess ? ( + + ) : ( + <> +

{GitHubReposComponent.ManageGitHubRepoDescription}

+ + {GitHubReposComponent.ManageGitHubRepoResetConnection} + + + + ); + + const okProps: IButtonProps = { + text: GitHubReposComponent.OKButtonText, + ariaLabel: GitHubReposComponent.OKButtonText, + onClick: this.props.onOkClick, + }; + + const cancelProps: IButtonProps = { + text: GitHubReposComponent.CancelButtonText, + ariaLabel: GitHubReposComponent.CancelButtonText, + onClick: this.props.onCancelClick, + }; + + return ( + <> +
+ {header} +
+
{content}
+ {!this.props.showAuthorizeAccess && ( + <> +
+ +
+
+ + +
+ + )} + + ); + } +} diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx index 362b8fe98..aa8cabde5 100644 --- a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx +++ b/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx @@ -1,20 +1,20 @@ -import ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent"; - -export class GitHubReposComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - constructor(private props: GitHubReposComponentProps) { - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - return ; - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} +import ko from "knockout"; +import * as React from "react"; +import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; +import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent"; + +export class GitHubReposComponentAdapter implements ReactAdapter { + public parameters: ko.Observable; + + constructor(private props: GitHubReposComponentProps) { + this.parameters = ko.observable(Date.now()); + } + + public renderComponent(): JSX.Element { + return ; + } + + public triggerRender(): void { + window.requestAnimationFrame(() => this.parameters(Date.now())); + } +} diff --git a/src/Explorer/Controls/GitHub/GitHubStyleConstants.ts b/src/Explorer/Controls/GitHub/GitHubStyleConstants.ts index c547eac9b..25866271d 100644 --- a/src/Explorer/Controls/GitHub/GitHubStyleConstants.ts +++ b/src/Explorer/Controls/GitHub/GitHubStyleConstants.ts @@ -1,58 +1,58 @@ -import { - IStyleFunctionOrObject, - ICheckboxStyleProps, - ICheckboxStyles, - IDropdownStyles, - IDropdownStyleProps -} from "office-ui-fabric-react"; - -export const ButtonsFooterStyle: React.CSSProperties = { - padding: 14, - height: "auto" -}; - -export const ContentFooterStyle: React.CSSProperties = { - padding: "10px 24px 10px 24px", - height: "auto" -}; - -export const ChildrenMargin = 10; -export const FontSize = 12; - -export const ReposListCheckboxStyles: IStyleFunctionOrObject = { - label: { - margin: 0, - padding: "2 0 2 0" - }, - text: { - fontSize: FontSize - } -}; - -export const BranchesDropdownCheckboxStyles: IStyleFunctionOrObject = { - label: { - margin: 0, - padding: 0, - fontSize: FontSize - }, - root: { - padding: 0 - }, - text: { - fontSize: FontSize - } -}; - -export const BranchesDropdownStyles: IStyleFunctionOrObject = { - title: { - fontSize: FontSize - } -}; - -export const BranchesDropdownOptionContainerStyle: React.CSSProperties = { - padding: 8 -}; - -export const ReposListRepoColumnMinWidth = 192; -export const ReposListBranchesColumnWidth = 116; -export const BranchesDropdownWidth = 200; +import { + IStyleFunctionOrObject, + ICheckboxStyleProps, + ICheckboxStyles, + IDropdownStyles, + IDropdownStyleProps, +} from "office-ui-fabric-react"; + +export const ButtonsFooterStyle: React.CSSProperties = { + padding: 14, + height: "auto", +}; + +export const ContentFooterStyle: React.CSSProperties = { + padding: "10px 24px 10px 24px", + height: "auto", +}; + +export const ChildrenMargin = 10; +export const FontSize = 12; + +export const ReposListCheckboxStyles: IStyleFunctionOrObject = { + label: { + margin: 0, + padding: "2 0 2 0", + }, + text: { + fontSize: FontSize, + }, +}; + +export const BranchesDropdownCheckboxStyles: IStyleFunctionOrObject = { + label: { + margin: 0, + padding: 0, + fontSize: FontSize, + }, + root: { + padding: 0, + }, + text: { + fontSize: FontSize, + }, +}; + +export const BranchesDropdownStyles: IStyleFunctionOrObject = { + title: { + fontSize: FontSize, + }, +}; + +export const BranchesDropdownOptionContainerStyle: React.CSSProperties = { + padding: 8, +}; + +export const ReposListRepoColumnMinWidth = 192; +export const ReposListBranchesColumnWidth = 116; +export const BranchesDropdownWidth = 200; diff --git a/src/Explorer/Controls/GitHub/ReposListComponent.tsx b/src/Explorer/Controls/GitHub/ReposListComponent.tsx index e7bcb0b51..b63c9150b 100644 --- a/src/Explorer/Controls/GitHub/ReposListComponent.tsx +++ b/src/Explorer/Controls/GitHub/ReposListComponent.tsx @@ -1,304 +1,304 @@ -import { - Checkbox, - DetailsList, - DetailsRow, - Dropdown, - ICheckboxProps, - IDetailsFooterProps, - IDetailsListProps, - IDetailsRowBaseProps, - IDropdown, - IDropdownOption, - IDropdownProps, - ILinkProps, - ISelectableDroppableTextProps, - Link, - ResponsiveMode, - SelectionMode, - Text -} from "office-ui-fabric-react"; -import * as React from "react"; -import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import { RepoListItem } from "./GitHubReposComponent"; -import { - BranchesDropdownCheckboxStyles, - BranchesDropdownOptionContainerStyle, - ReposListCheckboxStyles, - ReposListRepoColumnMinWidth, - ReposListBranchesColumnWidth, - BranchesDropdownWidth, - BranchesDropdownStyles -} from "./GitHubStyleConstants"; - -export interface ReposListComponentProps { - branchesProps: Record; // key'd by repo key - pinnedReposProps: PinnedReposProps; - unpinnedReposProps: UnpinnedReposProps; - pinRepo: (repo: RepoListItem) => void; - unpinRepo: (repo: RepoListItem) => void; -} - -export interface BranchesProps { - branches: IGitHubBranch[]; - lastPageInfo?: IGitHubPageInfo; - hasMore: boolean; - isLoading: boolean; - loadMore: () => void; -} - -export interface PinnedReposProps { - repos: RepoListItem[]; -} - -export interface UnpinnedReposProps { - repos: RepoListItem[]; - hasMore: boolean; - isLoading: boolean; - loadMore: () => void; -} - -export class ReposListComponent extends React.Component { - private static readonly PinnedReposColumnName = "Pinned repos"; - private static readonly UnpinnedReposColumnName = "Unpinned repos"; - private static readonly BranchesColumnName = "Branches"; - private static readonly LoadingText = "Loading..."; - private static readonly LoadMoreText = "Load more"; - private static readonly DefaultBranchName = "master"; - private static readonly FooterIndex = -1; - - public render(): JSX.Element { - const pinnedReposListProps: IDetailsListProps = { - styles: { - contentWrapper: { - height: this.props.pinnedReposProps.repos.length ? undefined : 0 - } - }, - items: this.props.pinnedReposProps.repos, - getKey: ReposListComponent.getKey, - selectionMode: SelectionMode.none, - compact: true, - columns: [ - { - key: ReposListComponent.PinnedReposColumnName, - name: ReposListComponent.PinnedReposColumnName, - ariaLabel: ReposListComponent.PinnedReposColumnName, - minWidth: ReposListRepoColumnMinWidth, - onRender: this.onRenderPinnedReposColumnItem - }, - { - key: ReposListComponent.BranchesColumnName, - name: ReposListComponent.BranchesColumnName, - ariaLabel: ReposListComponent.BranchesColumnName, - minWidth: ReposListBranchesColumnWidth, - maxWidth: ReposListBranchesColumnWidth, - onRender: this.onRenderPinnedReposBranchesColumnItem - } - ], - onRenderDetailsFooter: this.props.pinnedReposProps.repos.length ? undefined : this.onRenderReposFooter - }; - - const unpinnedReposListProps: IDetailsListProps = { - items: this.props.unpinnedReposProps.repos, - getKey: ReposListComponent.getKey, - selectionMode: SelectionMode.none, - compact: true, - columns: [ - { - key: ReposListComponent.UnpinnedReposColumnName, - name: ReposListComponent.UnpinnedReposColumnName, - ariaLabel: ReposListComponent.UnpinnedReposColumnName, - minWidth: ReposListRepoColumnMinWidth, - onRender: this.onRenderUnpinnedReposColumnItem - }, - { - key: ReposListComponent.BranchesColumnName, - name: ReposListComponent.BranchesColumnName, - ariaLabel: ReposListComponent.BranchesColumnName, - minWidth: ReposListBranchesColumnWidth, - maxWidth: ReposListBranchesColumnWidth, - onRender: this.onRenderUnpinnedReposBranchesColumnItem - } - ], - onRenderDetailsFooter: - this.props.unpinnedReposProps.isLoading || this.props.unpinnedReposProps.hasMore - ? this.onRenderReposFooter - : undefined - }; - - return ( - <> - - - - ); - } - - private onRenderPinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - return None; - } - - const checkboxProps: ICheckboxProps = { - ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)), - styles: ReposListCheckboxStyles, - defaultChecked: true, - onChange: () => this.props.unpinRepo(item) - }; - - return ; - }; - - private onRenderPinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - return <>; - } - - const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]; - const options: IDropdownOption[] = branchesProps.branches.map(branch => ({ - key: branch.name, - text: branch.name, - data: item, - disabled: item.branches.length === 1 && branch.name === item.branches[0].name, - selected: item.branches.findIndex(element => element.name === branch.name) !== -1 - })); - - if (branchesProps.hasMore || branchesProps.isLoading) { - const text = branchesProps.isLoading ? ReposListComponent.LoadingText : ReposListComponent.LoadMoreText; - options.push({ - key: text, - text, - data: item, - index: ReposListComponent.FooterIndex - }); - } - - const dropdownProps: IDropdownProps = { - styles: BranchesDropdownStyles, - dropdownWidth: BranchesDropdownWidth, - responsiveMode: ResponsiveMode.large, - options, - onRenderList: this.onRenderBranchesDropdownList - }; - - if (item.branches.length === 1) { - dropdownProps.placeholder = item.branches[0].name; - } else if (item.branches.length > 1) { - dropdownProps.placeholder = `${item.branches.length} branches`; - } - - return ; - }; - - private onRenderUnpinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - return <>; - } - - const dropdownProps: IDropdownProps = { - styles: BranchesDropdownStyles, - options: [], - placeholder: ReposListComponent.DefaultBranchName, - disabled: true - }; - - return ; - }; - - private onRenderBranchesDropdownList = ( - props: ISelectableDroppableTextProps - ): JSX.Element => { - const renderedList: JSX.Element[] = []; - props.options.forEach((option: IDropdownOption) => { - const item = ( -
- {this.onRenderPinnedReposBranchesDropdownOption(option)} -
- ); - renderedList.push(item); - }); - - return <>{renderedList}; - }; - - private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element { - const item: RepoListItem = option.data; - const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]; - - if (option.index === ReposListComponent.FooterIndex) { - const linkProps: ILinkProps = { - disabled: branchesProps.isLoading, - onClick: branchesProps.loadMore - }; - - return {option.text}; - } - - const checkboxProps: ICheckboxProps = { - ...ReposListComponent.getCheckboxPropsForLabel(option.text), - styles: BranchesDropdownCheckboxStyles, - defaultChecked: option.selected, - disabled: option.disabled, - onChange: (event, checked) => { - const repoListItem = { ...item }; - const branch: IGitHubBranch = { name: option.text }; - repoListItem.branches = repoListItem.branches.filter(element => element.name !== branch.name); - if (checked) { - repoListItem.branches.push(branch); - } - - this.props.pinRepo(repoListItem); - } - }; - - return ; - } - - private onRenderUnpinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - const linkProps: ILinkProps = { - disabled: this.props.unpinnedReposProps.isLoading, - onClick: this.props.unpinnedReposProps.loadMore - }; - - const linkText = this.props.unpinnedReposProps.isLoading - ? ReposListComponent.LoadingText - : ReposListComponent.LoadMoreText; - return {linkText}; - } - - const checkboxProps: ICheckboxProps = { - ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)), - styles: ReposListCheckboxStyles, - onChange: () => { - const repoListItem = { ...item }; - repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }]; - this.props.pinRepo(repoListItem); - } - }; - - return ; - }; - - private onRenderReposFooter = (detailsFooterProps: IDetailsFooterProps): JSX.Element => { - const props: IDetailsRowBaseProps = { - ...detailsFooterProps, - item: {}, - itemIndex: ReposListComponent.FooterIndex - }; - - return ; - }; - - private static getCheckboxPropsForLabel(label: string): ICheckboxProps { - return { - label, - title: label, - ariaLabel: label - }; - } - - private static getKey(item: RepoListItem): string { - return item.key; - } -} +import { + Checkbox, + DetailsList, + DetailsRow, + Dropdown, + ICheckboxProps, + IDetailsFooterProps, + IDetailsListProps, + IDetailsRowBaseProps, + IDropdown, + IDropdownOption, + IDropdownProps, + ILinkProps, + ISelectableDroppableTextProps, + Link, + ResponsiveMode, + SelectionMode, + Text, +} from "office-ui-fabric-react"; +import * as React from "react"; +import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient"; +import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { RepoListItem } from "./GitHubReposComponent"; +import { + BranchesDropdownCheckboxStyles, + BranchesDropdownOptionContainerStyle, + ReposListCheckboxStyles, + ReposListRepoColumnMinWidth, + ReposListBranchesColumnWidth, + BranchesDropdownWidth, + BranchesDropdownStyles, +} from "./GitHubStyleConstants"; + +export interface ReposListComponentProps { + branchesProps: Record; // key'd by repo key + pinnedReposProps: PinnedReposProps; + unpinnedReposProps: UnpinnedReposProps; + pinRepo: (repo: RepoListItem) => void; + unpinRepo: (repo: RepoListItem) => void; +} + +export interface BranchesProps { + branches: IGitHubBranch[]; + lastPageInfo?: IGitHubPageInfo; + hasMore: boolean; + isLoading: boolean; + loadMore: () => void; +} + +export interface PinnedReposProps { + repos: RepoListItem[]; +} + +export interface UnpinnedReposProps { + repos: RepoListItem[]; + hasMore: boolean; + isLoading: boolean; + loadMore: () => void; +} + +export class ReposListComponent extends React.Component { + private static readonly PinnedReposColumnName = "Pinned repos"; + private static readonly UnpinnedReposColumnName = "Unpinned repos"; + private static readonly BranchesColumnName = "Branches"; + private static readonly LoadingText = "Loading..."; + private static readonly LoadMoreText = "Load more"; + private static readonly DefaultBranchName = "master"; + private static readonly FooterIndex = -1; + + public render(): JSX.Element { + const pinnedReposListProps: IDetailsListProps = { + styles: { + contentWrapper: { + height: this.props.pinnedReposProps.repos.length ? undefined : 0, + }, + }, + items: this.props.pinnedReposProps.repos, + getKey: ReposListComponent.getKey, + selectionMode: SelectionMode.none, + compact: true, + columns: [ + { + key: ReposListComponent.PinnedReposColumnName, + name: ReposListComponent.PinnedReposColumnName, + ariaLabel: ReposListComponent.PinnedReposColumnName, + minWidth: ReposListRepoColumnMinWidth, + onRender: this.onRenderPinnedReposColumnItem, + }, + { + key: ReposListComponent.BranchesColumnName, + name: ReposListComponent.BranchesColumnName, + ariaLabel: ReposListComponent.BranchesColumnName, + minWidth: ReposListBranchesColumnWidth, + maxWidth: ReposListBranchesColumnWidth, + onRender: this.onRenderPinnedReposBranchesColumnItem, + }, + ], + onRenderDetailsFooter: this.props.pinnedReposProps.repos.length ? undefined : this.onRenderReposFooter, + }; + + const unpinnedReposListProps: IDetailsListProps = { + items: this.props.unpinnedReposProps.repos, + getKey: ReposListComponent.getKey, + selectionMode: SelectionMode.none, + compact: true, + columns: [ + { + key: ReposListComponent.UnpinnedReposColumnName, + name: ReposListComponent.UnpinnedReposColumnName, + ariaLabel: ReposListComponent.UnpinnedReposColumnName, + minWidth: ReposListRepoColumnMinWidth, + onRender: this.onRenderUnpinnedReposColumnItem, + }, + { + key: ReposListComponent.BranchesColumnName, + name: ReposListComponent.BranchesColumnName, + ariaLabel: ReposListComponent.BranchesColumnName, + minWidth: ReposListBranchesColumnWidth, + maxWidth: ReposListBranchesColumnWidth, + onRender: this.onRenderUnpinnedReposBranchesColumnItem, + }, + ], + onRenderDetailsFooter: + this.props.unpinnedReposProps.isLoading || this.props.unpinnedReposProps.hasMore + ? this.onRenderReposFooter + : undefined, + }; + + return ( + <> + + + + ); + } + + private onRenderPinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => { + if (index === ReposListComponent.FooterIndex) { + return None; + } + + const checkboxProps: ICheckboxProps = { + ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)), + styles: ReposListCheckboxStyles, + defaultChecked: true, + onChange: () => this.props.unpinRepo(item), + }; + + return ; + }; + + private onRenderPinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => { + if (index === ReposListComponent.FooterIndex) { + return <>; + } + + const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]; + const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({ + key: branch.name, + text: branch.name, + data: item, + disabled: item.branches.length === 1 && branch.name === item.branches[0].name, + selected: item.branches.findIndex((element) => element.name === branch.name) !== -1, + })); + + if (branchesProps.hasMore || branchesProps.isLoading) { + const text = branchesProps.isLoading ? ReposListComponent.LoadingText : ReposListComponent.LoadMoreText; + options.push({ + key: text, + text, + data: item, + index: ReposListComponent.FooterIndex, + }); + } + + const dropdownProps: IDropdownProps = { + styles: BranchesDropdownStyles, + dropdownWidth: BranchesDropdownWidth, + responsiveMode: ResponsiveMode.large, + options, + onRenderList: this.onRenderBranchesDropdownList, + }; + + if (item.branches.length === 1) { + dropdownProps.placeholder = item.branches[0].name; + } else if (item.branches.length > 1) { + dropdownProps.placeholder = `${item.branches.length} branches`; + } + + return ; + }; + + private onRenderUnpinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => { + if (index === ReposListComponent.FooterIndex) { + return <>; + } + + const dropdownProps: IDropdownProps = { + styles: BranchesDropdownStyles, + options: [], + placeholder: ReposListComponent.DefaultBranchName, + disabled: true, + }; + + return ; + }; + + private onRenderBranchesDropdownList = ( + props: ISelectableDroppableTextProps + ): JSX.Element => { + const renderedList: JSX.Element[] = []; + props.options.forEach((option: IDropdownOption) => { + const item = ( +
+ {this.onRenderPinnedReposBranchesDropdownOption(option)} +
+ ); + renderedList.push(item); + }); + + return <>{renderedList}; + }; + + private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element { + const item: RepoListItem = option.data; + const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]; + + if (option.index === ReposListComponent.FooterIndex) { + const linkProps: ILinkProps = { + disabled: branchesProps.isLoading, + onClick: branchesProps.loadMore, + }; + + return {option.text}; + } + + const checkboxProps: ICheckboxProps = { + ...ReposListComponent.getCheckboxPropsForLabel(option.text), + styles: BranchesDropdownCheckboxStyles, + defaultChecked: option.selected, + disabled: option.disabled, + onChange: (event, checked) => { + const repoListItem = { ...item }; + const branch: IGitHubBranch = { name: option.text }; + repoListItem.branches = repoListItem.branches.filter((element) => element.name !== branch.name); + if (checked) { + repoListItem.branches.push(branch); + } + + this.props.pinRepo(repoListItem); + }, + }; + + return ; + } + + private onRenderUnpinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => { + if (index === ReposListComponent.FooterIndex) { + const linkProps: ILinkProps = { + disabled: this.props.unpinnedReposProps.isLoading, + onClick: this.props.unpinnedReposProps.loadMore, + }; + + const linkText = this.props.unpinnedReposProps.isLoading + ? ReposListComponent.LoadingText + : ReposListComponent.LoadMoreText; + return {linkText}; + } + + const checkboxProps: ICheckboxProps = { + ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)), + styles: ReposListCheckboxStyles, + onChange: () => { + const repoListItem = { ...item }; + repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }]; + this.props.pinRepo(repoListItem); + }, + }; + + return ; + }; + + private onRenderReposFooter = (detailsFooterProps: IDetailsFooterProps): JSX.Element => { + const props: IDetailsRowBaseProps = { + ...detailsFooterProps, + item: {}, + itemIndex: ReposListComponent.FooterIndex, + }; + + return ; + }; + + private static getCheckboxPropsForLabel(label: string): ICheckboxProps { + return { + label, + title: label, + ariaLabel: label, + }; + } + + private static getKey(item: RepoListItem): string { + return item.key; + } +} diff --git a/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx b/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx index 052e00402..516050108 100644 --- a/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx +++ b/src/Explorer/Controls/Header/GalleryHeaderComponent.tsx @@ -1,81 +1,81 @@ -import * as React from "react"; -import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "office-ui-fabric-react"; - -export class GalleryHeaderComponent extends React.Component { - private static readonly azureText = "Microsoft Azure"; - private static readonly cosmosdbText = "Cosmos DB"; - private static readonly galleryText = "Gallery"; - private static readonly loginText = "Sign In"; - private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank"); - private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href); - private static readonly headerItemStyle: React.CSSProperties = { - color: "white" - }; - private static readonly mainHeaderTextProps: ITextProps = { - style: GalleryHeaderComponent.headerItemStyle, - variant: "mediumPlus", - styles: { - root: { - fontWeight: FontWeights.semibold - } - } - }; - private static readonly headerItemTextProps: ITextProps = { style: GalleryHeaderComponent.headerItemStyle }; - - private renderHeaderItem = (text: string, onClick: () => void, textProps: ITextProps): JSX.Element => { - return ( - - {text} - - ); - }; - - public render(): JSX.Element { - return ( - - - {this.renderHeaderItem( - GalleryHeaderComponent.azureText, - GalleryHeaderComponent.openPortal, - GalleryHeaderComponent.mainHeaderTextProps - )} - - - - - - {this.renderHeaderItem( - GalleryHeaderComponent.cosmosdbText, - GalleryHeaderComponent.openDataExplorer, - GalleryHeaderComponent.headerItemTextProps - )} - - - - - - {this.renderHeaderItem( - GalleryHeaderComponent.galleryText, - undefined, - GalleryHeaderComponent.headerItemTextProps - )} - - - <> - - - {this.renderHeaderItem( - GalleryHeaderComponent.loginText, - GalleryHeaderComponent.openDataExplorer, - GalleryHeaderComponent.headerItemTextProps - )} - - - ); - } -} +import * as React from "react"; +import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "office-ui-fabric-react"; + +export class GalleryHeaderComponent extends React.Component { + private static readonly azureText = "Microsoft Azure"; + private static readonly cosmosdbText = "Cosmos DB"; + private static readonly galleryText = "Gallery"; + private static readonly loginText = "Sign In"; + private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank"); + private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href); + private static readonly headerItemStyle: React.CSSProperties = { + color: "white", + }; + private static readonly mainHeaderTextProps: ITextProps = { + style: GalleryHeaderComponent.headerItemStyle, + variant: "mediumPlus", + styles: { + root: { + fontWeight: FontWeights.semibold, + }, + }, + }; + private static readonly headerItemTextProps: ITextProps = { style: GalleryHeaderComponent.headerItemStyle }; + + private renderHeaderItem = (text: string, onClick: () => void, textProps: ITextProps): JSX.Element => { + return ( + + {text} + + ); + }; + + public render(): JSX.Element { + return ( + + + {this.renderHeaderItem( + GalleryHeaderComponent.azureText, + GalleryHeaderComponent.openPortal, + GalleryHeaderComponent.mainHeaderTextProps + )} + + + + + + {this.renderHeaderItem( + GalleryHeaderComponent.cosmosdbText, + GalleryHeaderComponent.openDataExplorer, + GalleryHeaderComponent.headerItemTextProps + )} + + + + + + {this.renderHeaderItem( + GalleryHeaderComponent.galleryText, + undefined, + GalleryHeaderComponent.headerItemTextProps + )} + + + <> + + + {this.renderHeaderItem( + GalleryHeaderComponent.loginText, + GalleryHeaderComponent.openDataExplorer, + GalleryHeaderComponent.headerItemTextProps + )} + + + ); + } +} diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts b/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts index aff4a14fd..14d9ca8dd 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts +++ b/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts @@ -1,186 +1,186 @@ -/** - * How to use this component: - * - * In your html markup, use: - * - * The parameters are documented below. - * - * Notes: - * - dynamic:true by default, this allows choices to change after initialization. - * To turn it off, use: - * typeaheadOverrideOptions: { dynamic:false } - * - */ - -import "jquery-typeahead"; -import template from "./input-typeahead.html"; - -/** - * Helper class for ko component registration - */ -export class InputTypeaheadComponent { - constructor() { - return { - viewModel: InputTypeaheadViewModel, - template - }; - } -} - -export interface Item { - caption: string; - value: any; -} - -/** - * Parameters for this component - */ -interface InputTypeaheadParams { - /** - * List of choices available in the dropdown. - */ - choices: ko.ObservableArray; - - /** - * Gets updated when user clicks on the choice in the dropdown - */ - selection?: ko.Observable; - - /** - * The current string value of - */ - inputValue?: ko.Observable; - - /** - * Define what text you want as the input placeholder - */ - placeholder: string; - - /** - * Override default jquery-typeahead options - * WARNING: do not override input, source or callback to avoid breaking the components behavior. - */ - typeaheadOverrideOptions?: any; - - /** - * This function gets called when pressing ENTER on the input box - */ - submitFct?: (inputValue: string | null, selection: Item | null) => void; - - /** - * Typehead comes with a Search button that we normally remove. - * If you want to use it, turn this on - */ - showSearchButton?: boolean; -} - -interface OnClickItem { - matchedKey: string; - value: any; - caption: string; - group: string; -} - -interface Cache { - inputValue: string | null; - selection: Item | null; -} - -class InputTypeaheadViewModel { - private static instanceCount = 0; // Generate unique id for each component's typeahead instance - private instanceNumber: number; - private params: InputTypeaheadParams; - - private cache: Cache; - - public constructor(params: InputTypeaheadParams) { - this.instanceNumber = InputTypeaheadViewModel.instanceCount++; - this.params = params; - - this.params.choices.subscribe(this.initializeTypeahead.bind(this)); - this.cache = { - inputValue: null, - selection: null - }; - } - - /** - * Must execute once ko is rendered, so that it can find the input element by id - */ - private initializeTypeahead() { - let params = this.params; - let cache = this.cache; - let options: any = { - input: `#${this.getComponentId()}`, //'.input-typeahead', - order: "asc", - minLength: 0, - searchOnFocus: true, - source: { - display: "caption", - data: () => { - return this.params.choices(); - } - }, - callback: { - onClick: (node: any, a: any, item: OnClickItem, event: any) => { - cache.selection = item; - - if (params.selection) { - params.selection(item); - } - }, - onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) { - cache.inputValue = query; - if (params.inputValue) { - params.inputValue(query); - } - } - }, - template: (query: string, item: any) => { - // Don't display id if caption *IS* the id - return item.caption === item.value - ? "{{caption}}" - : "
{{caption}}
{{value}}
"; - }, - dynamic: true - }; - - // Override options - if (params.typeaheadOverrideOptions) { - for (let p in params.typeaheadOverrideOptions) { - options[p] = params.typeaheadOverrideOptions[p]; - } - } - - ($ as any).typeahead(options); - } - - /** - * Get this component id - * @return unique id per instance - */ - private getComponentId(): string { - return `input-typeahead${this.instanceNumber}`; - } - - /** - * Executed once ko is done rendering bindings - * Use ko's "template: afterRender" callback to do that without actually using any template. - * Another way is to call it within setTimeout() in constructor. - */ - public afterRender(): void { - this.initializeTypeahead(); - } - - public submit(): void { - if (this.params.submitFct) { - this.params.submitFct(this.cache.inputValue, this.cache.selection); - } - } -} +/** + * How to use this component: + * + * In your html markup, use: + * + * The parameters are documented below. + * + * Notes: + * - dynamic:true by default, this allows choices to change after initialization. + * To turn it off, use: + * typeaheadOverrideOptions: { dynamic:false } + * + */ + +import "jquery-typeahead"; +import template from "./input-typeahead.html"; + +/** + * Helper class for ko component registration + */ +export class InputTypeaheadComponent { + constructor() { + return { + viewModel: InputTypeaheadViewModel, + template, + }; + } +} + +export interface Item { + caption: string; + value: any; +} + +/** + * Parameters for this component + */ +interface InputTypeaheadParams { + /** + * List of choices available in the dropdown. + */ + choices: ko.ObservableArray; + + /** + * Gets updated when user clicks on the choice in the dropdown + */ + selection?: ko.Observable; + + /** + * The current string value of + */ + inputValue?: ko.Observable; + + /** + * Define what text you want as the input placeholder + */ + placeholder: string; + + /** + * Override default jquery-typeahead options + * WARNING: do not override input, source or callback to avoid breaking the components behavior. + */ + typeaheadOverrideOptions?: any; + + /** + * This function gets called when pressing ENTER on the input box + */ + submitFct?: (inputValue: string | null, selection: Item | null) => void; + + /** + * Typehead comes with a Search button that we normally remove. + * If you want to use it, turn this on + */ + showSearchButton?: boolean; +} + +interface OnClickItem { + matchedKey: string; + value: any; + caption: string; + group: string; +} + +interface Cache { + inputValue: string | null; + selection: Item | null; +} + +class InputTypeaheadViewModel { + private static instanceCount = 0; // Generate unique id for each component's typeahead instance + private instanceNumber: number; + private params: InputTypeaheadParams; + + private cache: Cache; + + public constructor(params: InputTypeaheadParams) { + this.instanceNumber = InputTypeaheadViewModel.instanceCount++; + this.params = params; + + this.params.choices.subscribe(this.initializeTypeahead.bind(this)); + this.cache = { + inputValue: null, + selection: null, + }; + } + + /** + * Must execute once ko is rendered, so that it can find the input element by id + */ + private initializeTypeahead() { + let params = this.params; + let cache = this.cache; + let options: any = { + input: `#${this.getComponentId()}`, //'.input-typeahead', + order: "asc", + minLength: 0, + searchOnFocus: true, + source: { + display: "caption", + data: () => { + return this.params.choices(); + }, + }, + callback: { + onClick: (node: any, a: any, item: OnClickItem, event: any) => { + cache.selection = item; + + if (params.selection) { + params.selection(item); + } + }, + onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) { + cache.inputValue = query; + if (params.inputValue) { + params.inputValue(query); + } + }, + }, + template: (query: string, item: any) => { + // Don't display id if caption *IS* the id + return item.caption === item.value + ? "{{caption}}" + : "
{{caption}}
{{value}}
"; + }, + dynamic: true, + }; + + // Override options + if (params.typeaheadOverrideOptions) { + for (let p in params.typeaheadOverrideOptions) { + options[p] = params.typeaheadOverrideOptions[p]; + } + } + + ($ as any).typeahead(options); + } + + /** + * Get this component id + * @return unique id per instance + */ + private getComponentId(): string { + return `input-typeahead${this.instanceNumber}`; + } + + /** + * Executed once ko is done rendering bindings + * Use ko's "template: afterRender" callback to do that without actually using any template. + * Another way is to call it within setTimeout() in constructor. + */ + public afterRender(): void { + this.initializeTypeahead(); + } + + public submit(): void { + if (this.params.submitFct) { + this.params.submitFct(this.cache.inputValue, this.cache.selection); + } + } +} diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.test.tsx b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.test.tsx index caa3821bc..e06b28ade 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.test.tsx +++ b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.test.tsx @@ -8,10 +8,10 @@ describe("inputTypeahead", () => { const props: InputTypeaheadComponentProps = { choices: [ { caption: "item1", value: "value1" }, - { caption: "item2", value: "value2" } + { caption: "item2", value: "value2" }, ], placeholder: "placeholder", - useTextarea: false + useTextarea: false, }; const wrapper = shallow(); @@ -22,10 +22,10 @@ describe("inputTypeahead", () => { const props: InputTypeaheadComponentProps = { choices: [ { caption: "item1", value: "value1" }, - { caption: "item2", value: "value2" } + { caption: "item2", value: "value2" }, ], placeholder: "placeholder", - useTextarea: true + useTextarea: true, }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx index 6fa224ba1..22aa71388 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx +++ b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx @@ -103,7 +103,7 @@ export class InputTypeaheadComponent extends React.Component< super(props); this.cache = { inputValue: null, - selection: null + selection: null, }; } @@ -138,7 +138,7 @@ export class InputTypeaheadComponent extends React.Component< className="input-typehead" onKeyDown={(event: React.KeyboardEvent) => this.onKeyDown(event)} > -
(this.containerElt = input)}> +
(this.containerElt = input)}>
{this.props.useTextarea ? ( @@ -147,7 +147,7 @@ export class InputTypeaheadComponent extends React.Component< name="q" autoComplete="off" aria-label="Input query" - ref={input => (this.inputElt = input)} + ref={(input) => (this.inputElt = input)} defaultValue={this.props.defaultValue} /> ) : ( @@ -156,7 +156,7 @@ export class InputTypeaheadComponent extends React.Component< type="search" autoComplete="off" aria-label="Input query" - ref={input => (this.inputElt = input)} + ref={(input) => (this.inputElt = input)} defaultValue={this.props.defaultValue} /> )} @@ -181,9 +181,7 @@ export class InputTypeaheadComponent extends React.Component< event.preventDefault(); event.stopPropagation(); this.props.submitFct(this.cache.inputValue, this.cache.selection); - $(this.containerElt) - .children(".typeahead__result") - .hide(); + $(this.containerElt).children(".typeahead__result").hide(); } } } @@ -203,7 +201,7 @@ export class InputTypeaheadComponent extends React.Component< display: "caption", data: () => { return props.choices; - } + }, }, callback: { onClick: (node: any, a: any, item: OnClickItem, event: any) => { @@ -218,7 +216,7 @@ export class InputTypeaheadComponent extends React.Component< if (props.onNewValue) { props.onNewValue(query); } - } + }, }, template: (query: string, item: any) => { // Don't display id if caption *IS* the id @@ -226,7 +224,7 @@ export class InputTypeaheadComponent extends React.Component< ? "{{caption}}" : "
{{caption}}
{{value}}
"; }, - dynamic: true + dynamic: true, }; // Override options diff --git a/src/Explorer/Controls/InputTypeahead/input-typeahead.html b/src/Explorer/Controls/InputTypeahead/input-typeahead.html index 9fe4b4167..171fef8bf 100644 --- a/src/Explorer/Controls/InputTypeahead/input-typeahead.html +++ b/src/Explorer/Controls/InputTypeahead/input-typeahead.html @@ -1,19 +1,19 @@ - -
-
-
- - - - - - -
-
-
-
+ +
+
+
+ + + + + + +
+
+
+
diff --git a/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts b/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts index 7611fe567..542f3e6a8 100644 --- a/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts +++ b/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts @@ -1,173 +1,173 @@ -import Q from "q"; -import * as monaco from "monaco-editor"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel"; -import template from "./json-editor-component.html"; - -/** - * Helper class for ko component registration - */ -export class JsonEditorComponent { - constructor() { - return { - viewModel: JsonEditorViewModel, - template - }; - } -} - -/** - * Parameters for this component - */ -export interface JsonEditorParams { - content: ViewModels.Editable; // Sets the initial content of the editor - isReadOnly: boolean; - ariaLabel: string; // Sets what will be read to the user to define the control - updatedContent?: ViewModels.Editable; // Gets updated when user edits - selectedContent?: ViewModels.Editable; // Gets updated when user selects content from the editor - lineNumbers?: monaco.editor.IEditorOptions["lineNumbers"]; - theme?: string; // Monaco editor theme -} - -/** - * JSON Editor: - * A ko wrapper for the Monaco editor - * - * How to use in your markup: - * - * - * In writable mode, if you want to get changes to the content pass updatedContent and subscribe to it. - * content and updateContent are different to prevent circular updates. - */ -export class JsonEditorViewModel extends WaitsForTemplateViewModel { - protected editorContainer: HTMLElement; - protected params: JsonEditorParams; - private static instanceCount = 0; // Generate unique id to get different monaco editor - private editor: monaco.editor.IStandaloneCodeEditor; - private instanceNumber: number; - private resizer: EventListenerOrEventListenerObject; - private observer: MutationObserver; - private offsetWidth: number; - private offsetHeight: number; - private selectionListener: monaco.IDisposable; - private latestContentVersionId: number; - - public constructor(params: JsonEditorParams) { - super(); - - this.instanceNumber = JsonEditorViewModel.instanceCount++; - this.params = params; - - this.params.content.subscribe((newValue: string) => { - if (newValue) { - if (!!this.editor) { - this.editor.getModel().setValue(newValue); - } else { - this.createEditor(newValue, this.configureEditor.bind(this)); - } - } - }); - - const onObserve: MutationCallback = (mutations: MutationRecord[], observer: MutationObserver): void => { - if ( - this.offsetWidth !== this.editorContainer.offsetWidth || - this.offsetHeight !== this.editorContainer.offsetHeight - ) { - this.editor.layout(); - this.offsetWidth = this.editorContainer.offsetWidth; - this.offsetHeight = this.editorContainer.offsetHeight; - } - }; - this.observer = new MutationObserver(onObserve); - } - - protected getEditorId(): string { - return `jsoneditor${this.instanceNumber}`; - } - - /** - * Create the monaco editor and attach to DOM - */ - protected createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) { - this.registerCompletionItemProvider(); - this.editorContainer = document.getElementById(this.getEditorId()); - const options: monaco.editor.IEditorConstructionOptions = { - value: content, - language: this.getEditorLanguage(), - readOnly: this.params.isReadOnly, - lineNumbers: this.params.lineNumbers || "off", - fontSize: 12, - ariaLabel: this.params.ariaLabel, - theme: this.params.theme - }; - - this.editorContainer.innerHTML = ""; - createCallback(monaco.editor.create(this.editorContainer, options)); - } - - // Interface. Will be implemented in children editor view model such as EditorViewModel. - protected registerCompletionItemProvider() {} - - // Interface. Will be implemented in children editor view model such as EditorViewModel. - protected getErrorMarkers(input: string): Q.Promise { - return Q.Promise(() => {}); - } - - protected getEditorLanguage(): string { - return "json"; - } - - protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { - this.editor = editor; - const queryEditorModel = this.editor.getModel(); - if (!this.params.isReadOnly && this.params.updatedContent) { - queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => { - const queryEditorModel = this.editor.getModel(); - this.params.updatedContent(queryEditorModel.getValue()); - }); - } - - if (this.params.selectedContent) { - this.selectionListener = this.editor.onDidChangeCursorSelection( - (event: monaco.editor.ICursorSelectionChangedEvent) => { - const selectedContent: string = this.editor.getModel().getValueInRange(event.selection); - this.params.selectedContent(selectedContent); - } - ); - } - - this.resizer = () => { - editor.layout(); - }; - window.addEventListener("resize", this.resizer); - - this.offsetHeight = this.editorContainer.offsetHeight; - this.offsetWidth = this.editorContainer.offsetWidth; - - this.observer.observe(document.body, { - attributes: true, - subtree: true, - childList: true - }); - - this.editor.getModel().onDidChangeContent(async (e: monaco.editor.IModelContentChangedEvent) => { - if (!(e).isFlush) { - return; - } - - this.latestContentVersionId = e.versionId; - let input = (e).changes[0].text; - let marks = await this.getErrorMarkers(input); - if (e.versionId === this.latestContentVersionId) { - monaco.editor.setModelMarkers(this.editor.getModel(), "ErrorMarkerOwner", marks); - } - }); - this.editor.focus(); - } - - private dispose() { - window.removeEventListener("resize", this.resizer); - this.selectionListener && this.selectionListener.dispose(); - this.observer.disconnect(); - } -} +import Q from "q"; +import * as monaco from "monaco-editor"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel"; +import template from "./json-editor-component.html"; + +/** + * Helper class for ko component registration + */ +export class JsonEditorComponent { + constructor() { + return { + viewModel: JsonEditorViewModel, + template, + }; + } +} + +/** + * Parameters for this component + */ +export interface JsonEditorParams { + content: ViewModels.Editable; // Sets the initial content of the editor + isReadOnly: boolean; + ariaLabel: string; // Sets what will be read to the user to define the control + updatedContent?: ViewModels.Editable; // Gets updated when user edits + selectedContent?: ViewModels.Editable; // Gets updated when user selects content from the editor + lineNumbers?: monaco.editor.IEditorOptions["lineNumbers"]; + theme?: string; // Monaco editor theme +} + +/** + * JSON Editor: + * A ko wrapper for the Monaco editor + * + * How to use in your markup: + * + * + * In writable mode, if you want to get changes to the content pass updatedContent and subscribe to it. + * content and updateContent are different to prevent circular updates. + */ +export class JsonEditorViewModel extends WaitsForTemplateViewModel { + protected editorContainer: HTMLElement; + protected params: JsonEditorParams; + private static instanceCount = 0; // Generate unique id to get different monaco editor + private editor: monaco.editor.IStandaloneCodeEditor; + private instanceNumber: number; + private resizer: EventListenerOrEventListenerObject; + private observer: MutationObserver; + private offsetWidth: number; + private offsetHeight: number; + private selectionListener: monaco.IDisposable; + private latestContentVersionId: number; + + public constructor(params: JsonEditorParams) { + super(); + + this.instanceNumber = JsonEditorViewModel.instanceCount++; + this.params = params; + + this.params.content.subscribe((newValue: string) => { + if (newValue) { + if (!!this.editor) { + this.editor.getModel().setValue(newValue); + } else { + this.createEditor(newValue, this.configureEditor.bind(this)); + } + } + }); + + const onObserve: MutationCallback = (mutations: MutationRecord[], observer: MutationObserver): void => { + if ( + this.offsetWidth !== this.editorContainer.offsetWidth || + this.offsetHeight !== this.editorContainer.offsetHeight + ) { + this.editor.layout(); + this.offsetWidth = this.editorContainer.offsetWidth; + this.offsetHeight = this.editorContainer.offsetHeight; + } + }; + this.observer = new MutationObserver(onObserve); + } + + protected getEditorId(): string { + return `jsoneditor${this.instanceNumber}`; + } + + /** + * Create the monaco editor and attach to DOM + */ + protected createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) { + this.registerCompletionItemProvider(); + this.editorContainer = document.getElementById(this.getEditorId()); + const options: monaco.editor.IEditorConstructionOptions = { + value: content, + language: this.getEditorLanguage(), + readOnly: this.params.isReadOnly, + lineNumbers: this.params.lineNumbers || "off", + fontSize: 12, + ariaLabel: this.params.ariaLabel, + theme: this.params.theme, + }; + + this.editorContainer.innerHTML = ""; + createCallback(monaco.editor.create(this.editorContainer, options)); + } + + // Interface. Will be implemented in children editor view model such as EditorViewModel. + protected registerCompletionItemProvider() {} + + // Interface. Will be implemented in children editor view model such as EditorViewModel. + protected getErrorMarkers(input: string): Q.Promise { + return Q.Promise(() => {}); + } + + protected getEditorLanguage(): string { + return "json"; + } + + protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { + this.editor = editor; + const queryEditorModel = this.editor.getModel(); + if (!this.params.isReadOnly && this.params.updatedContent) { + queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => { + const queryEditorModel = this.editor.getModel(); + this.params.updatedContent(queryEditorModel.getValue()); + }); + } + + if (this.params.selectedContent) { + this.selectionListener = this.editor.onDidChangeCursorSelection( + (event: monaco.editor.ICursorSelectionChangedEvent) => { + const selectedContent: string = this.editor.getModel().getValueInRange(event.selection); + this.params.selectedContent(selectedContent); + } + ); + } + + this.resizer = () => { + editor.layout(); + }; + window.addEventListener("resize", this.resizer); + + this.offsetHeight = this.editorContainer.offsetHeight; + this.offsetWidth = this.editorContainer.offsetWidth; + + this.observer.observe(document.body, { + attributes: true, + subtree: true, + childList: true, + }); + + this.editor.getModel().onDidChangeContent(async (e: monaco.editor.IModelContentChangedEvent) => { + if (!(e).isFlush) { + return; + } + + this.latestContentVersionId = e.versionId; + let input = (e).changes[0].text; + let marks = await this.getErrorMarkers(input); + if (e.versionId === this.latestContentVersionId) { + monaco.editor.setModelMarkers(this.editor.getModel(), "ErrorMarkerOwner", marks); + } + }); + this.editor.focus(); + } + + private dispose() { + window.removeEventListener("resize", this.resizer); + this.selectionListener && this.selectionListener.dispose(); + this.observer.disconnect(); + } +} diff --git a/src/Explorer/Controls/JsonEditor/json-editor-component.html b/src/Explorer/Controls/JsonEditor/json-editor-component.html index f23a2ccc3..b02e18ed2 100644 --- a/src/Explorer/Controls/JsonEditor/json-editor-component.html +++ b/src/Explorer/Controls/JsonEditor/json-editor-component.html @@ -1 +1 @@ -
+
diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx index 961c287f5..cca31f60a 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx @@ -1,158 +1,158 @@ -import * as DataModels from "../../../Contracts/DataModels"; -import { NotebookTerminalComponent } from "./NotebookTerminalComponent"; - -const createTestDatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: null, - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - gremlinEndpoint: null, - tableEndpoint: null - }, - tags: "testTags", - type: "testType" - }; -}; - -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 - }, - tags: "testTags", - type: "testType" - }; -}; - -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/" - }, - tags: "testTags", - type: "testType" - }; -}; - -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 - }, - tags: "testTags", - type: "testType" - }; -}; - -const createTerminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/" - }, - databaseAccount: createTestDatabaseAccount() - }); -}; - -const createMongo32Terminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo" - }, - databaseAccount: createTestMongo32DatabaseAccount() - }); -}; - -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("getTerminalParams: Test for terminal", () => { - const terminal: NotebookTerminalComponent = createTerminal(); - const params: Map = terminal.getTerminalParams(); - - expect(params).toEqual( - new Map([["terminal", "true"]]) - ); - }); - - it("getTerminalParams: Test for Mongo 3.2 terminal", () => { - const terminal: NotebookTerminalComponent = createMongo32Terminal(); - const params: Map = terminal.getTerminalParams(); - - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host] - ]) - ); - }); - - it("getTerminalParams: Test for Mongo 3.6 terminal", () => { - const terminal: NotebookTerminalComponent = createMongo36Terminal(); - const params: Map = terminal.getTerminalParams(); - - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host] - ]) - ); - }); - - it("getTerminalParams: Test for Cassandra terminal", () => { - const terminal: NotebookTerminalComponent = createCassandraTerminal(); - const params: Map = terminal.getTerminalParams(); - - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host] - ]) - ); - }); -}); +import * as DataModels from "../../../Contracts/DataModels"; +import { NotebookTerminalComponent } from "./NotebookTerminalComponent"; + +const createTestDatabaseAccount = (): DataModels.DatabaseAccount => { + return { + id: "testId", + kind: "testKind", + location: "testLocation", + name: "testName", + properties: { + cassandraEndpoint: null, + documentEndpoint: "https://testDocumentEndpoint.azure.com/", + gremlinEndpoint: null, + tableEndpoint: null, + }, + tags: "testTags", + type: "testType", + }; +}; + +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, + }, + tags: "testTags", + type: "testType", + }; +}; + +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/", + }, + tags: "testTags", + type: "testType", + }; +}; + +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, + }, + tags: "testTags", + type: "testType", + }; +}; + +const createTerminal = (): NotebookTerminalComponent => { + return new NotebookTerminalComponent({ + notebookServerInfo: { + authToken: "testAuthToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/", + }, + databaseAccount: createTestDatabaseAccount(), + }); +}; + +const createMongo32Terminal = (): NotebookTerminalComponent => { + return new NotebookTerminalComponent({ + notebookServerInfo: { + authToken: "testAuthToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", + }, + databaseAccount: createTestMongo32DatabaseAccount(), + }); +}; + +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("getTerminalParams: Test for terminal", () => { + const terminal: NotebookTerminalComponent = createTerminal(); + const params: Map = terminal.getTerminalParams(); + + expect(params).toEqual( + new Map([["terminal", "true"]]) + ); + }); + + it("getTerminalParams: Test for Mongo 3.2 terminal", () => { + const terminal: NotebookTerminalComponent = createMongo32Terminal(); + const params: Map = terminal.getTerminalParams(); + + expect(params).toEqual( + new Map([ + ["terminal", "true"], + ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host], + ]) + ); + }); + + it("getTerminalParams: Test for Mongo 3.6 terminal", () => { + const terminal: NotebookTerminalComponent = createMongo36Terminal(); + const params: Map = terminal.getTerminalParams(); + + expect(params).toEqual( + new Map([ + ["terminal", "true"], + ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host], + ]) + ); + }); + + it("getTerminalParams: Test for Cassandra terminal", () => { + const terminal: NotebookTerminalComponent = createCassandraTerminal(); + const params: Map = terminal.getTerminalParams(); + + expect(params).toEqual( + new Map([ + ["terminal", "true"], + ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host], + ]) + ); + }); +}); diff --git a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx index 31b77de10..d96fda04b 100644 --- a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx +++ b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx @@ -20,7 +20,7 @@ describe("GalleryCardComponent", () => { views: 0, newCellId: undefined, policyViolations: undefined, - pendingScanJobIds: undefined + pendingScanJobIds: undefined, }, isFavorite: false, showDownload: true, @@ -30,7 +30,7 @@ describe("GalleryCardComponent", () => { onFavoriteClick: undefined, onUnfavoriteClick: undefined, onDownloadClick: undefined, - onDeleteClick: undefined + onDeleteClick: undefined, }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx index 00ff0775a..37c976668 100644 --- a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx @@ -12,7 +12,7 @@ import { Button, LinkBase, Separator, - TooltipHost + TooltipHost, } from "office-ui-fabric-react"; import * as React from "react"; import { IGalleryItem } from "../../../../Juno/JunoClient"; @@ -47,7 +47,7 @@ export class GalleryCardComponent extends React.Component this.onClick(event, this.props.onClick)} + onClick={(event) => this.onClick(event, this.props.onClick)} > {this.props.data.tags?.map((tag, index, array) => ( - this.onClick(event, () => this.props.onTagClick(tag))}>{tag} + this.onClick(event, () => this.props.onTagClick(tag))}>{tag} {index === array.length - 1 ? <> : ", "} ))} @@ -92,8 +92,8 @@ export class GalleryCardComponent extends React.Component @@ -117,8 +117,8 @@ export class GalleryCardComponent extends React.Component @@ -176,7 +176,7 @@ export class GalleryCardComponent extends React.Component this.onClick(event, activate)} + onClick={(event) => this.onClick(event, activate)} /> ); diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx index 01d06d8e8..baf990a4a 100644 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx @@ -12,11 +12,11 @@ describe("CodeOfConductComponent", () => { const junoClient = new JunoClient(undefined); junoClient.acceptCodeOfConduct = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, - data: true + data: true, }); codeOfConductProps = { junoClient: junoClient, - onAcceptCodeOfConduct: jest.fn() + onAcceptCodeOfConduct: jest.fn(), }; }); @@ -27,10 +27,7 @@ describe("CodeOfConductComponent", () => { it("onAcceptedCodeOfConductCalled", async () => { const wrapper = shallow(); - wrapper - .find(".genericPaneSubmitBtn") - .first() - .simulate("click"); + wrapper.find(".genericPaneSubmitBtn").first().simulate("click"); await Promise.resolve(); expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled(); }); diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx index 90ddf7b09..263669daf 100644 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx @@ -24,7 +24,7 @@ export class CodeOfConductComponent extends React.Component { - constructor(props: GalleryAndNotebookViewerComponentProps) { - super(props); - - this.state = { - notebookUrl: props.notebookUrl, - galleryItem: props.galleryItem, - isFavorite: props.isFavorite, - selectedTab: props.selectedTab, - sortBy: props.sortBy, - searchText: props.searchText - }; - } - - public render(): JSX.Element { - if (this.state.notebookUrl) { - const props: NotebookViewerComponentProps = { - container: this.props.container, - junoClient: this.props.junoClient, - notebookUrl: this.state.notebookUrl, - galleryItem: this.state.galleryItem, - isFavorite: this.state.isFavorite, - backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab), - onBackClick: this.onBackClick, - onTagClick: this.loadTaggedItems - }; - - return ; - } - - const props: GalleryViewerComponentProps = { - container: this.props.container, - junoClient: this.props.junoClient, - selectedTab: this.state.selectedTab, - sortBy: this.state.sortBy, - searchText: this.state.searchText, - openNotebook: this.openNotebook, - onSelectedTabChange: this.onSelectedTabChange, - onSortByChange: this.onSortByChange, - onSearchTextChange: this.onSearchTextChange - }; - - return ; - } - - private onBackClick = (): void => { - this.setState({ - notebookUrl: undefined - }); - }; - - private loadTaggedItems = (tag: string): void => { - this.setState({ - notebookUrl: undefined, - searchText: tag - }); - }; - - private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => { - this.setState({ - notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id), - galleryItem: data, - isFavorite - }); - }; - - private onSelectedTabChange = (selectedTab: GalleryTab): void => { - this.setState({ - selectedTab - }); - }; - - private onSortByChange = (sortBy: SortBy): void => { - this.setState({ - sortBy - }); - }; - - private onSearchTextChange = (searchText: string): void => { - this.setState({ - searchText - }); - }; -} +import * as React from "react"; +import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient"; +import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent"; +import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent"; +import * as GalleryUtils from "../../../Utils/GalleryUtils"; +import Explorer from "../../Explorer"; + +export interface GalleryAndNotebookViewerComponentProps { + container?: Explorer; + junoClient: JunoClient; + notebookUrl?: string; + galleryItem?: IGalleryItem; + isFavorite?: boolean; + selectedTab: GalleryTab; + sortBy: SortBy; + searchText: string; +} + +interface GalleryAndNotebookViewerComponentState { + notebookUrl: string; + galleryItem: IGalleryItem; + isFavorite: boolean; + selectedTab: GalleryTab; + sortBy: SortBy; + searchText: string; +} + +export class GalleryAndNotebookViewerComponent extends React.Component< + GalleryAndNotebookViewerComponentProps, + GalleryAndNotebookViewerComponentState +> { + constructor(props: GalleryAndNotebookViewerComponentProps) { + super(props); + + this.state = { + notebookUrl: props.notebookUrl, + galleryItem: props.galleryItem, + isFavorite: props.isFavorite, + selectedTab: props.selectedTab, + sortBy: props.sortBy, + searchText: props.searchText, + }; + } + + public render(): JSX.Element { + if (this.state.notebookUrl) { + const props: NotebookViewerComponentProps = { + container: this.props.container, + junoClient: this.props.junoClient, + notebookUrl: this.state.notebookUrl, + galleryItem: this.state.galleryItem, + isFavorite: this.state.isFavorite, + backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab), + onBackClick: this.onBackClick, + onTagClick: this.loadTaggedItems, + }; + + return ; + } + + const props: GalleryViewerComponentProps = { + container: this.props.container, + junoClient: this.props.junoClient, + selectedTab: this.state.selectedTab, + sortBy: this.state.sortBy, + searchText: this.state.searchText, + openNotebook: this.openNotebook, + onSelectedTabChange: this.onSelectedTabChange, + onSortByChange: this.onSortByChange, + onSearchTextChange: this.onSearchTextChange, + }; + + return ; + } + + private onBackClick = (): void => { + this.setState({ + notebookUrl: undefined, + }); + }; + + private loadTaggedItems = (tag: string): void => { + this.setState({ + notebookUrl: undefined, + searchText: tag, + }); + }; + + private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => { + this.setState({ + notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id), + galleryItem: data, + isFavorite, + }); + }; + + private onSelectedTabChange = (selectedTab: GalleryTab): void => { + this.setState({ + selectedTab, + }); + }; + + private onSortByChange = (sortBy: SortBy): void => { + this.setState({ + sortBy, + }); + }; + + private onSearchTextChange = (searchText: string): void => { + this.setState({ + searchText, + }); + }; +} diff --git a/src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx b/src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx index 1d03c251e..b41b55337 100644 --- a/src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx +++ b/src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx @@ -1,23 +1,23 @@ -import ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { - GalleryAndNotebookViewerComponentProps, - GalleryAndNotebookViewerComponent -} from "./GalleryAndNotebookViewerComponent"; - -export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - constructor(private props: GalleryAndNotebookViewerComponentProps) { - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - return ; - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} +import ko from "knockout"; +import * as React from "react"; +import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; +import { + GalleryAndNotebookViewerComponentProps, + GalleryAndNotebookViewerComponent, +} from "./GalleryAndNotebookViewerComponent"; + +export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter { + public parameters: ko.Observable; + + constructor(private props: GalleryAndNotebookViewerComponentProps) { + this.parameters = ko.observable(Date.now()); + } + + public renderComponent(): JSX.Element { + return ; + } + + public triggerRender(): void { + window.requestAnimationFrame(() => this.parameters(Date.now())); + } +} diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx index f98242a54..d5e2adecf 100644 --- a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx @@ -12,7 +12,7 @@ describe("GalleryViewerComponent", () => { openNotebook: undefined, onSelectedTabChange: undefined, onSortByChange: undefined, - onSearchTextChange: undefined + onSearchTextChange: undefined, }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx index b288ef475..addc71223 100644 --- a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx @@ -14,7 +14,7 @@ import { PivotItem, SearchBox, Stack, - Text + Text, } from "office-ui-fabric-react"; import * as React from "react"; import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient"; @@ -44,14 +44,14 @@ export enum GalleryTab { OfficialSamples, PublicGallery, Favorites, - Published + Published, } export enum SortBy { MostViewed, MostDownloaded, MostFavorited, - MostRecent + MostRecent, } interface GalleryViewerComponentState { @@ -106,27 +106,27 @@ export class GalleryViewerComponent extends React.Component { + const pivotItems = tabs.map((tab) => { const pivotItemProps: IPivotItemProps = { itemKey: GalleryTab[tab.tab], style: { marginTop: 20 }, - headerText: GalleryUtils.getTabTitle(tab.tab) + headerText: GalleryUtils.getTabTitle(tab.tab), }; return ( @@ -201,7 +201,7 @@ export class GalleryViewerComponent extends React.Component { return { tab, - content: this.createSearchBarHeader(this.createCardsTabContent(data)) + content: this.createSearchBarHeader(this.createCardsTabContent(data)), }; }; @@ -212,7 +212,7 @@ export class GalleryViewerComponent extends React.ComponentName Policy violations - {data.map(item => ( + {data.map((item) => ( {item.name} {item.policyViolations.join(", ")} @@ -391,7 +391,7 @@ export class GalleryViewerComponent extends React.Component this.isGalleryItemPresent(searchText, item)); + return data?.filter((item) => this.isGalleryItemPresent(searchText, item)); } return data; @@ -482,7 +482,7 @@ export class GalleryViewerComponent extends React.Component tag.toUpperCase())); + searchData.push(...item.tags.map((tag) => tag.toUpperCase())); } for (const data of searchData) { @@ -525,7 +525,7 @@ export class GalleryViewerComponent extends React.Component value.id === item.id); + const index = items?.findIndex((value) => value.id === item.id); if (index !== -1) { items?.splice(index, 1, item); } @@ -539,14 +539,14 @@ export class GalleryViewerComponent extends React.Component { let isFavorite: boolean; if (this.props.container?.isGalleryPublishEnabled()) { - isFavorite = this.favoriteNotebooks?.find(item => item.id === data.id) !== undefined; + isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined; } const props: GalleryCardComponentProps = { data, @@ -558,7 +558,7 @@ export class GalleryViewerComponent extends React.Component this.favoriteItem(data), onUnfavoriteClick: () => this.unfavoriteItem(data), onDownloadClick: () => this.downloadItem(data), - onDeleteClick: () => this.deleteItem(data) + onDeleteClick: () => this.deleteItem(data), }; return ( @@ -571,7 +571,7 @@ export class GalleryViewerComponent extends React.Component { const searchText = tag; this.setState({ - searchText + searchText, }); this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true); @@ -591,18 +591,20 @@ export class GalleryViewerComponent extends React.Component => { GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, data, (item: IGalleryItem) => { - this.favoriteNotebooks = this.favoriteNotebooks?.filter(value => value.id !== item.id); + this.favoriteNotebooks = this.favoriteNotebooks?.filter((value) => value.id !== item.id); this.refreshSelectedTab(item); }); }; private downloadItem = async (data: IGalleryItem): Promise => { - GalleryUtils.downloadItem(this.props.container, this.props.junoClient, data, item => this.refreshSelectedTab(item)); + GalleryUtils.downloadItem(this.props.container, this.props.junoClient, data, (item) => + this.refreshSelectedTab(item) + ); }; private deleteItem = async (data: IGalleryItem): Promise => { - GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => { - this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id); + GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, (item) => { + this.publishedNotebooks = this.publishedNotebooks?.filter((notebook) => item.id !== notebook.id); this.refreshSelectedTab(item); }); }; @@ -612,7 +614,7 @@ export class GalleryViewerComponent extends React.Component, newValue?: string): void => { const searchText = newValue; this.setState({ - searchText + searchText, }); this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true); @@ -632,7 +634,7 @@ export class GalleryViewerComponent extends React.Component, option?: IDropdownOption): void => { const sortBy = option.key as SortBy; this.setState({ - sortBy + sortBy, }); this.loadTabContent(this.state.selectedTab, this.state.searchText, sortBy, true); diff --git a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx index 5f288bda5..000aef69e 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx @@ -20,7 +20,7 @@ describe("NotebookMetadataComponent", () => { views: 0, newCellId: undefined, policyViolations: undefined, - pendingScanJobIds: undefined + pendingScanJobIds: undefined, }, isFavorite: false, downloadButtonText: "Download", @@ -28,7 +28,7 @@ describe("NotebookMetadataComponent", () => { onDownloadClick: undefined, onFavoriteClick: undefined, onUnfavoriteClick: undefined, - onReportAbuseClick: undefined + onReportAbuseClick: undefined, }; const wrapper = shallow(); @@ -52,7 +52,7 @@ describe("NotebookMetadataComponent", () => { views: 0, newCellId: undefined, policyViolations: undefined, - pendingScanJobIds: undefined + pendingScanJobIds: undefined, }, isFavorite: true, downloadButtonText: "Download", @@ -60,7 +60,7 @@ describe("NotebookMetadataComponent", () => { onDownloadClick: undefined, onFavoriteClick: undefined, onUnfavoriteClick: undefined, - onReportAbuseClick: undefined + onReportAbuseClick: undefined, }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx index d41118f0e..d175aa183 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx @@ -10,7 +10,7 @@ import { PersonaSize, PrimaryButton, Stack, - Text + Text, } from "office-ui-fabric-react"; import * as React from "react"; import { IGalleryItem } from "../../../Juno/JunoClient"; @@ -35,7 +35,7 @@ export class NotebookMetadataComponent extends React.Component +export class NotebookViewerComponent + extends React.Component implements DialogHost { private clientManager: NotebookClientV2; private notebookComponentBootstrapper: NotebookComponentBootstrapper; @@ -59,12 +60,12 @@ export class NotebookViewerComponent extends React.Component} @@ -173,7 +174,7 @@ export class NotebookViewerComponent extends React.Component => { - GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item => + GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, (item) => this.setState({ galleryItem: item, isFavorite: true }) ); }; private unfavoriteItem = async (): Promise => { - GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item => + GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, (item) => this.setState({ galleryItem: item, isFavorite: false }) ); }; private downloadItem = async (): Promise => { - GalleryUtils.downloadItem(this.props.container, this.props.junoClient, this.state.galleryItem, item => + GalleryUtils.downloadItem(this.props.container, this.props.junoClient, this.state.galleryItem, (item) => this.setState({ galleryItem: item }) ); }; diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx index 2b732ce89..104b0ee61 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx @@ -1,292 +1,292 @@ -import * as _ from "underscore"; -import * as React from "react"; -import * as Constants from "../../../Common/Constants"; -import * as DataModels from "../../../Contracts/DataModels"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; -import { - DetailsList, - DetailsListLayoutMode, - IDetailsListProps, - IDetailsRowProps, - DetailsRow -} from "office-ui-fabric-react/lib/DetailsList"; -import { FocusZone } from "office-ui-fabric-react/lib/FocusZone"; -import { IconButton, IButtonProps } from "office-ui-fabric-react/lib/Button"; -import { IColumn } from "office-ui-fabric-react/lib/DetailsList"; -import { IContextualMenuProps, ContextualMenu } from "office-ui-fabric-react/lib/ContextualMenu"; -import { - IObjectWithKey, - ISelectionZoneProps, - Selection, - SelectionMode, - SelectionZone -} from "office-ui-fabric-react/lib/utilities/selection/index"; -import { StyleConstants } from "../../../Common/Constants"; -import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField"; -import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; - -import SaveQueryBannerIcon from "../../../../images/save_query_banner.png"; -import { QueriesClient } from "../../../Common/QueriesClient"; -import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; - -export interface QueriesGridComponentProps { - queriesClient: QueriesClient; - onQuerySelect: (query: DataModels.Query) => void; - containerVisible: boolean; - saveQueryEnabled: boolean; -} - -export interface QueriesGridComponentState { - queries: Query[]; - filteredResults: Query[]; -} - -interface Query extends DataModels.Query, IObjectWithKey { - key: string; -} - -export class QueriesGridComponent extends React.Component { - private selection: Selection; - private queryFilter: ITextField; - - constructor(props: QueriesGridComponentProps) { - super(props); - this.state = { - queries: [], - filteredResults: [] - }; - this.selection = new Selection(); - this.selection.setItems(this.state.filteredResults); - } - - public componentDidUpdate(prevProps: QueriesGridComponentProps, prevState: QueriesGridComponentState): void { - this.selection.setItems( - this.state.filteredResults, - !_.isEqual(prevState.filteredResults, this.state.filteredResults) - ); - this.queryFilter && this.queryFilter.focus(); - const querySetupCompleted: boolean = !prevProps.saveQueryEnabled && this.props.saveQueryEnabled; - const noQueryFiltersApplied: boolean = !this.queryFilter || !this.queryFilter.value; - if (!this.props.containerVisible || !this.props.saveQueryEnabled) { - return; - } else if (noQueryFiltersApplied && (!prevProps.containerVisible || querySetupCompleted)) { - // refresh only when pane is opened or query setup was recently completed - this.fetchSavedQueries(); - } - } - - public render(): JSX.Element { - if (this.state.queries.length === 0) { - return this.renderBannerComponent(); - } - return this.renderQueryGridComponent(); - } - - private renderQueryGridComponent(): JSX.Element { - const searchFilterProps: ITextFieldProps = { - placeholder: "Search for Queries", - ariaLabel: "Query filter input", - onChange: this.onFilterInputChange, - componentRef: (queryInput: ITextField) => (this.queryFilter = queryInput), - styles: { - root: { paddingBottom: "12px" }, - field: { fontSize: `${StyleConstants.mediumFontSize}px` } - } - }; - const selectionContainerProps: ISelectionZoneProps = { - selection: this.selection, - selectionMode: SelectionMode.single, - onItemInvoked: (item: Query) => this.props.onQuerySelect(item) - }; - const detailsListProps: IDetailsListProps = { - items: this.state.filteredResults, - columns: this.getColumns(), - isHeaderVisible: false, - setKey: "queryName", - layoutMode: DetailsListLayoutMode.fixedColumns, - selection: this.selection, - selectionMode: SelectionMode.none, - compact: true, - onRenderRow: this.onRenderRow, - styles: { - root: { width: "100%" } - } - }; - - return ( - - - - - - - ); - } - - private renderBannerComponent(): JSX.Element { - const bannerProps: React.ImgHTMLAttributes = { - src: SaveQueryBannerIcon, - alt: "Save query helper banner", - style: { - height: "150px", - width: "310px", - marginTop: "20px", - border: `1px solid ${StyleConstants.BaseMedium}` - } - }; - return ( -
-
- You have not saved any queries yet.

- To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save - Query and follow the prompt in order to save the query. -
- -
- ); - } - - private onFilterInputChange = (event: React.FormEvent, query: string): void => { - if (query) { - const filteredQueries: Query[] = this.state.queries.filter( - (savedQuery: Query) => - savedQuery.queryName.indexOf(query) > -1 || savedQuery.queryName.toLowerCase().indexOf(query) > -1 - ); - this.setState({ - filteredResults: filteredQueries - }); - } else { - // no filter - this.setState({ - filteredResults: this.state.queries - }); - } - }; - - private onRenderRow = (props: IDetailsRowProps): JSX.Element => { - props.styles = { - root: { width: "100%" }, - fields: { - width: "100%", - justifyContent: "space-between" - }, - cell: { - margin: "auto 0" - } - }; - return ; - }; - - private getColumns(): IColumn[] { - return [ - { - key: "Name", - name: "Name", - fieldName: "queryName", - minWidth: 260 - }, - { - key: "Action", - name: "Action", - fieldName: null, - minWidth: 70, - onRender: (query: Query, index: number, column: IColumn) => { - const buttonProps: IButtonProps = { - iconProps: { - iconName: "More", - title: "More", - ariaLabel: "More actions button" - }, - menuIconProps: { - styles: { root: { display: "none" } } - }, - menuProps: { - isBeakVisible: true, - items: [ - { - key: "Open", - text: "Open query", - onClick: (event: React.MouseEvent | React.KeyboardEvent, menuItem: any) => { - this.props.onQuerySelect(query); - } - }, - { - key: "Delete", - text: "Delete query", - onClick: async ( - event: React.MouseEvent | React.KeyboardEvent, - 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, { - databaseAccountName: container && container.databaseAccount().name, - defaultExperience: container && container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: container && container.browseQueriesPane.title() - }); - try { - await this.props.queriesClient.deleteQuery(query); - TelemetryProcessor.traceSuccess( - Action.DeleteSavedQuery, - { - databaseAccountName: container && container.databaseAccount().name, - defaultExperience: container && container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: container && container.browseQueriesPane.title() - }, - startKey - ); - } catch (error) { - TelemetryProcessor.traceFailure( - Action.DeleteSavedQuery, - { - databaseAccountName: container && container.databaseAccount().name, - defaultExperience: container && container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: container && container.browseQueriesPane.title(), - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - startKey - ); - } - await this.fetchSavedQueries(); // get latest state - } - } - } - ] - }, - menuAs: (menuProps: IContextualMenuProps): JSX.Element => { - return ; - } - }; - return ; - } - } - ]; - } - - private async fetchSavedQueries(): Promise { - let queries: Query[]; - try { - queries = (await this.props.queriesClient.getQueries()) as Query[]; - } catch (error) { - console.error(error); - return; - } - queries = queries.map((query: Query) => { - query.key = query.queryName; - return query; - }); - - // we do a deep equality check before setting the state to avoid infinite re-renders - if (!_.isEqual(queries, this.state.queries)) { - this.setState({ - filteredResults: queries, - queries: queries - }); - } - } -} +import * as _ from "underscore"; +import * as React from "react"; +import * as Constants from "../../../Common/Constants"; +import * as DataModels from "../../../Contracts/DataModels"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import { + DetailsList, + DetailsListLayoutMode, + IDetailsListProps, + IDetailsRowProps, + DetailsRow, +} from "office-ui-fabric-react/lib/DetailsList"; +import { FocusZone } from "office-ui-fabric-react/lib/FocusZone"; +import { IconButton, IButtonProps } from "office-ui-fabric-react/lib/Button"; +import { IColumn } from "office-ui-fabric-react/lib/DetailsList"; +import { IContextualMenuProps, ContextualMenu } from "office-ui-fabric-react/lib/ContextualMenu"; +import { + IObjectWithKey, + ISelectionZoneProps, + Selection, + SelectionMode, + SelectionZone, +} from "office-ui-fabric-react/lib/utilities/selection/index"; +import { StyleConstants } from "../../../Common/Constants"; +import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; + +import SaveQueryBannerIcon from "../../../../images/save_query_banner.png"; +import { QueriesClient } from "../../../Common/QueriesClient"; +import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; + +export interface QueriesGridComponentProps { + queriesClient: QueriesClient; + onQuerySelect: (query: DataModels.Query) => void; + containerVisible: boolean; + saveQueryEnabled: boolean; +} + +export interface QueriesGridComponentState { + queries: Query[]; + filteredResults: Query[]; +} + +interface Query extends DataModels.Query, IObjectWithKey { + key: string; +} + +export class QueriesGridComponent extends React.Component { + private selection: Selection; + private queryFilter: ITextField; + + constructor(props: QueriesGridComponentProps) { + super(props); + this.state = { + queries: [], + filteredResults: [], + }; + this.selection = new Selection(); + this.selection.setItems(this.state.filteredResults); + } + + public componentDidUpdate(prevProps: QueriesGridComponentProps, prevState: QueriesGridComponentState): void { + this.selection.setItems( + this.state.filteredResults, + !_.isEqual(prevState.filteredResults, this.state.filteredResults) + ); + this.queryFilter && this.queryFilter.focus(); + const querySetupCompleted: boolean = !prevProps.saveQueryEnabled && this.props.saveQueryEnabled; + const noQueryFiltersApplied: boolean = !this.queryFilter || !this.queryFilter.value; + if (!this.props.containerVisible || !this.props.saveQueryEnabled) { + return; + } else if (noQueryFiltersApplied && (!prevProps.containerVisible || querySetupCompleted)) { + // refresh only when pane is opened or query setup was recently completed + this.fetchSavedQueries(); + } + } + + public render(): JSX.Element { + if (this.state.queries.length === 0) { + return this.renderBannerComponent(); + } + return this.renderQueryGridComponent(); + } + + private renderQueryGridComponent(): JSX.Element { + const searchFilterProps: ITextFieldProps = { + placeholder: "Search for Queries", + ariaLabel: "Query filter input", + onChange: this.onFilterInputChange, + componentRef: (queryInput: ITextField) => (this.queryFilter = queryInput), + styles: { + root: { paddingBottom: "12px" }, + field: { fontSize: `${StyleConstants.mediumFontSize}px` }, + }, + }; + const selectionContainerProps: ISelectionZoneProps = { + selection: this.selection, + selectionMode: SelectionMode.single, + onItemInvoked: (item: Query) => this.props.onQuerySelect(item), + }; + const detailsListProps: IDetailsListProps = { + items: this.state.filteredResults, + columns: this.getColumns(), + isHeaderVisible: false, + setKey: "queryName", + layoutMode: DetailsListLayoutMode.fixedColumns, + selection: this.selection, + selectionMode: SelectionMode.none, + compact: true, + onRenderRow: this.onRenderRow, + styles: { + root: { width: "100%" }, + }, + }; + + return ( + + + + + + + ); + } + + private renderBannerComponent(): JSX.Element { + const bannerProps: React.ImgHTMLAttributes = { + src: SaveQueryBannerIcon, + alt: "Save query helper banner", + style: { + height: "150px", + width: "310px", + marginTop: "20px", + border: `1px solid ${StyleConstants.BaseMedium}`, + }, + }; + return ( +
+
+ You have not saved any queries yet.

+ To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save + Query and follow the prompt in order to save the query. +
+ +
+ ); + } + + private onFilterInputChange = (event: React.FormEvent, query: string): void => { + if (query) { + const filteredQueries: Query[] = this.state.queries.filter( + (savedQuery: Query) => + savedQuery.queryName.indexOf(query) > -1 || savedQuery.queryName.toLowerCase().indexOf(query) > -1 + ); + this.setState({ + filteredResults: filteredQueries, + }); + } else { + // no filter + this.setState({ + filteredResults: this.state.queries, + }); + } + }; + + private onRenderRow = (props: IDetailsRowProps): JSX.Element => { + props.styles = { + root: { width: "100%" }, + fields: { + width: "100%", + justifyContent: "space-between", + }, + cell: { + margin: "auto 0", + }, + }; + return ; + }; + + private getColumns(): IColumn[] { + return [ + { + key: "Name", + name: "Name", + fieldName: "queryName", + minWidth: 260, + }, + { + key: "Action", + name: "Action", + fieldName: null, + minWidth: 70, + onRender: (query: Query, index: number, column: IColumn) => { + const buttonProps: IButtonProps = { + iconProps: { + iconName: "More", + title: "More", + ariaLabel: "More actions button", + }, + menuIconProps: { + styles: { root: { display: "none" } }, + }, + menuProps: { + isBeakVisible: true, + items: [ + { + key: "Open", + text: "Open query", + onClick: (event: React.MouseEvent | React.KeyboardEvent, menuItem: any) => { + this.props.onQuerySelect(query); + }, + }, + { + key: "Delete", + text: "Delete query", + onClick: async ( + event: React.MouseEvent | React.KeyboardEvent, + 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, { + databaseAccountName: container && container.databaseAccount().name, + defaultExperience: container && container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: container && container.browseQueriesPane.title(), + }); + try { + await this.props.queriesClient.deleteQuery(query); + TelemetryProcessor.traceSuccess( + Action.DeleteSavedQuery, + { + databaseAccountName: container && container.databaseAccount().name, + defaultExperience: container && container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: container && container.browseQueriesPane.title(), + }, + startKey + ); + } catch (error) { + TelemetryProcessor.traceFailure( + Action.DeleteSavedQuery, + { + databaseAccountName: container && container.databaseAccount().name, + defaultExperience: container && container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: container && container.browseQueriesPane.title(), + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + await this.fetchSavedQueries(); // get latest state + } + }, + }, + ], + }, + menuAs: (menuProps: IContextualMenuProps): JSX.Element => { + return ; + }, + }; + return ; + }, + }, + ]; + } + + private async fetchSavedQueries(): Promise { + let queries: Query[]; + try { + queries = (await this.props.queriesClient.getQueries()) as Query[]; + } catch (error) { + console.error(error); + return; + } + queries = queries.map((query: Query) => { + query.key = query.queryName; + return query; + }); + + // we do a deep equality check before setting the state to avoid infinite re-renders + if (!_.isEqual(queries, this.state.queries)) { + this.setState({ + filteredResults: queries, + queries: queries, + }); + } + } +} diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx index 76bb303a5..5ac867bc2 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx @@ -1,33 +1,33 @@ -/** - * This adapter is responsible to render the QueriesGrid React component - * If the component signals a change through the callback passed in the properties, it must render the React component when appropriate - * and update any knockout observables passed from the parent. - */ -import * as ko from "knockout"; -import * as React from "react"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { QueriesGridComponent, QueriesGridComponentProps } from "./QueriesGridComponent"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import Explorer from "../../Explorer"; - -export class QueriesGridComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - constructor(private container: Explorer) { - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - const props: QueriesGridComponentProps = { - queriesClient: this.container.queriesClient, - onQuerySelect: this.container.browseQueriesPane.loadSavedQuery, - containerVisible: this.container.browseQueriesPane.visible(), - saveQueryEnabled: this.container.canSaveQueries() - }; - return ; - } - - public forceRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} +/** + * This adapter is responsible to render the QueriesGrid React component + * If the component signals a change through the callback passed in the properties, it must render the React component when appropriate + * and update any knockout observables passed from the parent. + */ +import * as ko from "knockout"; +import * as React from "react"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { QueriesGridComponent, QueriesGridComponentProps } from "./QueriesGridComponent"; +import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; +import Explorer from "../../Explorer"; + +export class QueriesGridComponentAdapter implements ReactAdapter { + public parameters: ko.Observable; + + constructor(private container: Explorer) { + this.parameters = ko.observable(Date.now()); + } + + public renderComponent(): JSX.Element { + const props: QueriesGridComponentProps = { + queriesClient: this.container.queriesClient, + onQuerySelect: this.container.browseQueriesPane.loadSavedQuery, + containerVisible: this.container.browseQueriesPane.visible(), + saveQueryEnabled: this.container.canSaveQueries(), + }; + return ; + } + + public forceRender(): void { + window.requestAnimationFrame(() => this.parameters(Date.now())); + } +} diff --git a/src/Explorer/Controls/RadioSwitchComponent/RadioSwitchComponent.tsx b/src/Explorer/Controls/RadioSwitchComponent/RadioSwitchComponent.tsx index c58d6472a..cc5cbe16b 100644 --- a/src/Explorer/Controls/RadioSwitchComponent/RadioSwitchComponent.tsx +++ b/src/Explorer/Controls/RadioSwitchComponent/RadioSwitchComponent.tsx @@ -28,7 +28,7 @@ export class RadioSwitchComponent extends React.Component this.onSelect(choice)} - onKeyPress={event => this.onKeyPress(event, choice)} + onKeyPress={(event) => this.onKeyPress(event, choice)} > {choice.label} diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index 01d0c8d6d..e1d71690c 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -9,7 +9,7 @@ import ko from "knockout"; import { TtlType, isDirty } from "./SettingsUtils"; import Explorer from "../../Explorer"; jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({ - getIndexTransformationProgress: jest.fn().mockReturnValue(undefined) + getIndexTransformationProgress: jest.fn().mockReturnValue(undefined), })); import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; jest.mock("../../../Common/dataAccess/updateCollection", () => ({ @@ -20,20 +20,20 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({ conflictResolutionPolicy: undefined, changeFeedPolicy: undefined, analyticalStorageTtl: undefined, - geospatialConfig: undefined + geospatialConfig: undefined, } as DataModels.Collection), updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({ id: undefined, shardKey: undefined, indexes: [], - analyticalStorageTtl: undefined - } as MongoDBCollectionResource) + analyticalStorageTtl: undefined, + } as MongoDBCollectionResource), })); import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types"; import Q from "q"; jest.mock("../../../Common/dataAccess/updateOffer", () => ({ - updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer) + updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), })); describe("SettingsComponent", () => { @@ -49,8 +49,8 @@ describe("SettingsComponent", () => { onUpdateTabsButtons: undefined, getPendingNotification: Q.Promise(() => { return; - }) - }) + }), + }), }; it("renders", () => { @@ -71,7 +71,7 @@ describe("SettingsComponent", () => { isScaleSaveable: false, isScaleDiscardable: false, isSubSettingsSaveable: true, - isSubSettingsDiscardable: true + isSubSettingsDiscardable: true, }); wrapper.update(); expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true); @@ -93,7 +93,7 @@ describe("SettingsComponent", () => { manualThroughput: undefined, minimumThroughput: 400, id: "test", - offerReplacePending: false + offerReplacePending: false, }); const props = { ...baseProps }; @@ -106,7 +106,7 @@ describe("SettingsComponent", () => { userCanChangeProvisioningTypes: true, isAutoPilotSelected: true, wasAutopilotOriginallySet: false, - autoPilotThroughput: 1000 + autoPilotThroughput: 1000, }); wrapper.update(); expect(settingsComponentInstance.hasProvisioningTypeChanged()).toEqual(true); @@ -141,7 +141,7 @@ describe("SettingsComponent", () => { onDeleteDatabaseContextMenuClick: undefined, readSettings: undefined, onSettingsClick: undefined, - loadOffer: undefined + loadOffer: undefined, } as ViewModels.Database; newCollection.getDatabase = () => newDatabase; newCollection.offer = ko.observable(undefined); @@ -170,14 +170,14 @@ describe("SettingsComponent", () => { tableEndpoint: undefined, gremlinEndpoint: undefined, cassandraEndpoint: undefined, - enableMultipleWriteLocations: true - } + enableMultipleWriteLocations: true, + }, }); const newCollection = { ...collection }; newCollection.container = newContainer; newCollection.conflictResolutionPolicy = ko.observable({ mode: DataModels.ConflictResolutionMode.Custom, - conflictResolutionProcedure: undefined + conflictResolutionProcedure: undefined, } as DataModels.ConflictResolutionPolicy); const props = { ...baseProps }; @@ -193,7 +193,7 @@ describe("SettingsComponent", () => { wrapper.update(); const settingsComponentInstance = wrapper.instance() as SettingsComponent; settingsComponentInstance.mongoDBCollectionResource = { - id: "id" + id: "id", }; await settingsComponentInstance.onSaveClick(); expect(updateCollection).toBeCalled(); @@ -238,7 +238,7 @@ describe("SettingsComponent", () => { wrapper.setState({ conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins, - conflictResolutionPolicyPath: conflictResolutionPolicyPath + conflictResolutionPolicyPath: conflictResolutionPolicyPath, }); wrapper.update(); const settingsComponentInstance = wrapper.instance() as SettingsComponent; @@ -248,7 +248,7 @@ describe("SettingsComponent", () => { wrapper.setState({ conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom, - conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure + conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure, }); wrapper.update(); conflictResolutionPolicy = settingsComponentInstance.getUpdatedConflictResolutionPolicy(); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 8b901bb19..63e271918 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -16,7 +16,7 @@ import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent"; import { MongoIndexingPolicyComponent, - MongoIndexingPolicyComponentProps + MongoIndexingPolicyComponentProps, } from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent"; import { hasDatabaseSharedThroughput, @@ -30,11 +30,11 @@ import { MongoIndexTypes, parseConflictResolutionMode, parseConflictResolutionProcedure, - getMongoNotification + getMongoNotification, } from "./SettingsUtils"; import { ConflictResolutionComponent, - ConflictResolutionComponentProps + ConflictResolutionComponentProps, } from "./SettingsSubComponents/ConflictResolutionComponent"; import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent"; import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react"; @@ -187,21 +187,21 @@ export class SettingsComponent extends React.Component { return true; - } + }, }; this.discardSettingsChangesButton = { isEnabled: this.isDiscardSettingsButtonEnabled, isVisible: () => { return true; - } + }, }; } @@ -234,7 +234,7 @@ export class SettingsComponent extends React.Component { trace(Action.SettingsV2Discarded, ActionModifiers.Mark, { - message: "Settings Discarded" + message: "Settings Discarded", }); this.setState({ @@ -528,7 +528,7 @@ export class SettingsComponent extends React.Component + content: , }); } tabs.push({ tab: SettingsV2TabTypes.SubSettingsTab, - content: + content: , }); if (this.shouldShowIndexingPolicyEditor) { tabs.push({ tab: SettingsV2TabTypes.IndexingPolicyTab, - content: + content: , }); } else if (this.container.isPreferredApiMongoDB()) { if (isEmpty(this.container.features())) { tabs.push({ tab: SettingsV2TabTypes.IndexingPolicyTab, - content: mongoIndexingPolicyAADError + content: mongoIndexingPolicyAADError, }); } else if (this.container.isEnableMongoCapabilityPresent()) { tabs.push({ tab: SettingsV2TabTypes.IndexingPolicyTab, - content: + content: , }); } } @@ -933,20 +933,20 @@ export class SettingsComponent extends React.Component + content: , }); } const pivotProps: IPivotProps = { onLinkClick: this.onPivotChange, - selectedKey: SettingsV2TabTypes[this.state.selectedTab] + selectedKey: SettingsV2TabTypes[this.state.selectedTab], }; - const pivotItems = tabs.map(tab => { + const pivotItems = tabs.map((tab) => { const pivotItemProps: IPivotItemProps = { itemKey: SettingsV2TabTypes[tab.tab], style: { marginTop: 20 }, - headerText: getTabTitle(tab.tab) + headerText: getTabTitle(tab.tab), }; return ( diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx index a6d010a1a..fec70d644 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx @@ -22,7 +22,7 @@ import { renderMongoIndexTransformationRefreshMessage, ManualEstimatedSpendingDisplayProps, PriceBreakdown, - getRuPriceBreakdown + getRuPriceBreakdown, } from "./SettingsRenderUtils"; class SettingsRenderUtilsTestComponent extends React.Component { @@ -31,15 +31,15 @@ class SettingsRenderUtilsTestComponent extends React.Component { { key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true }, { key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true }, { key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true }, - { key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true } + { key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true }, ]; const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [ { costType: Current Cost, hourly: $ 1.02, daily: $ 24.48, - monthly: $ 744.6 - } + monthly: $ 744.6, + }, ]; const priceBreakdown: PriceBreakdown = { hourlyPrice: 1.02, @@ -47,7 +47,7 @@ class SettingsRenderUtilsTestComponent extends React.Component { monthlyPrice: 744.6, pricePerRu: 0.00051, currency: "RMB", - currencySign: "¥" + currencySign: "¥", }; return ( diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index 1891f5875..2c1ece979 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -9,7 +9,7 @@ import { getMultimasterMultiplier, computeRUUsagePriceHourly, getPricePerRu, - estimatedCostDisclaimer + estimatedCostDisclaimer, } from "../../../Utils/PricingUtils"; import { ITextFieldStyles, @@ -38,7 +38,7 @@ import { DetailsListLayoutMode, IDetailsRowProps, DetailsRow, - IDetailsColumnStyles + IDetailsColumnStyles, } from "office-ui-fabric-react"; import { isDirtyTypes, isDirty } from "./SettingsUtils"; @@ -71,49 +71,49 @@ export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } }; export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { label: { margin: 0, - padding: "2 0 2 0" + padding: "2 0 2 0", }, text: { - fontSize: 12 - } + fontSize: 12, + }, }; export const subComponentStackProps: Partial = { - tokens: { childrenGap: 20 } + tokens: { childrenGap: 20 }, }; export const titleAndInputStackProps: Partial = { - tokens: { childrenGap: 5 } + tokens: { childrenGap: 5 }, }; export const mongoWarningStackProps: Partial = { - tokens: { childrenGap: 5 } + tokens: { childrenGap: 5 }, }; export const mongoErrorMessageStyles: Partial = { root: { marginLeft: 10 } }; export const createAndAddMongoIndexStackProps: Partial = { - tokens: { childrenGap: 5 } + tokens: { childrenGap: 5 }, }; export const addMongoIndexStackProps: Partial = { - tokens: { childrenGap: 10 } + tokens: { childrenGap: 10 }, }; export const checkBoxAndInputStackProps: Partial = { - tokens: { childrenGap: 10 } + tokens: { childrenGap: 10 }, }; export const toolTipLabelStackTokens: IStackTokens = { - childrenGap: 6 + childrenGap: 6, }; export const accordionStackTokens: IStackTokens = { - childrenGap: 10 + childrenGap: 10, }; export const addMongoIndexSubElementsTokens: IStackTokens = { - childrenGap: 20 + childrenGap: 20, }; export const accordionIconStyles: IIconStyles = { root: { paddingTop: 7 } }; @@ -128,30 +128,30 @@ export const transparentDetailsRowStyles: Partial = { root: { selectors: { ":hover": { - background: "transparent" - } - } - } + background: "transparent", + }, + }, + }, }; export const transparentDetailsHeaderStyle: Partial = { root: { selectors: { ":hover": { - background: "transparent" - } - } - } + background: "transparent", + }, + }, + }, }; export const customDetailsListStyles: Partial = { root: { selectors: { ".ms-FocusZone": { - paddingTop: 0 - } - } - } + paddingTop: 0, + }, + }, + }, }; export const separatorStyles: Partial = { @@ -159,16 +159,16 @@ export const separatorStyles: Partial = { { selectors: { "::before": { - background: StyleConstants.BaseMedium - } - } - } - ] + background: StyleConstants.BaseMedium, + }, + }, + }, + ], }; export const messageBarStyles: Partial = { root: { marginTop: "5px", backgroundColor: "white" }, - text: { fontSize: 14 } + text: { fontSize: 14 }, }; export const throughputUnit = "RU/s"; @@ -224,7 +224,7 @@ export const getRuPriceBreakdown = ( requestUnits: throughput, numberOfRegions: numberOfRegions, multimasterEnabled: isMultimaster, - isAutoscale: isAutoscale + isAutoscale: isAutoscale, }); const basePricePerRu: number = isAutoscale ? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster)) @@ -235,7 +235,7 @@ export const getRuPriceBreakdown = ( monthlyPrice: hourlyPrice * hoursInAMonth, pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster), currency: getPriceCurrency(serverId), - currencySign: getCurrencySign(serverId) + currencySign: getCurrencySign(serverId), }; }; @@ -488,10 +488,10 @@ export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes selectors: { ":disabled": { backgroundColor: StyleConstants.BaseMedium, - borderColor: StyleConstants.BaseMediumHigh - } - } - } + borderColor: StyleConstants.BaseMediumHigh, + }, + }, + }, }); export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial => ({ @@ -499,18 +499,18 @@ export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTyp { selectors: { ".ms-ChoiceField-field.is-checked::before": { - borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "" + borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "", }, ".ms-ChoiceField-field.is-checked::after": { - borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "" + borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "", }, ".ms-ChoiceField-wrapper label": { whiteSpace: "nowrap", fontSize: 14, fontFamily: StyleConstants.DataExplorerFont, - padding: "2px 5px" - } - } - } - ] + padding: "2px 5px", + }, + }, + }, + ], }); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx index 367f75c89..4d5f7fd6e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx @@ -25,7 +25,7 @@ describe("ConflictResolutionComponent", () => { }, onConflictResolutionDirtyChange: () => { return; - } + }, }; it("Sproc text field displayed", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx index 74f57105a..2ac3aef2e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx @@ -7,7 +7,7 @@ import { conflictResolutionLwwTooltip, conflictResolutionCustomToolTip, subComponentStackProps, - getChoiceGroupStyles + getChoiceGroupStyles, } from "../SettingsRenderUtils"; import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "office-ui-fabric-react"; import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; @@ -35,9 +35,9 @@ export class ConflictResolutionComponent extends React.Component { automatic: false, indexingMode: "", includedPaths: [], - excludedPaths: [] + excludedPaths: [], }; const baseProps: IndexingPolicyComponentProps = { shouldDiscardIndexingPolicy: false, @@ -27,7 +27,7 @@ describe("IndexingPolicyComponent", () => { return; }, indexTransformationProgress: undefined, - refreshIndexTransformationProgress: () => new Promise(jest.fn()) + refreshIndexTransformationProgress: () => new Promise(jest.fn()), }; it("renders", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index 992a92b47..631880844 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -33,7 +33,7 @@ export class IndexingPolicyComponent extends React.Component< constructor(props: IndexingPolicyComponentProps) { super(props); this.state = { - indexingPolicyContentIsValid: true + indexingPolicyContentIsValid: true, }; } @@ -55,7 +55,7 @@ export class IndexingPolicyComponent extends React.Component< this.createIndexingPolicyEditor(); } else { this.indexingPolicyEditor.updateOptions({ - readOnly: isIndexTransforming(this.props.indexTransformationProgress) + readOnly: isIndexTransforming(this.props.indexTransformationProgress), }); const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel(); const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4); @@ -91,7 +91,7 @@ export class IndexingPolicyComponent extends React.Component< value: value, language: "json", readOnly: isIndexTransforming(this.props.indexTransformationProgress), - ariaLabel: "Indexing Policy" + ariaLabel: "Indexing Policy", }); if (this.indexingPolicyEditor) { const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel(); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx index 2e0da86de..d71a19620 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx @@ -6,7 +6,7 @@ describe("IndexingPolicyRefreshComponent", () => { it("renders", () => { const props: IndexingPolicyRefreshComponentProps = { indexTransformationProgress: 90, - refreshIndexTransformationProgress: () => new Promise(jest.fn()) + refreshIndexTransformationProgress: () => new Promise(jest.fn()), }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx index 62ef35d04..5ff7875f3 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { MessageBar, MessageBarType } from "office-ui-fabric-react"; import { mongoIndexTransformationRefreshingMessage, - renderMongoIndexTransformationRefreshMessage + renderMongoIndexTransformationRefreshMessage, } from "../../SettingsRenderUtils"; import { handleError } from "../../../../../Common/ErrorHandlingUtils"; import { isIndexTransforming } from "../../SettingsUtils"; @@ -23,7 +23,7 @@ export class IndexingPolicyRefreshComponent extends React.Component< constructor(props: IndexingPolicyRefreshComponentProps) { super(props); this.state = { - isRefreshing: false + isRefreshing: false, }; } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.test.tsx index 749825bad..c4df1d1ba 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.test.tsx @@ -15,7 +15,7 @@ describe("AddMongoIndexComponent", () => { }, onDiscard: () => { return; - } + }, }; const wrapper = shallow(); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.tsx index 23d094477..9291d2aa9 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/AddMongoIndexComponent.tsx @@ -7,21 +7,21 @@ import { TextField, Dropdown, IDropdownOption, - ITextField + ITextField, } from "office-ui-fabric-react"; import { addMongoIndexSubElementsTokens, mongoErrorMessageStyles, mongoWarningStackProps, shortWidthDropDownStyles, - shortWidthTextFieldStyles + shortWidthTextFieldStyles, } from "../../SettingsRenderUtils"; import { getMongoIndexTypeText, MongoIndexTypes, MongoNotificationMessage, MongoNotificationType, - MongoWildcardPlaceHolder + MongoWildcardPlaceHolder, } from "../../SettingsUtils"; export interface AddMongoIndexComponentProps { @@ -39,7 +39,7 @@ export class AddMongoIndexComponent extends React.Component ({ text: getMongoIndexTypeText(value), - key: value + key: value, }) ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx index 95ca5eef1..07cee23d7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx @@ -28,7 +28,7 @@ describe("MongoIndexingPolicyComponent", () => { }, onMongoIndexingPolicyDiscardableChange: () => { return; - } + }, }; it("renders", () => { @@ -55,24 +55,24 @@ describe("MongoIndexingPolicyComponent", () => { false, false, true, - sampleWarning + sampleWarning, ], [ { type: MongoNotificationType.Error, message: sampleError } as MongoNotificationMessage, false, false, true, - undefined + undefined, ], [ { type: MongoNotificationType.Error, message: sampleError } as MongoNotificationMessage, true, false, true, - undefined + undefined, ], [undefined, false, true, true, undefined], - [undefined, true, true, true, undefined] + [undefined, true, true, true, undefined], ]; test.each(cases)( @@ -87,7 +87,7 @@ describe("MongoIndexingPolicyComponent", () => { const addMongoIndexProps = { mongoIndex: { key: { keys: ["sampleKey"] } }, type: MongoIndexTypes.Single, - notification: notification + notification: notification, }; let indexesToDrop: number[] = []; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx index 8c20c88e6..506acdee7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx @@ -11,7 +11,7 @@ import { MessageBarType, Spinner, SpinnerSize, - Separator + Separator, } from "office-ui-fabric-react"; import { addMongoIndexStackProps, @@ -23,7 +23,7 @@ import { separatorStyles, indexingPolicynUnsavedWarningMessage, infoAndToolTipTextStyle, - onRenderRow + onRenderRow, } from "../../SettingsRenderUtils"; import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types"; import { @@ -33,7 +33,7 @@ import { MongoNotificationType, getMongoIndexType, getMongoIndexTypeText, - isIndexTransforming + isIndexTransforming, } from "../../SettingsUtils"; import { AddMongoIndexComponent } from "./AddMongoIndexComponent"; import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent"; @@ -71,8 +71,8 @@ export class MongoIndexingPolicyComponent extends React.Component addMongoIndexProps.notification); + const addErrorsExist = !!this.props.indexesToAdd.find((addMongoIndexProps) => addMongoIndexProps.notification); if (addErrorsExist) { return false; @@ -129,7 +129,7 @@ export class MongoIndexingPolicyComponent extends React.Component { const warningMessage = this.props.indexesToAdd.find( - addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning + (addMongoIndexProps) => addMongoIndexProps.notification?.type === MongoNotificationType.Warning )?.notification.message; if (warningMessage) { @@ -172,7 +172,7 @@ export class MongoIndexingPolicyComponent extends React.Component{definition}, type: {getMongoIndexTypeText(type)}, - actionButton: definition === MongoIndexIdField ? <> : this.getActionButton(arrayPosition, isCurrentIndex) + actionButton: definition === MongoIndexIdField ? <> : this.getActionButton(arrayPosition, isCurrentIndex), }; } return mongoIndexDisplayProps; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx index cab9803b7..54b6cc5e4 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx @@ -40,8 +40,8 @@ describe("ScaleComponent", () => { return; }, initialNotification: { - description: `Throughput update for ${targetThroughput} ${throughputUnit}` - } as DataModels.Notification + description: `Throughput update for ${targetThroughput} ${throughputUnit}`, + } as DataModels.Notification, }; it("renders with correct initial notification", () => { @@ -59,12 +59,12 @@ describe("ScaleComponent", () => { autoscaleMaxThroughput: maxThroughput, minimumThroughput: 400, id: "offer", - offerReplacePending: true + offerReplacePending: true, }); const newProps = { ...baseProps, initialNotification: undefined as DataModels.Notification, - collection: newCollection + collection: newCollection, }; wrapper = shallow(); expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true); @@ -95,10 +95,10 @@ describe("ScaleComponent", () => { capabilities: [ { name: Constants.CapabilityNames.EnableAutoScale.toLowerCase(), - description: undefined - } - ] - } + description: undefined, + }, + ], + }, }); const props = { ...baseProps, container: newContainer }; const scaleComponent = new ScaleComponent(props); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 6f46b7cdf..8819024cb 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -12,7 +12,7 @@ import { throughputUnit, getThroughputApplyLongDelayMessage, getThroughputApplyShortDelayMessage, - updateThroughputBeyondLimitWarningMessage + updateThroughputBeyondLimitWarningMessage, } from "../SettingsRenderUtils"; import { hasDatabaseSharedThroughput } from "../SettingsUtils"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx index 55c51e382..9e6b3368b 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx @@ -54,7 +54,7 @@ describe("SubSettingsComponent", () => { }, onSubSettingsDiscardableChange: () => { return; - } + }, }; it("renders", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index 04944804a..1f8133a35 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -9,7 +9,7 @@ import { TtlOn, TtlOff, TtlOnNoDefault, - getSanitizedInputValue + getSanitizedInputValue, } from "../SettingsUtils"; import Explorer from "../../../Explorer"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; @@ -21,7 +21,7 @@ import { titleAndInputStackProps, getChoiceGroupStyles, ttlWarning, - messageBarStyles + messageBarStyles, } from "../SettingsRenderUtils"; import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; @@ -119,7 +119,7 @@ export class SubSettingsComponent extends React.Component { @@ -207,7 +207,7 @@ export class SubSettingsComponent extends React.Component ( @@ -244,7 +244,7 @@ export class SubSettingsComponent extends React.Component ( @@ -260,7 +260,7 @@ export class SubSettingsComponent extends React.Component { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index b52617552..e102edd73 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -2,7 +2,7 @@ import { shallow } from "enzyme"; import React from "react"; import { ThroughputInputAutoPilotV3Component, - ThroughputInputAutoPilotV3Props + ThroughputInputAutoPilotV3Props, } from "./ThroughputInputAutoPilotV3Component"; import * as DataModels from "../../../../../Contracts/DataModels"; @@ -43,7 +43,7 @@ describe("ThroughputInputAutoPilotV3Component", () => { onScaleDiscardableChange: () => { return; }, - getThroughputWarningMessage: () => undefined + getThroughputWarningMessage: () => undefined, }; it("throughput input visible", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 864cc9385..02316edcf 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -16,7 +16,7 @@ import { AutoscaleEstimatedSpendingDisplayProps, PriceBreakdown, getRuPriceBreakdown, - transparentDetailsHeaderStyle + transparentDetailsHeaderStyle, } from "../../SettingsRenderUtils"; import { Text, @@ -29,7 +29,7 @@ import { Link, MessageBar, FontIcon, - IColumn + IColumn, } from "office-ui-fabric-react"; import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils"; @@ -95,7 +95,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< private autoPilotInputMaxValue: number; private options: IChoiceGroupOption[] = [ { key: "true", text: "Autoscale" }, - { key: "false", text: "Manual" } + { key: "false", text: "Manual" }, ]; componentDidMount(): void { @@ -157,7 +157,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< this.state = { spendAckChecked: this.props.spendAckChecked, exceedFreeTierThroughput: - this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400 + this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400, }; this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep; @@ -224,7 +224,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< minWidth: 100, maxWidth: 200, isResizable: true, - styles: transparentDetailsHeaderStyle + styles: transparentDetailsHeaderStyle, }, { key: "minPerMonth", @@ -233,7 +233,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< minWidth: 100, maxWidth: 200, isResizable: true, - styles: transparentDetailsHeaderStyle + styles: transparentDetailsHeaderStyle, }, { key: "maxPerMonth", @@ -242,8 +242,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< minWidth: 100, maxWidth: 200, isResizable: true, - styles: transparentDetailsHeaderStyle - } + styles: transparentDetailsHeaderStyle, + }, ]; const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [ { @@ -257,8 +257,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} - ) - } + ), + }, ]; if (newThroughput) { @@ -288,7 +288,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} - ) + ), }); } @@ -318,7 +318,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< minWidth: 100, maxWidth: 200, isResizable: true, - styles: transparentDetailsHeaderStyle + styles: transparentDetailsHeaderStyle, }, { key: "hourly", @@ -327,7 +327,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< minWidth: 100, maxWidth: 200, isResizable: true, - styles: transparentDetailsHeaderStyle + styles: transparentDetailsHeaderStyle, }, { key: "daily", @@ -336,7 +336,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< minWidth: 100, maxWidth: 200, isResizable: true, - styles: transparentDetailsHeaderStyle + styles: transparentDetailsHeaderStyle, }, { key: "monthly", @@ -345,8 +345,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< minWidth: 100, maxWidth: 200, isResizable: true, - styles: transparentDetailsHeaderStyle - } + styles: transparentDetailsHeaderStyle, + }, ]; const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [ { @@ -365,8 +365,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} - ) - } + ), + }, ]; if (newThroughput) { @@ -403,7 +403,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} - ) + ), }); } @@ -462,7 +462,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< databaseName: this.props.databaseName, collectionName: this.props.collectionName, apiKind: userContext.defaultExperience, - dataExplorerArea: "Scale Tab V2" + dataExplorerArea: "Scale Tab V2", }); }; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ToolTipLabelComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ToolTipLabelComponent.test.tsx index a8501a9bd..b4a0a5471 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ToolTipLabelComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ToolTipLabelComponent.test.tsx @@ -5,7 +5,7 @@ import { ToolTipLabelComponent, ToolTipLabelComponentProps } from "./ToolTipLabe describe("ToolTipLabelComponent", () => { const props: ToolTipLabelComponentProps = { label: "sample tool tip label", - toolTipElement: sample tool tip text + toolTipElement: sample tool tip text, }; it("renders", () => { diff --git a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx index 3783ef102..cd1adbfee 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx @@ -13,7 +13,7 @@ import { getMongoIndexTypeText, SingleFieldText, WildcardText, - isIndexTransforming + isIndexTransforming, } from "./SettingsUtils"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; @@ -45,7 +45,7 @@ describe("SettingsUtils", () => { onDeleteDatabaseContextMenuClick: undefined, readSettings: undefined, onSettingsClick: undefined, - loadOffer: undefined + loadOffer: undefined, } as ViewModels.Database; }; newCollection.offer(undefined); @@ -67,7 +67,7 @@ describe("SettingsUtils", () => { automatic: true, indexingMode: "consistent", includedPaths: [], - excludedPaths: [] + excludedPaths: [], } as DataModels.IndexingPolicy; it("works on all types", () => { diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index daa9f5e85..6deee03b1 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -15,23 +15,23 @@ export const WildcardText = "Wildcard"; export enum ChangeFeedPolicyState { Off = "Off", - On = "On" + On = "On", } export enum TtlType { Off = "off", On = "on", - OnNoDefault = "on-nodefault" + OnNoDefault = "on-nodefault", } export enum GeospatialConfigType { Geography = "Geography", - Geometry = "Geometry" + Geometry = "Geometry", } export enum MongoIndexTypes { Single = "Single", - Wildcard = "Wildcard" + Wildcard = "Wildcard", } export interface AddMongoIndexProps { @@ -44,7 +44,7 @@ export enum SettingsV2TabTypes { ScaleTab, ConflictResolutionTab, SubSettingsTab, - IndexingPolicyTab + IndexingPolicyTab, } export interface IsComponentDirtyResult { @@ -54,7 +54,7 @@ export interface IsComponentDirtyResult { export enum MongoNotificationType { Warning = "Warning", - Error = "Error" + Error = "Error", } export interface MongoNotificationMessage { @@ -155,19 +155,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes) if (description && !type) { return { type: MongoNotificationType.Warning, - message: "Please select a type for each index." + message: "Please select a type for each index.", }; } if (type && (!description || description.trim().length === 0)) { return { type: MongoNotificationType.Error, - message: "Please enter a field name." + message: "Please enter a field name.", }; } else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) { return { type: MongoNotificationType.Error, - message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder + message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder, }; } diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index 6a318bf80..486a5259f 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -15,7 +15,7 @@ export const collection = ({ automatic: true, indexingMode: "default", includedPaths: [], - excludedPaths: [] + excludedPaths: [], }), uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy, usageSizeInKB: ko.observable(100), @@ -24,7 +24,7 @@ export const collection = ({ manualThroughput: 10000, minimumThroughput: 6000, id: "offer", - offerReplacePending: false + offerReplacePending: false, }), conflictResolutionPolicy: ko.observable( {} as DataModels.ConflictResolutionPolicy @@ -37,10 +37,10 @@ export const collection = ({ partitionKey: { paths: [], kind: "hash", - version: 2 + version: 2, }, partitionKeyProperty: "partitionKey", readSettings: () => { return; - } + }, } as unknown) as ViewModels.Collection; diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx index 155cf2dcc..000e8b905 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx @@ -10,8 +10,8 @@ describe("SmartUiComponent", () => { message: "Start at $24/mo per database", link: { href: "https://aka.ms/azure-cosmos-db-pricing", - text: "More Details" - } + text: "More Details", + }, }, children: [ { @@ -24,8 +24,8 @@ describe("SmartUiComponent", () => { max: 500, step: 10, defaultValue: 400, - uiType: UiType.Spinner - } + uiType: UiType.Spinner, + }, }, { id: "throughput2", @@ -37,8 +37,8 @@ describe("SmartUiComponent", () => { max: 500, step: 10, defaultValue: 400, - uiType: UiType.Slider - } + uiType: UiType.Slider, + }, }, { id: "throughput3", @@ -51,16 +51,16 @@ describe("SmartUiComponent", () => { step: 10, defaultValue: 400, uiType: UiType.Spinner, - errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'" - } + errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'", + }, }, { id: "containerId", input: { label: "Container id", dataFieldName: "containerId", - type: "string" - } + type: "string", + }, }, { id: "analyticalStore", @@ -70,8 +70,8 @@ describe("SmartUiComponent", () => { falseLabel: "Disabled", defaultValue: true, dataFieldName: "analyticalStore", - type: "boolean" - } + type: "boolean", + }, }, { id: "database", @@ -82,20 +82,20 @@ describe("SmartUiComponent", () => { choices: [ { label: "Database 1", key: "db1" }, { label: "Database 2", key: "db2" }, - { label: "Database 3", key: "db3" } + { label: "Database 3", key: "db3" }, ], - defaultKey: "db2" - } - } - ] - } + defaultKey: "db2", + }, + }, + ], + }, }; it("should render", async () => { const wrapper = shallow( ); - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx index 619c09bbc..4befeed08 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx @@ -23,7 +23,7 @@ export type InputTypeValue = "number" | "string" | "boolean" | "object"; export enum UiType { Spinner = "Spinner", - Slider = "Slider" + Slider = "Slider", } export type ChoiceItem = { label: string; key: string }; @@ -101,13 +101,13 @@ export class SmartUiComponent extends React.Component
@@ -200,7 +200,7 @@ export class SmartUiComponent extends React.Component this.onValidate(input, newValue, props.min, props.max)} - onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)} - onDecrement={newValue => this.onDecrement(input, newValue, props.step, props.min)} + onValidate={(newValue) => this.onValidate(input, newValue, props.min, props.max)} + onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)} + onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)} labelPosition={Position.top} styles={{ label: { ...SmartUiComponent.labelStyle, - fontWeight: 600 - } + fontWeight: 600, + }, }} /> {this.state.errors.has(dataFieldName) && ( @@ -233,13 +233,13 @@ export class SmartUiComponent extends React.Component this.props.onInputChange(input, newValue)} + onChange={(newValue) => this.props.onInputChange(input, newValue)} styles={{ titleLabel: { ...SmartUiComponent.labelStyle, - fontWeight: 600 + fontWeight: 600, }, - valueLabel: SmartUiComponent.labelStyle + valueLabel: SmartUiComponent.labelStyle, }} />
@@ -264,13 +264,13 @@ export class SmartUiComponent extends React.Component this.props.onInputChange(input, false) + onSelect: () => this.props.onInputChange(input, false), }, { label: input.trueLabel, key: "true", - onSelect: () => this.props.onInputChange(input, true) - } + onSelect: () => this.props.onInputChange(input, true), + }, ]} selectedKey={selectedKey} /> @@ -288,16 +288,16 @@ export class SmartUiComponent extends React.Component this.props.onInputChange(input, item.key.toString())} placeholder={placeholder} - options={choices.map(c => ({ + options={choices.map((c) => ({ key: c.key, - text: c.label + text: c.label, }))} styles={{ label: { ...SmartUiComponent.labelStyle, - fontWeight: 600 + fontWeight: 600, }, - dropdown: SmartUiComponent.labelStyle + dropdown: SmartUiComponent.labelStyle, }} /> ); @@ -334,7 +334,7 @@ export class SmartUiComponent extends React.Component - {node.children && node.children.map(child =>
{this.renderNode(child)}
)} + {node.children && node.children.map((child) =>
{this.renderNode(child)}
)} ); } diff --git a/src/Explorer/Controls/Tabs/TabComponent.tsx b/src/Explorer/Controls/Tabs/TabComponent.tsx index 6e1bf5d09..f1fd76bf8 100644 --- a/src/Explorer/Controls/Tabs/TabComponent.tsx +++ b/src/Explorer/Controls/Tabs/TabComponent.tsx @@ -58,7 +58,7 @@ export class TabComponent extends React.Component { as="span" className={className} role="presentation" - onActivated={e => this.setActiveTab(index)} + onActivated={(e) => this.setActiveTab(index)} aria-label={`Select tab: ${tab.title}`} > {tab.title} diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts index 276177bd3..c15b9df3a 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts @@ -204,13 +204,13 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel { this.label = options.label || ko.observable(); this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable(true); this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable(false); - this.isAutoPilotSelected.subscribe(value => { + this.isAutoPilotSelected.subscribe((value) => { TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, { changedSelectedValueTo: value ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff, databaseAccountName: userContext.databaseAccount?.name, subscriptionId: userContext.subscriptionId, apiKind: userContext.defaultExperience, - dataExplorerArea: "Scale Tab V1" + dataExplorerArea: "Scale Tab V1", }); }); @@ -310,5 +310,5 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel { export const ThroughputInputComponentAutoPilotV3 = { viewModel: ThroughputInputViewModel, - template: ThroughputInputComponentAutoscaleV3 + template: ThroughputInputComponentAutoscaleV3, }; diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html index 8ec328aba..8b32134bd 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoscaleV3.html @@ -163,7 +163,7 @@
- Warning + Warning
diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx index 875d6406d..1138f0b45 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx @@ -4,18 +4,18 @@ import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent"; const buildChildren = (): TreeNode[] => { const grandChild11: TreeNode = { - label: "ZgrandChild11" + label: "ZgrandChild11", }; const grandChild12: TreeNode = { - label: "AgrandChild12" + label: "AgrandChild12", }; const child1: TreeNode = { label: "Bchild1", - children: [grandChild11, grandChild12] + children: [grandChild11, grandChild12], }; const child2: TreeNode = { - label: "2child2" + label: "2child2", }; return [child1, child2]; @@ -23,28 +23,28 @@ const buildChildren = (): TreeNode[] => { const buildChildren2 = (): TreeNode[] => { const grandChild11: TreeNode = { - label: "ZgrandChild11" + label: "ZgrandChild11", }; const grandChild12: TreeNode = { - label: "AgrandChild12" + label: "AgrandChild12", }; const child1: TreeNode = { - label: "aChild" + label: "aChild", }; const child2: TreeNode = { label: "bchild", - children: [grandChild11, grandChild12] + children: [grandChild11, grandChild12], }; const child3: TreeNode = { - label: "cchild" + label: "cchild", }; const child4: TreeNode = { label: "dchild", - children: [grandChild11, grandChild12] + children: [grandChild11, grandChild12], }; return [child1, child2, child3, child4]; @@ -54,12 +54,12 @@ describe("TreeComponent", () => { it("renders a simple tree", () => { const root = { label: "root", - children: buildChildren() + children: buildChildren(), }; const props = { rootNode: root, - className: "tree" + className: "tree", }; const wrapper = shallow(); @@ -78,8 +78,8 @@ describe("TreeNodeComponent", () => { label: "menuLabel", onClick: undefined, iconSrc: undefined, - isDisabled: true - } + isDisabled: true, + }, ], iconSrc: undefined, isExpanded: true, @@ -90,13 +90,13 @@ describe("TreeNodeComponent", () => { isSelected: undefined, onClick: undefined, onExpanded: undefined, - onCollapsed: undefined + onCollapsed: undefined, }; const props = { node, generation: 12, - paddingLeft: 23 + paddingLeft: 23, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -106,12 +106,12 @@ describe("TreeNodeComponent", () => { const node: TreeNode = { label: "label", children: buildChildren(), - isExpanded: true + isExpanded: true, }; const props = { node, generation: 2, - paddingLeft: 9 + paddingLeft: 9, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -121,12 +121,12 @@ describe("TreeNodeComponent", () => { const node: TreeNode = { label: "label", children: buildChildren(), - isAlphaSorted: false + isAlphaSorted: false, }; const props = { node, generation: 2, - paddingLeft: 9 + paddingLeft: 9, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -148,13 +148,13 @@ describe("TreeNodeComponent", () => { isSelected: undefined, onClick: undefined, onExpanded: undefined, - onCollapsed: undefined + onCollapsed: undefined, }; const props = { node, generation: 12, - paddingLeft: 23 + paddingLeft: 23, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -164,13 +164,13 @@ describe("TreeNodeComponent", () => { const node: TreeNode = { label: "label", children: [], - isExpanded: true + isExpanded: true, }; const props = { node, generation: 2, - paddingLeft: 9 + paddingLeft: 9, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx index 1ceef8320..23cb08fcc 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx @@ -12,7 +12,7 @@ import { IconButton, IButtonStyles } from "office-ui-fabric-react/lib/Button"; import { DirectionalHint, IContextualMenuItemProps, - IContextualMenuProps + IContextualMenuProps, } from "office-ui-fabric-react/lib/ContextualMenu"; import TriangleDownIcon from "../../../../images/Triangle-down.svg"; @@ -89,7 +89,7 @@ export class TreeNodeComponent extends React.Component node.children); - const leaves: TreeNode[] = treeNode.children.filter(node => !node.children); + const parents: TreeNode[] = treeNode.children.filter((node) => node.children); + const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children); if (treeNode.isAlphaSorted) { parents.sort(compareFct); @@ -235,7 +235,7 @@ export class TreeNodeComponent extends React.Component = { - rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` } + rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` }, }; return ( @@ -246,7 +246,7 @@ export class TreeNodeComponent extends React.Component e.target.dispatchEvent(TreeNodeComponent.createClickEvent())} + onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())} > {props.item.onRenderIcon()} { menuItem.onClick(); TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, { - label: menuItem.label + label: menuItem.label, }); }, - onRenderIcon: (props: any) => - })) + onRenderIcon: (props: any) => , + })), }} styles={buttonStyles} /> diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts index 838e829a4..c98227256 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts @@ -41,26 +41,26 @@ describe("ContainerSampleGenerator", () => { data: [ { firstname: "Eva", - age: 44 + age: 44, }, { firstname: "Véronique", - age: 50 + age: 50, }, { firstname: "亜妃子", - age: 5 + age: 5, }, { firstname: "John", - age: 23 - } - ] + age: 23, + }, + ], }; const collection = { id: ko.observable(sampleCollectionId) } as ViewModels.Collection; const database = { id: ko.observable(sampleDatabaseId), - collections: ko.observableArray([collection]) + collections: ko.observableArray([collection]), } as ViewModels.Database; database.findCollectionWithId = () => collection; @@ -87,9 +87,9 @@ describe("ContainerSampleGenerator", () => { documentEndpoint: "bar", gremlinEndpoint: "foo", tableEndpoint: "foo", - cassandraEndpoint: "foo" - } - } + cassandraEndpoint: "foo", + }, + }, }); const sampleCollectionId = "SampleCollection"; @@ -102,13 +102,13 @@ describe("ContainerSampleGenerator", () => { createNewDatabase: true, collectionId: sampleCollectionId, data: [ - "g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)" - ] + "g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)", + ], }; const collection = { id: ko.observable(sampleCollectionId) } as ViewModels.Collection; const database = { id: ko.observable(sampleDatabaseId), - collections: ko.observableArray([collection]) + collections: ko.observableArray([collection]), } as ViewModels.Database; database.findCollectionWithId = () => collection; collection.databaseId = database.id(); diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index b115a98b1..125a57973 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -54,7 +54,7 @@ export class ContainerSampleGenerator { private async createContainerAsync(): Promise { const createRequest: DataModels.CreateCollectionParams = { - ...this.sampleDataFile + ...this.sampleDataFile, }; await createCollection(createRequest); @@ -87,16 +87,16 @@ export class ContainerSampleGenerator { databaseId: databaseId, collectionId: collection.id(), masterKey: userContext.masterKey || "", - maxResultSize: 100 + maxResultSize: 100, }); await queries - .map(query => () => gremlinClient.execute(query)) + .map((query) => () => gremlinClient.execute(query)) .reduce((previous, current) => previous.then(current), Promise.resolve()); } else { // For SQL all queries are executed at the same time await Promise.all( - this.sampleDataFile.data.map(async doc => { + this.sampleDataFile.data.map(async (doc) => { try { await createDocument(collection, doc); } catch (error) { diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index b7905dbe5..634354c8d 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -13,7 +13,7 @@ describe("DataSampleUtils", () => { const collection = { id: ko.observable(sampleCollectionId) } as Collection; const database = { id: ko.observable(sampleDatabaseId), - collections: ko.observableArray([collection]) + collections: ko.observableArray([collection]), } as Database; const explorer = {} as Explorer; explorer.nonSystemDatabases = ko.computed(() => [database]); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index dcf62498e..81e191048 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -26,7 +26,7 @@ export class DataSamplesUtil { await generator .createSampleContainerAsync() - .catch(error => + .catch((error) => NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Error creating sample container: ${error}`) ); const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; @@ -48,10 +48,10 @@ export class DataSamplesUtil { * @param containerDatabases */ public hasContainer(databaseName: string, containerName: string, containerDatabases: ViewModels.Database[]): boolean { - const filteredDatabases = containerDatabases.filter(database => database.id() === databaseName); + const filteredDatabases = containerDatabases.filter((database) => database.id() === databaseName); return ( filteredDatabases.length > 0 && - filteredDatabases[0].collections().filter(collection => collection.id() === containerName).length > 0 + filteredDatabases[0].collections().filter((collection) => collection.id() === containerName).length > 0 ); } diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 4aaa767d9..04e36e87d 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -1,3073 +1,3070 @@ -import * as ComponentRegisterer from "./ComponentRegisterer"; -import * as Constants from "../Common/Constants"; -import * as DataModels from "../Contracts/DataModels"; -import * as ko from "knockout"; -import * as MostRecentActivity from "./MostRecentActivity/MostRecentActivity"; -import * as path from "path"; -import * as SharedConstants from "../Shared/Constants"; -import * as ViewModels from "../Contracts/ViewModels"; -import _ from "underscore"; -import AddCollectionPane from "./Panes/AddCollectionPane"; -import AddDatabasePane from "./Panes/AddDatabasePane"; -import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; -import AuthHeadersUtil from "../Platform/Hosted/Authorization"; -import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; -import Database from "./Tree/Database"; -import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; -import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; -import { readCollection } from "../Common/dataAccess/readCollection"; -import { readDatabases } from "../Common/dataAccess/readDatabases"; -import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; -import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; -import GraphStylingPane from "./Panes/GraphStylingPane"; -import hasher from "hasher"; -import NewVertexPane from "./Panes/NewVertexPane"; -import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; -import Q from "q"; -import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; -import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; -import TerminalTab from "./Tabs/TerminalTab"; -import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; -import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts"; -import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager"; -import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; -import { AuthType } from "../AuthType"; -import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; -import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; -import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; -import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import { configContext, Platform, updateConfigContext } from "../ConfigContext"; -import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; -import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; -import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; -import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter"; -import { DialogProps, TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent"; -import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane"; -import { ExplorerMetrics } from "../Common/Constants"; -import { ExplorerSettings } from "../Shared/ExplorerSettings"; -import { FileSystemUtil } from "./Notebook/FileSystemUtil"; -import { handleOpenAction } from "./OpenActions"; -import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; -import { IGalleryItem } from "../Juno/JunoClient"; -import { LoadQueryPane } from "./Panes/LoadQueryPane"; -import * as Logger from "../Common/Logger"; -import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler"; -import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; -import { NotebookUtil } from "./Notebook/NotebookUtil"; -import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; -import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter"; -import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; -import { QueriesClient } from "../Common/QueriesClient"; -import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; -import { RenewAdHocAccessPane } from "./Panes/RenewAdHocAccessPane"; -import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; -import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; -import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; -import { RouteHandler } from "../RouteHandlers/RouteHandler"; -import { SaveQueryPane } from "./Panes/SaveQueryPane"; -import { SettingsPane } from "./Panes/SettingsPane"; -import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; -import { SplashScreenComponentAdapter } from "./SplashScreen/SplashScreenComponentApdapter"; -import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"; -import { StringInputPane } from "./Panes/StringInputPane"; -import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; -import { TabsManager } from "./Tabs/TabsManager"; -import { UploadFilePane } from "./Panes/UploadFilePane"; -import { UploadItemsPane } from "./Panes/UploadItemsPane"; -import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter"; -import { ReactAdapter } from "../Bindings/ReactBindingHandler"; -import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils"; -import UserDefinedFunction from "./Tree/UserDefinedFunction"; -import StoredProcedure from "./Tree/StoredProcedure"; -import Trigger from "./Tree/Trigger"; -import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; -import TabsBase from "./Tabs/TabsBase"; -import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; -import { updateUserContext, userContext } from "../UserContext"; -import { stringToBlob } from "../Utils/BlobUtils"; -import { IChoiceGroupProps } from "office-ui-fabric-react"; -import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils"; -import { SubscriptionType } from "../Contracts/SubscriptionType"; -import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter"; -import { SelfServeType } from "../SelfServe/SelfServeUtils"; -import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter"; - -BindingHandlersRegisterer.registerBindingHandlers(); -// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import -var tmp = ComponentRegisterer; - -enum ShareAccessToggleState { - ReadWrite, - Read -} - -interface AdHocAccessData { - readWriteUrl: string; - readUrl: string; -} - -export default class Explorer { - public flight: ko.Observable = ko.observable( - SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight - ); - - public addCollectionText: ko.Observable; - public addDatabaseText: ko.Observable; - public collectionTitle: ko.Observable; - public deleteCollectionText: ko.Observable; - public deleteDatabaseText: ko.Observable; - public collectionTreeNodeAltText: ko.Observable; - public refreshTreeTitle: ko.Observable; - public hasWriteAccess: ko.Observable; - public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; - - public databaseAccount: ko.Observable; - public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; - public subscriptionType: ko.Observable; - public defaultExperience: ko.Observable; - public isPreferredApiDocumentDB: ko.Computed; - public isPreferredApiCassandra: ko.Computed; - public isPreferredApiMongoDB: ko.Computed; - public isPreferredApiGraph: ko.Computed; - public isPreferredApiTable: ko.Computed; - public isFixedCollectionWithSharedThroughputSupported: ko.Computed; - public isEnableMongoCapabilityPresent: ko.Computed; - public isServerlessEnabled: ko.Computed; - public isAccountReady: ko.Observable; - public selfServeType: ko.Observable; - public canSaveQueries: ko.Computed; - public features: ko.Observable; - public serverId: ko.Observable; - public isTryCosmosDBSubscription: ko.Observable; - public queriesClient: QueriesClient; - public tableDataClient: TableDataClient; - public splitter: Splitter; - public mostRecentActivity: MostRecentActivity.MostRecentActivity; - - // Notification Console - public notificationConsoleData: ko.ObservableArray; - public isNotificationConsoleExpanded: ko.Observable; - - // Panes - public contextPanes: ContextualPaneBase[]; - - // Resource Tree - public databases: ko.ObservableArray; - public nonSystemDatabases: ko.Computed; - public selectedDatabaseId: ko.Computed; - public selectedCollectionId: ko.Computed; - public isLeftPaneExpanded: ko.Observable; - public selectedNode: ko.Observable; - public isRefreshingExplorer: ko.Observable; - private resourceTree: ResourceTreeAdapter; - private selfServeComponentAdapter: SelfServeComponentAdapter; - - // Resource Token - public resourceTokenDatabaseId: ko.Observable; - public resourceTokenCollectionId: ko.Observable; - public resourceTokenCollection: ko.Observable; - public resourceTokenPartitionKey: ko.Observable; - public isAuthWithResourceToken: ko.Observable; - public isResourceTokenCollectionNodeSelected: ko.Computed; - private resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; - - // Tabs - public isTabsContentExpanded: ko.Observable; - public galleryTab: any; - public notebookViewerTab: any; - public tabsManager: TabsManager; - - // Contextual panes - public addDatabasePane: AddDatabasePane; - public addCollectionPane: AddCollectionPane; - public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane; - public deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane; - public graphStylingPane: GraphStylingPane; - public addTableEntityPane: AddTableEntityPane; - public editTableEntityPane: EditTableEntityPane; - public tableColumnOptionsPane: TableColumnOptionsPane; - public querySelectPane: QuerySelectPane; - public newVertexPane: NewVertexPane; - public cassandraAddCollectionPane: CassandraAddCollectionPane; - public settingsPane: SettingsPane; - public executeSprocParamsPane: ExecuteSprocParamsPane; - public renewAdHocAccessPane: RenewAdHocAccessPane; - public uploadItemsPane: UploadItemsPane; - public uploadItemsPaneAdapter: UploadItemsPaneAdapter; - public loadQueryPane: LoadQueryPane; - public saveQueryPane: ContextualPaneBase; - public browseQueriesPane: BrowseQueriesPane; - public uploadFilePane: UploadFilePane; - public stringInputPane: StringInputPane; - public setupNotebooksPane: SetupNotebooksPane; - public gitHubReposPane: ContextualPaneBase; - public publishNotebookPaneAdapter: ReactAdapter; - public copyNotebookPaneAdapter: ReactAdapter; - - // features - public isGalleryPublishEnabled: ko.Computed; - public isLinkInjectionEnabled: ko.Computed; - public isGitHubPaneEnabled: ko.Observable; - public isPublishNotebookPaneEnabled: ko.Observable; - public isCopyNotebookPaneEnabled: ko.Observable; - public isHostedDataExplorerEnabled: ko.Computed; - public isRightPanelV2Enabled: ko.Computed; - public isMongoIndexingEnabled: ko.Observable; - public canExceedMaximumValue: ko.Computed; - public isAutoscaleDefaultEnabled: ko.Observable; - - public shouldShowShareDialogContents: ko.Observable; - public shareAccessData: ko.Observable; - public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise; - public renewTokenError: ko.Observable; - public tokenForRenewal: ko.Observable; - public shareAccessToggleState: ko.Observable; - public shareAccessUrl: ko.Observable; - public shareUrlCopyHelperText: ko.Observable; - public shareTokenCopyHelperText: ko.Observable; - public shouldShowDataAccessExpiryDialog: ko.Observable; - public shouldShowContextSwitchPrompt: ko.Observable; - public isSchemaEnabled: ko.Computed; - - // Notebooks - public isNotebookEnabled: ko.Observable; - public isNotebooksEnabledForAccount: ko.Observable; - public notebookServerInfo: ko.Observable; - public notebookWorkspaceManager: NotebookWorkspaceManager; - public sparkClusterConnectionInfo: ko.Observable; - public isSparkEnabled: ko.Observable; - public isSparkEnabledForAccount: ko.Observable; - public arcadiaToken: ko.Observable; - public arcadiaWorkspaces: ko.ObservableArray; - public hasStorageAnalyticsAfecFeature: ko.Observable; - public isSynapseLinkUpdating: ko.Observable; - public memoryUsageInfo: ko.Observable; - public notebookManager?: any; // This is dynamically loaded - - private _panes: ContextualPaneBase[] = []; - private _importExplorerConfigComplete: boolean = false; - private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = database => false; - private _isInitializingNotebooks: boolean; - private _isInitializingSparkConnectionInfo: boolean; - private notebookBasePath: ko.Observable; - private _arcadiaManager: ArcadiaResourceManager; - private notebookToImport: { - name: string; - content: string; - }; - - // React adapters - private commandBarComponentAdapter: CommandBarComponentAdapter; - private splashScreenAdapter: SplashScreenComponentAdapter; - private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter; - private dialogComponentAdapter: DialogComponentAdapter; - private _dialogProps: ko.Observable; - private addSynapseLinkDialog: DialogComponentAdapter; - private _addSynapseLinkDialogProps: ko.Observable; - private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter; - - private static readonly MaxNbDatabasesToAutoExpand = 5; - - constructor() { - const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { - dataExplorerArea: Constants.Areas.ResourceTree - }); - this.addCollectionText = ko.observable("New Collection"); - this.addDatabaseText = ko.observable("New Database"); - this.hasWriteAccess = ko.observable(true); - this.collectionTitle = ko.observable("Collections"); - this.collectionTreeNodeAltText = ko.observable("Collection"); - this.deleteCollectionText = ko.observable("Delete Collection"); - this.deleteDatabaseText = ko.observable("Delete Database"); - this.refreshTreeTitle = ko.observable("Refresh collections"); - - this.databaseAccount = ko.observable(); - this.subscriptionType = ko.observable(SharedConstants.CollectionCreation.DefaultSubscriptionType); - let firstInitialization = true; - this.isRefreshingExplorer = ko.observable(true); - this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { - if (!isRefreshing && firstInitialization) { - // set focus on first element - firstInitialization = false; - try { - document.getElementById("createNewContainerCommandButton").parentElement.parentElement.focus(); - } catch (e) { - Logger.logWarning( - "getElementById('createNewContainerCommandButton') failed to find element", - "Explorer/this.isRefreshingExplorer.subscribe" - ); - } - } - }); - this.isAccountReady = ko.observable(false); - this.selfServeType = ko.observable(undefined); - this._isInitializingNotebooks = false; - this._isInitializingSparkConnectionInfo = false; - this.arcadiaToken = ko.observable(); - this.arcadiaToken.subscribe((token: string) => { - if (token) { - const notebookTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2); - (notebookTabs || []).forEach((tab: NotebookV2Tab) => { - tab.reconfigureServiceEndpoints(); - }); - } - }); - this.isNotebooksEnabledForAccount = ko.observable(false); - this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); - this.isSparkEnabledForAccount = ko.observable(false); - this.isSparkEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); - this.hasStorageAnalyticsAfecFeature = ko.observable(false); - this.hasStorageAnalyticsAfecFeature.subscribe((enabled: boolean) => this.refreshCommandBarButtons()); - this.isSynapseLinkUpdating = ko.observable(false); - this.isAccountReady.subscribe(async (isAccountReady: boolean) => { - if (isAccountReady) { - this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); - RouteHandler.getInstance().initHandler(); - this.notebookWorkspaceManager = new NotebookWorkspaceManager(); - this.arcadiaWorkspaces = ko.observableArray(); - this._arcadiaManager = new ArcadiaResourceManager(); - this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered => - this.hasStorageAnalyticsAfecFeature(isRegistered) - ); - Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then( - async () => { - this.isNotebookEnabled( - !this.isAuthWithResourceToken() && - ((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) || - this.isFeatureEnabled(Constants.Features.enableNotebooks)) - ); - - TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { - isNotebookEnabled: this.isNotebookEnabled(), - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }); - - if (this.isNotebookEnabled()) { - await this.initNotebooks(this.databaseAccount()); - const workspaces = await this._getArcadiaWorkspaces(); - this.arcadiaWorkspaces(workspaces); - } else if (this.notebookToImport) { - // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane - this._openSetupNotebooksPaneForQuickstart(); - } - - this.isSparkEnabled( - (this.isNotebookEnabled() && - this.isSparkEnabledForAccount() && - this.arcadiaWorkspaces() && - this.arcadiaWorkspaces().length > 0) || - this.isFeatureEnabled(Constants.Features.enableSpark) - ); - if (this.isSparkEnabled()) { - const pollArcadiaTokenRefresh = async () => { - this.arcadiaToken(await this.getArcadiaToken()); - setTimeout(() => pollArcadiaTokenRefresh(), this.getTokenRefreshInterval(this.arcadiaToken())); - }; - await pollArcadiaTokenRefresh(); - } - } - ); - } - }); - this.memoryUsageInfo = ko.observable(); - - this.features = ko.observable(); - this.serverId = ko.observable(); - this.queriesClient = new QueriesClient(this); - this.isTryCosmosDBSubscription = ko.observable(false); - - this.resourceTokenDatabaseId = ko.observable(); - this.resourceTokenCollectionId = ko.observable(); - this.resourceTokenCollection = ko.observable(); - this.resourceTokenPartitionKey = ko.observable(); - this.isAuthWithResourceToken = ko.observable(false); - - this.shareAccessData = ko.observable({ - readWriteUrl: undefined, - readUrl: undefined - }); - this.tokenForRenewal = ko.observable(""); - this.renewTokenError = ko.observable(""); - this.shareAccessUrl = ko.observable(); - this.shareUrlCopyHelperText = ko.observable("Click to copy"); - this.shareTokenCopyHelperText = ko.observable("Click to copy"); - this.shareAccessToggleState = ko.observable(ShareAccessToggleState.ReadWrite); - this.shareAccessToggleState.subscribe((toggleState: ShareAccessToggleState) => { - if (toggleState === ShareAccessToggleState.ReadWrite) { - this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readWriteUrl); - } else { - this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readUrl); - } - }); - this.shouldShowShareDialogContents = ko.observable(false); - this.shouldShowDataAccessExpiryDialog = ko.observable(false); - this.shouldShowContextSwitchPrompt = ko.observable(false); - this.isGalleryPublishEnabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableGalleryPublish) - ); - this.isLinkInjectionEnabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableLinkInjection) - ); - this.isGitHubPaneEnabled = ko.observable(false); - this.isMongoIndexingEnabled = ko.observable(false); - this.isPublishNotebookPaneEnabled = ko.observable(false); - this.isCopyNotebookPaneEnabled = ko.observable(false); - - this.canExceedMaximumValue = ko.computed(() => - this.isFeatureEnabled(Constants.Features.canExceedMaximumValue) - ); - - this.isSchemaEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSchema)); - this.isNotificationConsoleExpanded = ko.observable(false); - - this.isAutoscaleDefaultEnabled = ko.observable(false); - - this.databases = ko.observableArray(); - this.canSaveQueries = ko.computed(() => { - const savedQueriesDatabase: ViewModels.Database = _.find( - this.databases(), - (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName - ); - if (!savedQueriesDatabase) { - return false; - } - const savedQueriesCollection: ViewModels.Collection = - savedQueriesDatabase && - _.find( - savedQueriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName - ); - if (!savedQueriesCollection) { - return false; - } - return true; - }); - this.isLeftPaneExpanded = ko.observable(true); - this.selectedNode = ko.observable(); - this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { - // Make sure switching tabs restores tabs display - this.isTabsContentExpanded(false); - }); - this.isResourceTokenCollectionNodeSelected = ko.computed(() => { - return ( - this.selectedNode() && - this.resourceTokenCollection() && - this.selectedNode().id() === this.resourceTokenCollection().id() - ); - }); - - const splitterBounds: SplitterBounds = { - min: ExplorerMetrics.SplitterMinWidth, - max: ExplorerMetrics.SplitterMaxWidth - }; - this.splitter = new Splitter({ - splitterId: "h_splitter1", - leftId: "resourcetree", - bounds: splitterBounds, - direction: SplitterDirection.Vertical - }); - this.notificationConsoleData = ko.observableArray([]); - this.defaultExperience = ko.observable(); - this.databaseAccount.subscribe(databaseAccount => { - const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount( - databaseAccount - ); - this.defaultExperience(defaultExperience); - updateUserContext({ - defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience) - }); - }); - - this.isPreferredApiDocumentDB = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.DocumentDB.toLowerCase(); - }); - - this.isPreferredApiCassandra = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase(); - }); - this.isPreferredApiGraph = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase(); - }); - - this.isPreferredApiTable = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase(); - }); - - this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { - if (this.isFeatureEnabled(Constants.Features.enableFixedCollectionWithSharedThroughput)) { - return true; - } - - if (this.databaseAccount && !this.databaseAccount()) { - return false; - } - - return this.isEnableMongoCapabilityPresent(); - }); - - this.isServerlessEnabled = ko.computed( - () => - this.databaseAccount && - this.databaseAccount()?.properties?.capabilities?.find( - item => item.name === Constants.CapabilityNames.EnableServerless - ) !== undefined - ); - - this.isPreferredApiMongoDB = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) { - return true; - } - - if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) { - return true; - } - - if ( - this.databaseAccount && - this.databaseAccount() && - this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB - ) { - return true; - } - - return false; - }); - - this.isEnableMongoCapabilityPresent = ko.computed(() => { - const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities; - if (!capabilities) { - return false; - } - - for (let i = 0; i < capabilities.length; i++) { - if (typeof capabilities[i] === "object" && capabilities[i].name === Constants.CapabilityNames.EnableMongo) { - return true; - } - } - - return false; - }); - - this.isHostedDataExplorerEnabled = ko.computed( - () => - configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() - ); - this.isRightPanelV2Enabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableRightPanelV2) - ); - this.defaultExperience.subscribe((defaultExperience: string) => { - if ( - defaultExperience && - defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase() - ) { - this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => { - return database.id() === "system"; - }; - } - }); - - this.selectedDatabaseId = ko.computed(() => { - const selectedNode = this.selectedNode(); - if (!selectedNode) { - return ""; - } - - switch (selectedNode.nodeKind) { - case "Collection": - return (selectedNode as ViewModels.CollectionBase).databaseId || ""; - case "Database": - return selectedNode.id() || ""; - case "DocumentId": - case "StoredProcedure": - case "Trigger": - case "UserDefinedFunction": - return selectedNode.collection.databaseId || ""; - default: - return ""; - } - }); - - this.nonSystemDatabases = ko.computed(() => { - return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database)); - }); - - this.addDatabasePane = new AddDatabasePane({ - id: "adddatabasepane", - visible: ko.observable(false), - - container: this - }); - - this.addCollectionPane = new AddCollectionPane({ - isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), - id: "addcollectionpane", - visible: ko.observable(false), - - container: this - }); - - this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({ - id: "deletecollectionconfirmationpane", - visible: ko.observable(false), - - container: this - }); - - this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({ - id: "deletedatabaseconfirmationpane", - visible: ko.observable(false), - - container: this - }); - - this.graphStylingPane = new GraphStylingPane({ - id: "graphstylingpane", - visible: ko.observable(false), - - container: this - }); - - this.addTableEntityPane = new AddTableEntityPane({ - id: "addtableentitypane", - visible: ko.observable(false), - - container: this - }); - - this.editTableEntityPane = new EditTableEntityPane({ - id: "edittableentitypane", - visible: ko.observable(false), - - container: this - }); - - this.tableColumnOptionsPane = new TableColumnOptionsPane({ - id: "tablecolumnoptionspane", - visible: ko.observable(false), - - container: this - }); - - this.querySelectPane = new QuerySelectPane({ - id: "queryselectpane", - visible: ko.observable(false), - - container: this - }); - - this.newVertexPane = new NewVertexPane({ - id: "newvertexpane", - visible: ko.observable(false), - - container: this - }); - - this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ - id: "cassandraaddcollectionpane", - visible: ko.observable(false), - - container: this - }); - - this.settingsPane = new SettingsPane({ - id: "settingspane", - visible: ko.observable(false), - - container: this - }); - - this.executeSprocParamsPane = new ExecuteSprocParamsPane({ - id: "executesprocparamspane", - visible: ko.observable(false), - - container: this - }); - - this.renewAdHocAccessPane = new RenewAdHocAccessPane({ - id: "renewadhocaccesspane", - visible: ko.observable(false), - - container: this - }); - - this.uploadItemsPane = new UploadItemsPane({ - id: "uploaditemspane", - visible: ko.observable(false), - - container: this - }); - - this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); - this.selfServeComponentAdapter = new SelfServeComponentAdapter(this); - - this.loadQueryPane = new LoadQueryPane({ - id: "loadquerypane", - visible: ko.observable(false), - - container: this - }); - - this.saveQueryPane = new SaveQueryPane({ - id: "savequerypane", - visible: ko.observable(false), - - container: this - }); - - this.browseQueriesPane = new BrowseQueriesPane({ - id: "browsequeriespane", - visible: ko.observable(false), - - container: this - }); - - this.uploadFilePane = new UploadFilePane({ - id: "uploadfilepane", - visible: ko.observable(false), - - container: this - }); - - this.stringInputPane = new StringInputPane({ - id: "stringinputpane", - visible: ko.observable(false), - - container: this - }); - - this.setupNotebooksPane = new SetupNotebooksPane({ - id: "setupnotebookspane", - visible: ko.observable(false), - - container: this - }); - - this.tabsManager = new TabsManager(); - - this._panes = [ - this.addDatabasePane, - this.addCollectionPane, - this.deleteCollectionConfirmationPane, - this.deleteDatabaseConfirmationPane, - this.graphStylingPane, - this.addTableEntityPane, - this.editTableEntityPane, - this.tableColumnOptionsPane, - this.querySelectPane, - this.newVertexPane, - this.cassandraAddCollectionPane, - this.settingsPane, - this.executeSprocParamsPane, - this.renewAdHocAccessPane, - this.uploadItemsPane, - this.loadQueryPane, - this.saveQueryPane, - this.browseQueriesPane, - this.uploadFilePane, - this.stringInputPane, - this.setupNotebooksPane - ]; - this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); - this.isTabsContentExpanded = ko.observable(false); - - document.addEventListener( - "contextmenu", - function(e) { - e.preventDefault(); - }, - false - ); - - $(function() { - $(document.body).click(() => $(".commandDropdownContainer").hide()); - }); - - // TODO move this to API customization class - this.defaultExperience.subscribe(defaultExperience => { - const defaultExperienceNormalizedString = ( - defaultExperience || Constants.DefaultAccountExperience.Default - ).toLowerCase(); - - switch (defaultExperienceNormalizedString) { - case Constants.DefaultAccountExperience.DocumentDB.toLowerCase(): - this.addCollectionText("New Container"); - this.addDatabaseText("New Database"); - this.collectionTitle("SQL API"); - this.collectionTreeNodeAltText("Container"); - this.deleteCollectionText("Delete Container"); - this.deleteDatabaseText("Delete Database"); - this.addCollectionPane.title("Add Container"); - this.addCollectionPane.collectionIdTitle("Container id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle( - "Provision dedicated throughput for this container" - ); - this.deleteCollectionConfirmationPane.title("Delete Container"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); - this.refreshTreeTitle("Refresh containers"); - break; - case Constants.DefaultAccountExperience.MongoDB.toLowerCase(): - case Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase(): - this.addCollectionText("New Collection"); - this.addDatabaseText("New Database"); - this.collectionTitle("Collections"); - this.collectionTreeNodeAltText("Collection"); - this.deleteCollectionText("Delete Collection"); - this.deleteDatabaseText("Delete Database"); - this.addCollectionPane.title("Add Collection"); - this.addCollectionPane.collectionIdTitle("Collection id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle( - "Provision dedicated throughput for this collection" - ); - this.refreshTreeTitle("Refresh collections"); - break; - case Constants.DefaultAccountExperience.Graph.toLowerCase(): - this.addCollectionText("New Graph"); - this.addDatabaseText("New Database"); - this.deleteCollectionText("Delete Graph"); - this.deleteDatabaseText("Delete Database"); - this.collectionTitle("Gremlin API"); - this.collectionTreeNodeAltText("Graph"); - this.addCollectionPane.title("Add Graph"); - this.addCollectionPane.collectionIdTitle("Graph id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); - this.deleteCollectionConfirmationPane.title("Delete Graph"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id"); - this.refreshTreeTitle("Refresh graphs"); - break; - case Constants.DefaultAccountExperience.Table.toLowerCase(): - this.addCollectionText("New Table"); - this.addDatabaseText("New Database"); - this.deleteCollectionText("Delete Table"); - this.deleteDatabaseText("Delete Database"); - this.collectionTitle("Azure Table API"); - this.collectionTreeNodeAltText("Table"); - this.addCollectionPane.title("Add Table"); - this.addCollectionPane.collectionIdTitle("Table id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); - this.refreshTreeTitle("Refresh tables"); - this.addTableEntityPane.title("Add Table Entity"); - this.editTableEntityPane.title("Edit Table Entity"); - this.deleteCollectionConfirmationPane.title("Delete Table"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); - this.tableDataClient = new TablesAPIDataClient(); - break; - case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): - this.addCollectionText("New Table"); - this.addDatabaseText("New Keyspace"); - this.deleteCollectionText("Delete Table"); - this.deleteDatabaseText("Delete Keyspace"); - this.collectionTitle("Cassandra API"); - this.collectionTreeNodeAltText("Table"); - this.addCollectionPane.title("Add Table"); - this.addCollectionPane.collectionIdTitle("Table id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); - this.refreshTreeTitle("Refresh tables"); - this.addTableEntityPane.title("Add Table Row"); - this.editTableEntityPane.title("Edit Table Row"); - this.deleteCollectionConfirmationPane.title("Delete Table"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); - this.deleteDatabaseConfirmationPane.title("Delete Keyspace"); - this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id"); - this.tableDataClient = new CassandraAPIDataClient(); - break; - } - }); - - this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); - this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter(); - this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this); - - this._initSettings(); - - TelemetryProcessor.traceSuccess( - Action.InitializeDataExplorer, - { dataExplorerArea: Constants.Areas.ResourceTree }, - startKey - ); - - this.isNotebookEnabled = ko.observable(false); - this.isNotebookEnabled.subscribe(async () => { - if (!this.notebookManager) { - const notebookManagerModule = await import( - /* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager" - ); - this.notebookManager = new notebookManagerModule.default(); - this.notebookManager.initialize({ - container: this, - dialogProps: this._dialogProps, - notebookBasePath: this.notebookBasePath, - resourceTree: this.resourceTree, - refreshCommandBarButtons: () => this.refreshCommandBarButtons(), - refreshNotebookList: () => this.refreshNotebookList() - }); - - this.gitHubReposPane = this.notebookManager.gitHubReposPane; - this.isGitHubPaneEnabled(true); - } - - this.refreshCommandBarButtons(); - this.refreshNotebookList(); - }); - - this.isSparkEnabled = ko.observable(false); - this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); - this.resourceTree = new ResourceTreeAdapter(this); - this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); - this.notebookServerInfo = ko.observable({ - notebookServerEndpoint: undefined, - authToken: undefined - }); - this.notebookBasePath = ko.observable(Constants.Notebook.defaultBasePath); - this.sparkClusterConnectionInfo = ko.observable({ - userName: undefined, - password: undefined, - endpoints: [] - }); - - // Override notebook server parameters from URL parameters - const featureSubcription = this.features.subscribe(features => { - const serverInfo = this.notebookServerInfo(); - if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { - serverInfo.notebookServerEndpoint = features[Constants.Features.notebookServerUrl]; - } - - if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { - serverInfo.authToken = features[Constants.Features.notebookServerToken]; - } - this.notebookServerInfo(serverInfo); - this.notebookServerInfo.valueHasMutated(); - - if (this.isFeatureEnabled(Constants.Features.notebookBasePath)) { - this.notebookBasePath(features[Constants.Features.notebookBasePath]); - } - - if (this.isFeatureEnabled(Constants.Features.livyEndpoint)) { - this.sparkClusterConnectionInfo({ - userName: undefined, - password: undefined, - endpoints: [ - { - endpoint: features[Constants.Features.livyEndpoint], - kind: DataModels.SparkClusterEndpointKind.Livy - } - ] - }); - this.sparkClusterConnectionInfo.valueHasMutated(); - } - - if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) { - updateUserContext({ useSDKOperations: true }); - } - - featureSubcription.dispose(); - }); - - this._dialogProps = ko.observable({ - isModal: false, - visible: false, - title: undefined, - subText: undefined, - primaryButtonText: undefined, - secondaryButtonText: undefined, - onPrimaryButtonClick: undefined, - onSecondaryButtonClick: undefined - }); - this.dialogComponentAdapter = new DialogComponentAdapter(); - this.dialogComponentAdapter.parameters = this._dialogProps; - this.splashScreenAdapter = new SplashScreenComponentAdapter(this); - this.mostRecentActivity = new MostRecentActivity.MostRecentActivity(this); - - this._addSynapseLinkDialogProps = ko.observable({ - isModal: false, - visible: false, - title: undefined, - subText: undefined, - primaryButtonText: undefined, - secondaryButtonText: undefined, - onPrimaryButtonClick: undefined, - onSecondaryButtonClick: undefined - }); - this.addSynapseLinkDialog = new DialogComponentAdapter(); - this.addSynapseLinkDialog.parameters = this._addSynapseLinkDialogProps; - } - - public openEnableSynapseLinkDialog(): void { - const addSynapseLinkDialogProps: DialogProps = { - linkProps: { - linkText: "Learn more", - linkUrl: "https://aka.ms/cosmosdb-synapselink" - }, - isModal: true, - visible: true, - title: `Enable Azure Synapse Link on your Cosmos DB account`, - subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads. - Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`, - primaryButtonText: "Enable Azure Synapse Link", - secondaryButtonText: "Cancel", - - onPrimaryButtonClick: async () => { - const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); - const logId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account." - ); - this.isSynapseLinkUpdating(true); - this._closeSynapseLinkModalDialog(); - - const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id); - - try { - const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( - this.databaseAccount().id, - "2019-12-12", - { - properties: { - enableAnalyticalStorage: true - } - } - ); - NotificationConsoleUtils.clearInProgressMessageWithId(logId); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - "Enabled Azure Synapse Link for this account" - ); - TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime); - this.databaseAccount(databaseAccount); - } catch (error) { - NotificationConsoleUtils.clearInProgressMessageWithId(logId); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}` - ); - TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime); - } finally { - this.isSynapseLinkUpdating(false); - } - }, - - onSecondaryButtonClick: () => { - this._closeSynapseLinkModalDialog(); - TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink); - } - }; - this._addSynapseLinkDialogProps(addSynapseLinkDialogProps); - TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); - - // TODO: return result - } - - public copyUrlLink(src: any, event: MouseEvent): void { - const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement; - urlLinkInput && urlLinkInput.select(); - document.execCommand("copy"); - this.shareUrlCopyHelperText("Copied"); - setTimeout(() => this.shareUrlCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); - - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Copy full screen URL", - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ShareDialog - }); - } - - public onCopyUrlLinkKeyPress(src: any, event: KeyboardEvent): boolean { - if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { - this.copyUrlLink(src, null); - return false; - } - - return true; - } - - public copyToken(src: any, event: MouseEvent): void { - const tokenInput: HTMLInputElement = document.getElementById("shareToken") as HTMLInputElement; - tokenInput && tokenInput.select(); - document.execCommand("copy"); - this.shareTokenCopyHelperText("Copied"); - setTimeout(() => this.shareTokenCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); - } - - public onCopyTokenKeyPress(src: any, event: KeyboardEvent): boolean { - if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { - this.copyToken(src, null); - return false; - } - - return true; - } - - public renewToken = (): void => { - TelemetryProcessor.trace(Action.ConnectEncryptionToken); - this.renewTokenError(""); - const id: string = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Initiating connection to account" - ); - this.renewExplorerShareAccess(this, this.tokenForRenewal()) - .fail((error: any) => { - const stringifiedError: string = getErrorMessage(error); - this.renewTokenError("Invalid connection string specified"); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to initiate connection to account: ${stringifiedError}` - ); - }) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); - }; - - public generateSharedAccessData(): void { - const id: string = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Generating share url"); - AuthHeadersUtil.generateEncryptedToken().then( - (tokenResponse: DataModels.GenerateTokenResponse) => { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully generated share url"); - this.shareAccessData({ - readWriteUrl: this._getShareAccessUrlForToken(tokenResponse.readWrite), - readUrl: this._getShareAccessUrlForToken(tokenResponse.read) - }); - !this.shareAccessData().readWriteUrl && this.shareAccessToggleState(ShareAccessToggleState.Read); // select read toggle by default for readers - this.shareAccessToggleState.valueHasMutated(); // to set initial url and token state - this.shareAccessData.valueHasMutated(); - this._openShareDialog(); - }, - (error: any) => { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to generate share url: ${getErrorMessage(error)}` - ); - console.error(error); - } - ); - } - - public renewShareAccess(token: string): Q.Promise { - if (!this.renewExplorerShareAccess) { - return Q.reject("Not implemented"); - } - - const deferred: Q.Deferred = Q.defer(); - const id: string = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Initiating connection to account" - ); - this.renewExplorerShareAccess(this, token) - .then( - () => { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Connection successful"); - this.renewAdHocAccessPane && this.renewAdHocAccessPane.close(); - deferred.resolve(); - }, - (error: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to connect: ${getErrorMessage(error)}` - ); - deferred.reject(error); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - }); - - return deferred.promise; - } - - public displayGuestAccessTokenRenewalPrompt(): void { - if (!$("#dataAccessTokenModal").dialog("instance")) { - const connectButton = { - text: "Connect", - class: "connectDialogButtons connectButton connectOkBtns", - click: () => { - this.renewAdHocAccessPane.open(); - $("#dataAccessTokenModal").dialog("close"); - } - }; - const cancelButton = { - text: "Cancel", - class: "connectDialogButtons cancelBtn", - click: () => { - $("#dataAccessTokenModal").dialog("close"); - } - }; - - $("#dataAccessTokenModal").dialog({ - autoOpen: false, - buttons: [connectButton, cancelButton], - closeOnEscape: false, - draggable: false, - dialogClass: "no-close", - height: 180, - modal: true, - position: { my: "center center", at: "center center", of: window }, - resizable: false, - title: "Temporary access expired", - width: 435, - close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false) - }); - $("#dataAccessTokenModal").dialog("option", "classes", { - "ui-dialog-titlebar": "connectTitlebar" - }); - } - this.shouldShowDataAccessExpiryDialog(true); - $("#dataAccessTokenModal").dialog("open"); - } - - public isConnectExplorerVisible(): boolean { - return $("#connectExplorer").is(":visible") || false; - } - - public displayContextSwitchPromptForConnectionString(connectionString: string): void { - const yesButton = { - text: "OK", - class: "connectDialogButtons okBtn connectOkBtns", - click: () => { - $("#contextSwitchPrompt").dialog("close"); - this.tabsManager.closeTabs(); // clear all tabs so we dont leave any tabs from previous session open - this.renewShareAccess(connectionString); - } - }; - const noButton = { - text: "Cancel", - class: "connectDialogButtons cancelBtn", - click: () => { - $("#contextSwitchPrompt").dialog("close"); - } - }; - - if (!$("#contextSwitchPrompt").dialog("instance")) { - $("#contextSwitchPrompt").dialog({ - autoOpen: false, - buttons: [yesButton, noButton], - closeOnEscape: false, - draggable: false, - dialogClass: "no-close", - height: 255, - modal: true, - position: { my: "center center", at: "center center", of: window }, - resizable: false, - title: "Switch account", - width: 440, - close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false) - }); - $("#contextSwitchPrompt").dialog("option", "classes", { - "ui-dialog-titlebar": "connectTitlebar" - }); - $("#contextSwitchPrompt").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => { - $(".ui-dialog ").css("z-index", 1001); - $("#contextSwitchPrompt") - .parent() - .siblings(".ui-widget-overlay") - .css("z-index", 1000); - }); - } - $("#contextSwitchPrompt").dialog("option", "buttons", [yesButton, noButton]); // rebind buttons so callbacks accept current connection string - this.shouldShowContextSwitchPrompt(true); - $("#contextSwitchPrompt").dialog("open"); - } - - public displayConnectExplorerForm(): void { - $("#divExplorer").hide(); - $("#connectExplorer").css("display", "flex"); - } - - public hideConnectExplorerForm(): void { - $("#connectExplorer").hide(); - $("#divExplorer").show(); - } - - public isReadWriteToggled: () => boolean = (): boolean => { - return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite; - }; - - public isReadToggled: () => boolean = (): boolean => { - return this.shareAccessToggleState() === ShareAccessToggleState.Read; - }; - - public toggleReadWrite: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { - this.shareAccessToggleState(ShareAccessToggleState.ReadWrite); - }; - - public toggleRead: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { - this.shareAccessToggleState(ShareAccessToggleState.Read); - }; - - public onToggleKeyDown: (src: any, event: KeyboardEvent) => boolean = (src: any, event: KeyboardEvent) => { - if (event.keyCode === Constants.KeyCodes.LeftArrow) { - this.toggleReadWrite(src, null); - return false; - } else if (event.keyCode === Constants.KeyCodes.RightArrow) { - this.toggleRead(src, null); - return false; - } - return true; - }; - - public isDatabaseNodeOrNoneSelected(): boolean { - return this.isNoneSelected() || this.isDatabaseNodeSelected(); - } - - public isDatabaseNodeSelected(): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false; - } - - public isNodeKindSelected(nodeKind: string): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false; - } - - public isNoneSelected(): boolean { - return this.selectedNode() == null; - } - - public isFeatureEnabled(feature: string): boolean { - const features = this.features(); - - if (!features) { - return false; - } - - if (feature in features && features[feature]) { - return true; - } - - return false; - } - - public logConsoleData(consoleData: ConsoleData): void { - this.notificationConsoleData.splice(0, 0, consoleData); - } - - public deleteInProgressConsoleDataWithId(id: string): void { - const updatedConsoleData = _.reject( - this.notificationConsoleData(), - (data: ConsoleData) => data.type === ConsoleDataType.InProgress && data.id === id - ); - this.notificationConsoleData(updatedConsoleData); - } - - public expandConsole(): void { - this.isNotificationConsoleExpanded(true); - } - - public collapseConsole(): void { - this.isNotificationConsoleExpanded(false); - } - - public toggleLeftPaneExpanded() { - this.isLeftPaneExpanded(!this.isLeftPaneExpanded()); - - if (this.isLeftPaneExpanded()) { - document.getElementById("expandToggleLeftPaneButton").focus(); - this.splitter.expandLeft(); - } else { - document.getElementById("collapseToggleLeftPaneButton").focus(); - this.splitter.collapseLeft(); - } - } - - public refreshDatabaseForResourceToken(): Q.Promise { - const databaseId = this.resourceTokenDatabaseId(); - const collectionId = this.resourceTokenCollectionId(); - if (!databaseId || !collectionId) { - return Q.reject(); - } - - const deferred: Q.Deferred = Q.defer(); - readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { - this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); - this.selectedNode(this.resourceTokenCollection()); - deferred.resolve(); - }); - - return deferred.promise; - } - - public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise { - this.isRefreshingExplorer(true); - const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - let resourceTreeStartKey: number = null; - if (isInitialLoad) { - resourceTreeStartKey = TelemetryProcessor.traceStart(Action.LoadResourceTree, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - // TODO: Refactor - const deferred: Q.Deferred = Q.defer(); - this._setLoadingStatusText("Fetching databases..."); - readDatabases().then( - (databases: DataModels.Database[]) => { - this._setLoadingStatusText("Successfully fetched databases."); - TelemetryProcessor.traceSuccess( - Action.LoadDatabases, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }, - startKey - ); - const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); - const deltaDatabases = this.getDeltaDatabases(databases); - this.addDatabasesToList(deltaDatabases.toAdd); - this.deleteDatabasesFromList(deltaDatabases.toDelete); - this.selectedNode(currentlySelectedNode); - this._setLoadingStatusText("Fetching containers..."); - this.refreshAndExpandNewDatabases(deltaDatabases.toAdd) - .then( - () => { - this._setLoadingStatusText("Successfully fetched containers."); - deferred.resolve(); - }, - reason => { - this._setLoadingStatusText("Failed to fetch containers."); - deferred.reject(reason); - } - ) - .finally(() => this.isRefreshingExplorer(false)); - }, - error => { - this._setLoadingStatusText("Failed to fetch databases."); - this.isRefreshingExplorer(false); - deferred.reject(error); - const errorMessage = getErrorMessage(error); - TelemetryProcessor.traceFailure( - Action.LoadDatabases, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while refreshing databases: ${errorMessage}` - ); - } - ); - - return deferred.promise.then( - () => { - if (resourceTreeStartKey != null) { - TelemetryProcessor.traceSuccess( - Action.LoadResourceTree, - { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }, - resourceTreeStartKey - ); - } - }, - error => { - if (resourceTreeStartKey != null) { - TelemetryProcessor.traceFailure( - Action.LoadResourceTree, - { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - resourceTreeStartKey - ); - } - } - ); - } - - public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.onRefreshResourcesClick(source, null); - return false; - } - return true; - }; - - public onRefreshResourcesClick = (source: any, event: MouseEvent): void => { - const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { - description: "Refresh button clicked", - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - this.isRefreshingExplorer(true); - this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); - this.refreshNotebookList(); - }; - - public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.toggleLeftPaneExpanded(); - return false; - } - return true; - }; - - // Facade - public provideFeedbackEmail = () => { - window.open(Constants.Urls.feedbackEmail, "_self"); - }; - - public async getArcadiaToken(): Promise { - return new Promise((resolve: (token: string) => void, reject: (error: any) => void) => { - sendCachedDataMessage(MessageTypes.GetArcadiaToken, undefined /** params **/).then( - (token: string) => { - resolve(token); - }, - (error: any) => { - Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken"); - resolve(undefined); - } - ); - }); - } - - private async _getArcadiaWorkspaces(): Promise { - try { - const workspaces = await this._arcadiaManager.listWorkspacesAsync([userContext.subscriptionId]); - let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length); - const sparkPromises: Promise[] = []; - workspaces.forEach((workspace, i) => { - let promise = this._arcadiaManager.listSparkPoolsAsync(workspaces[i].id).then( - sparkpools => { - workspaceItems[i] = { ...workspace, sparkPools: sparkpools }; - }, - error => { - Logger.logError(getErrorMessage(error), "Explorer/this._arcadiaManager.listSparkPoolsAsync"); - } - ); - sparkPromises.push(promise); - }); - - return Promise.all(sparkPromises).then(() => workspaceItems); - } catch (error) { - handleError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed"); - return Promise.resolve([]); - } - } - - public async createWorkspace(): Promise { - return sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/); - } - - public async createSparkPool(workspaceId: string): Promise { - return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]); - } - - public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - throw new Error("No database account specified"); - } - - if (this._isInitializingNotebooks) { - return; - } - this._isInitializingNotebooks = true; - - await this.ensureNotebookWorkspaceRunning(); - let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: undefined, - notebookServerEndpoint: undefined - }; - try { - connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync( - databaseAccount.id, - "default" - ); - } catch (error) { - this._isInitializingNotebooks = false; - handleError( - error, - "initNotebooks/getNotebookConnectionInfoAsync", - `Failed to get notebook workspace connection info: ${getErrorMessage(error)}` - ); - throw error; - } finally { - // Overwrite with feature flags - if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { - connectionInfo.notebookServerEndpoint = this.features()[Constants.Features.notebookServerUrl]; - } - - if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { - connectionInfo.authToken = this.features()[Constants.Features.notebookServerToken]; - } - - this.notebookServerInfo(connectionInfo); - this.notebookServerInfo.valueHasMutated(); - this.refreshNotebookList(); - } - - this._isInitializingNotebooks = false; - } - - public resetNotebookWorkspace() { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace" - ); - return; - } - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - visible: true, - title: "Reset Workspace", - subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?", - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: this._closeModalDialog - }; - this._dialogProps(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - - try { - const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id); - return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private async ensureNotebookWorkspaceRunning() { - if (!this.databaseAccount()) { - return; - } - - let clearMessage; - try { - const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync( - this.databaseAccount().id, - "default" - ); - if ( - notebookWorkspace && - notebookWorkspace.properties && - notebookWorkspace.properties.status && - notebookWorkspace.properties.status.toLowerCase() === "stopped" - ) { - clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace"); - await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); - } - } catch (error) { - handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace"); - } finally { - clearMessage && clearMessage(); - } - } - - private _resetNotebookWorkspace = async () => { - this._closeModalDialog(); - const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace"); - try { - await this.notebookManager?.notebookClient.resetWorkspace(); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); - } catch (error) { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }); - throw error; - } finally { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - } - }; - - private _closeModalDialog = () => { - this._dialogProps().visible = false; - this._dialogProps.valueHasMutated(); - }; - - private _closeSynapseLinkModalDialog = () => { - this._addSynapseLinkDialogProps().visible = false; - this._addSynapseLinkDialogProps.valueHasMutated(); - }; - - private _shouldProcessMessage(event: MessageEvent): boolean { - if (typeof event.data !== "object") { - return false; - } - if (event.data["signature"] !== "pcIframe") { - return false; - } - if (!("data" in event.data)) { - return false; - } - if (typeof event.data["data"] !== "object") { - return false; - } - - // before initialization completed give exception - const message = event.data.data; - if (!this._importExplorerConfigComplete && message && message.type) { - const messageType = message.type; - switch (messageType) { - case MessageTypes.SendNotification: - case MessageTypes.ClearNotification: - case MessageTypes.LoadingStatus: - case MessageTypes.InitTestExplorer: - return true; - } - } - if (!("inputs" in event.data["data"]) && !this._importExplorerConfigComplete) { - return false; - } - return true; - } - - public handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (!this._shouldProcessMessage(event)) { - return; - } - - const message: any = event.data.data; - const inputs: ViewModels.DataExplorerInputsFrame = message.inputs; - - const isRunningInPortal = configContext.platform === Platform.Portal; - const isRunningInDevMode = process.env.NODE_ENV === "development"; - if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) { - inputs.extensionEndpoint = configContext.PROXY_PATH; - } - - this.initDataExplorerWithFrameInputs(inputs); - - const openAction: ActionContracts.DataExplorerAction = message.openAction; - if (!!openAction) { - if (this.isRefreshingExplorer()) { - const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { - handleOpenAction(openAction, this.nonSystemDatabases(), this); - subscription.dispose(); - }); - } else { - handleOpenAction(openAction, this.nonSystemDatabases(), this); - } - } - if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { - handleCachedDataMessage(message); - return; - } - if (message.type) { - switch (message.type) { - case MessageTypes.UpdateLocationHash: - if (!message.locationHash) { - break; - } - hasher.replaceHash(message.locationHash); - RouteHandler.getInstance().parseHash(message.locationHash); - break; - case MessageTypes.SendNotification: - if (!message.message) { - break; - } - NotificationConsoleUtils.logConsoleMessage( - message.consoleDataType || ConsoleDataType.Info, - message.message, - message.id - ); - break; - case MessageTypes.ClearNotification: - if (!message.id) { - break; - } - NotificationConsoleUtils.clearInProgressMessageWithId(message.id); - break; - case MessageTypes.LoadingStatus: - if (!message.text) { - break; - } - this._setLoadingStatusText(message.text, message.title); - break; - } - return; - } - - this.splashScreenAdapter.forceRender(); - } - - public findSelectedDatabase(): ViewModels.Database { - if (!this.selectedNode()) { - return null; - } - if (this.selectedNode().nodeKind === "Database") { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); - } - return this.findSelectedCollection().database; - } - - public findDatabaseWithId(databaseId: string): ViewModels.Database { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); - } - - public isLastNonEmptyDatabase(): boolean { - if (this.isLastDatabase() && this.databases()[0].collections && this.databases()[0].collections().length > 0) { - return true; - } - return false; - } - - public isLastDatabase(): boolean { - if (this.databases().length > 1) { - return false; - } - return true; - } - - public isSelectedDatabaseShared(): boolean { - const database = this.findSelectedDatabase(); - if (!!database) { - return database.offer && !!database.offer(); - } - - return false; - } - - public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void { - const selfServeFeature = inputs.features[Constants.Features.selfServeType]; - if (selfServeFeature) { - // self serve type received from query string - const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType]; - this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid); - } else if (inputs.selfServeType) { - // self serve type received from portal - this.selfServeType(inputs.selfServeType); - } else { - this.selfServeType(SelfServeType.none); - } - } - - public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void { - if (inputs != null) { - // In development mode, save the iframe message from the portal in session storage. - // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { - sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); - } - - const authorizationToken = inputs.authorizationToken || ""; - const masterKey = inputs.masterKey || ""; - const databaseAccount = inputs.databaseAccount || null; - if (inputs.defaultCollectionThroughput) { - this.collectionCreationDefaults = inputs.defaultCollectionThroughput; - } - this.features(inputs.features); - this.serverId(inputs.serverId); - this.databaseAccount(databaseAccount); - this.subscriptionType(inputs.subscriptionType); - this.hasWriteAccess(inputs.hasWriteAccess); - this.flight(inputs.addCollectionDefaultFlight); - this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); - this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); - this.setFeatureFlagsFromFlights(inputs.flights); - this.setSelfServeType(inputs); - this._importExplorerConfigComplete = true; - - updateConfigContext({ - BACKEND_ENDPOINT: inputs.extensionEndpoint || "", - ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT) - }); - - updateUserContext({ - authorizationToken, - masterKey, - databaseAccount, - resourceGroup: inputs.resourceGroup, - subscriptionId: inputs.subscriptionId, - subscriptionType: inputs.subscriptionType, - quotaId: inputs.quotaId - }); - TelemetryProcessor.traceSuccess( - Action.LoadDatabaseAccount, - { - resourceId: this.databaseAccount && this.databaseAccount().id, - dataExplorerArea: Constants.Areas.ResourceTree, - databaseAccount: this.databaseAccount && this.databaseAccount() - }, - inputs.loadDatabaseAccountTimestamp - ); - - this.isAccountReady(true); - } - } - - public setFeatureFlagsFromFlights(flights: readonly string[]): void { - if (!flights) { - return; - } - if (flights.indexOf(Constants.Flights.AutoscaleTest) !== -1) { - this.isAutoscaleDefaultEnabled(true); - } - if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { - this.isMongoIndexingEnabled(true); - } - } - - public findSelectedCollection(): ViewModels.Collection { - return (this.selectedNode().nodeKind === "Collection" - ? this.selectedNode() - : this.selectedNode().collection) as ViewModels.Collection; - } - - // TODO: Refactor below methods, minimize dependencies and add unit tests where necessary - public findSelectedStoredProcedure(): StoredProcedure { - const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); - return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => { - const openedSprocTab = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.StoredProcedures, - tab => tab.node && tab.node.rid === storedProcedure.rid - ); - return ( - storedProcedure.rid === this.selectedNode().rid || - (!!openedSprocTab && openedSprocTab.length > 0 && openedSprocTab[0].isActive()) - ); - }); - } - - public findSelectedUDF(): UserDefinedFunction { - const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); - return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => { - const openedUdfTab = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.UserDefinedFunctions, - tab => tab.node && tab.node.rid === userDefinedFunction.rid - ); - return ( - userDefinedFunction.rid === this.selectedNode().rid || - (!!openedUdfTab && openedUdfTab.length > 0 && openedUdfTab[0].isActive()) - ); - }); - } - - public findSelectedTrigger(): Trigger { - const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); - return _.find(selectedCollection.triggers(), (trigger: Trigger) => { - const openedTriggerTab = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Triggers, - tab => tab.node && tab.node.rid === trigger.rid - ); - return ( - trigger.rid === this.selectedNode().rid || - (!!openedTriggerTab && openedTriggerTab.length > 0 && openedTriggerTab[0].isActive()) - ); - }); - } - - public closeAllPanes(): void { - this._panes.forEach((pane: ContextualPaneBase) => pane.close()); - } - - public isRunningOnNationalCloud(): boolean { - return ( - this.serverId() === Constants.ServerIds.blackforest || - this.serverId() === Constants.ServerIds.fairfax || - this.serverId() === Constants.ServerIds.mooncake - ); - } - - public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void { - this.commandBarComponentAdapter.onUpdateTabsButtons(buttons); - } - - public signInAad = () => { - TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" }); - sendMessage({ - type: MessageTypes.AadSignIn - }); - }; - - public onSwitchToConnectionString = () => { - $("#connectWithAad").hide(); - $("#connectWithConnectionString").show(); - }; - - public clickHostedAccountSwitch = () => { - sendMessage({ - type: MessageTypes.UpdateAccountSwitch, - click: true - }); - }; - - public clickHostedDirectorySwitch = () => { - sendMessage({ - type: MessageTypes.UpdateDirectoryControl, - click: true - }); - }; - - public refreshDatabaseAccount = () => { - sendMessage({ - type: MessageTypes.RefreshDatabaseAccount - }); - }; - - private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise { - // we reload collections for all databases so the resource tree reflects any collection-level changes - // i.e addition of stored procedures, etc. - const deferred: Q.Deferred = Q.defer(); - let loadCollectionPromises: Q.Promise[] = []; - - // If the user has a lot of databases, only load expanded databases. - const databasesToLoad = - this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand - ? this.databases() - : this.databases().filter(db => db.isDatabaseExpanded()); - - const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - databasesToLoad.forEach(async (database: ViewModels.Database) => { - await database.loadCollections(); - const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); - if (isNewDatabase) { - database.expandDatabase(); - } - this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().id() === database.id()); - }); - - Q.all(loadCollectionPromises).done( - () => { - deferred.resolve(); - TelemetryProcessor.traceSuccess( - Action.LoadCollections, - { dataExplorerArea: Constants.Areas.ResourceTree }, - startKey - ); - }, - (error: any) => { - deferred.reject(error); - TelemetryProcessor.traceFailure( - Action.LoadCollections, - { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - startKey - ); - } - ); - return deferred.promise; - } - - // TODO: Abstract this elsewhere - private _openShareDialog: () => void = (): void => { - if (!$("#shareDataAccessFlyout").dialog("instance")) { - const accountMetadataInfo = { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ShareDialog - }; - const openFullscreenButton = { - text: "Open", - class: "openFullScreenBtn openFullScreenCancelBtn", - click: () => { - TelemetryProcessor.trace( - Action.SelectItem, - ActionModifiers.Mark, - _.extend({}, { description: "Open full screen" }, accountMetadataInfo) - ); - - const hiddenAnchorElement: HTMLAnchorElement = document.createElement("a"); - hiddenAnchorElement.href = this.shareAccessUrl(); - hiddenAnchorElement.target = "_blank"; - $("#shareDataAccessFlyout").dialog("close"); - hiddenAnchorElement.click(); - } - }; - const cancelButton = { - text: "Cancel", - class: "shareCancelButton openFullScreenCancelBtn", - click: () => { - TelemetryProcessor.trace( - Action.SelectItem, - ActionModifiers.Mark, - _.extend({}, { description: "Cancel open full screen" }, accountMetadataInfo) - ); - $("#shareDataAccessFlyout").dialog("close"); - } - }; - $("#shareDataAccessFlyout").dialog({ - autoOpen: false, - buttons: [openFullscreenButton, cancelButton], - closeOnEscape: true, - draggable: false, - dialogClass: "no-close", - position: { my: "right top", at: "right bottom", of: $(".OpenFullScreen") }, - resizable: false, - title: "Open Full Screen", - width: 400, - close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowShareDialogContents(false) - }); - $("#shareDataAccessFlyout").dialog("option", "classes", { - "ui-widget-content": "shareUrlDialog", - "ui-widget-header": "shareUrlTitle", - "ui-dialog-titlebar-close": "shareClose", - "ui-button": "shareCloseIcon", - "ui-button-icon": "cancelIcon", - "ui-icon": "" - }); - $("#shareDataAccessFlyout").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => - $(".openFullScreenBtn").focus() - ); - } - $("#shareDataAccessFlyout").dialog("close"); - this.shouldShowShareDialogContents(true); - $("#shareDataAccessFlyout").dialog("open"); - }; - - private _getShareAccessUrlForToken(token: string): string { - if (!token) { - return undefined; - } - - const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`; - const currentActiveTab = this.tabsManager.activeTab(); - - return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`; - } - - private _initSettings() { - if (!ExplorerSettings.hasSettingsDefined()) { - ExplorerSettings.createDefaultSettings(); - } - } - - public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { - const database: ViewModels.Database = this.databases().find( - (database: ViewModels.Database) => database.id() === databaseId - ); - return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); - } - - public isLastCollection(): boolean { - let collectionCount = 0; - if (this.databases().length == 0) { - return false; - } - for (let i = 0; i < this.databases().length; i++) { - const database = this.databases()[i]; - collectionCount += database.collections().length; - if (collectionCount > 1) { - return false; - } - } - return true; - } - - private getDeltaDatabases( - updatedDatabaseList: DataModels.Database[] - ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { - const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { - const databaseExists = _.some( - this.databases(), - (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id - ); - return !databaseExists; - }); - const databasesToAdd: ViewModels.Database[] = newDatabases.map( - (newDatabase: DataModels.Database) => new Database(this, newDatabase) - ); - - let databasesToDelete: ViewModels.Database[] = []; - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const databasePresentInUpdatedList = _.some( - updatedDatabaseList, - (db: DataModels.Database) => db.id === database.id() - ); - if (!databasePresentInUpdatedList) { - databasesToDelete.push(database); - } - }); - - return { toAdd: databasesToAdd, toDelete: databasesToDelete }; - } - - private addDatabasesToList(databases: ViewModels.Database[]): void { - this.databases( - this.databases() - .concat(databases) - .sort((database1, database2) => database1.id().localeCompare(database2.id())) - ); - } - - private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { - const databasesToKeep: ViewModels.Database[] = []; - - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); - if (!shouldRemoveDatabase) { - databasesToKeep.push(database); - } - }); - - this.databases(databasesToKeep); - } - - public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to upload notebook, but notebook is not enabled"; - handleError(error, "Explorer/uploadFile"); - throw new Error(error); - } - - const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); - promise - .then(() => this.resourceTree.triggerRender()) - .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); - return promise; - } - - public async importAndOpen(path: string): Promise { - const name = NotebookUtil.getName(path); - const item = NotebookUtil.createNotebookContentItem(name, path, "file"); - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { - const existingItem = _.find(parent.children, node => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const content = await this.readFile(item); - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - return Promise.resolve(false); - } - - public async importAndOpenContent(name: string, content: string): Promise { - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { - if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { - this.notebookToImport = undefined; // we don't want to try opening this notebook again - } - - const existingItem = _.find(parent.children, node => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - this.notebookToImport = { name, content }; // we'll try opening this notebook later on - return Promise.resolve(false); - } - - public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise { - if (this.notebookManager) { - await this.notebookManager.openPublishNotebookPane( - name, - content, - parentDomElement, - this.isLinkInjectionEnabled() - ); - this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; - this.isPublishNotebookPaneEnabled(true); - } - } - - public copyNotebook(name: string, content: string): void { - if (this.notebookManager) { - this.notebookManager.openCopyNotebookPane(name, content); - this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter; - this.isCopyNotebookPaneEnabled(true); - } - } - - public showOkModalDialog(title: string, msg: string): void { - this._dialogProps({ - isModal: true, - visible: true, - title, - subText: msg, - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: this._closeModalDialog, - onSecondaryButtonClick: undefined - }); - } - - public showOkCancelModalDialog( - title: string, - msg: string, - okLabel: string, - onOk: () => void, - cancelLabel: string, - onCancel: () => void, - choiceGroupProps?: IChoiceGroupProps, - textFieldProps?: TextFieldProps, - isPrimaryButtonDisabled?: boolean - ): void { - this._dialogProps({ - isModal: true, - visible: true, - title, - subText: msg, - primaryButtonText: okLabel, - secondaryButtonText: cancelLabel, - onPrimaryButtonClick: () => { - this._closeModalDialog(); - onOk && onOk(); - }, - onSecondaryButtonClick: () => { - this._closeModalDialog(); - onCancel && onCancel(); - }, - choiceGroupProps, - textFieldProps, - primaryButtonDisabled: isPrimaryButtonDisabled - }); - } - - /** - * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. - * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. - * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder - * will not fetch its content if the children array exists (and has only one child which was manually created). - * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. - * - * @param name - * @param path - */ - public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { - return NotebookUtil.createNotebookContentItem(name, path, "file"); - } - - public async openNotebook(notebookContentItem: NotebookContentItem): Promise { - if (!notebookContentItem || !notebookContentItem.path) { - throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); - } - - const notebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - tab => - (tab as NotebookV2Tab).notebookPath && - FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path) - ) as NotebookV2Tab[]; - let notebookTab = notebookTabs && notebookTabs[0]; - - if (notebookTab) { - this.tabsManager.activateTab(notebookTab); - } else { - const options: NotebookTabOptions = { - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.NotebookV2, - node: null, - title: notebookContentItem.name, - tabPath: notebookContentItem.path, - collection: null, - masterKey: userContext.masterKey || "", - hashLocation: "notebooks", - isActive: ko.observable(false), - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - onUpdateTabsButtons: this.onUpdateTabsButtons, - container: this, - notebookContentItem - }; - - try { - const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); - notebookTab = new NotebookTabV2.default(options); - this.tabsManager.activateNewTab(notebookTab); - } catch (reason) { - console.error("Import NotebookV2Tab failed!", reason); - return false; - } - } - - return true; - } - - public renameNotebook(notebookFile: NotebookContentItem): Q.Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to rename notebook, but notebook is not enabled"; - handleError(error, "Explorer/renameNotebook"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); - } - ); - if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); - return Q.reject(); - } - - const originalPath = notebookFile.path; - const result = this.stringInputPane - .openWithOptions({ - errorMessage: "Could not rename notebook", - inProgressMessage: "Renaming notebook to", - successMessage: "Renamed notebook to", - inputLabel: "Enter new notebook name", - paneTitle: "Rename Notebook", - submitButtonLabel: "Rename", - defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"), - onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) - }) - .then(newNotebookFile => { - const notebookTabs = this.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); - (tab as NotebookV2Tab).notebookPath(newNotebookFile.path); - }); - - return newNotebookFile; - }); - result.then(() => this.resourceTree.triggerRender()); - return result; - } - - public onCreateDirectory(parent: NotebookContentItem): Q.Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create notebook directory, but notebook is not enabled"; - handleError(error, "Explorer/onCreateDirectory"); - throw new Error(error); - } - - const result = this.stringInputPane.openWithOptions({ - errorMessage: "Could not create directory ", - inProgressMessage: "Creating directory ", - successMessage: "Created directory ", - inputLabel: "Enter new directory name", - paneTitle: "Create new directory", - submitButtonLabel: "Create", - defaultInput: "", - onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input) - }); - result.then(() => this.resourceTree.triggerRender()); - return result; - } - - public readFile(notebookFile: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to read file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); - } - - public downloadFile(notebookFile: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to download file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( - (content: string) => { - const blob = stringToBlob(content, "text/plain"); - if (navigator.msSaveBlob) { - // for IE and Edge - navigator.msSaveBlob(blob, notebookFile.name); - } else { - const downloadLink: HTMLAnchorElement = document.createElement("a"); - const url = URL.createObjectURL(blob); - downloadLink.href = url; - downloadLink.target = "_self"; - downloadLink.download = notebookFile.name; - - // for some reason, FF displays the download prompt only when - // the link is added to the dom so we add and remove it - document.body.appendChild(downloadLink); - downloadLink.click(); - downloadLink.remove(); - } - - clearMessage(); - }, - (error: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Could not download notebook ${getErrorMessage(error)}` - ); - - clearMessage(); - } - ); - } - - private async _refreshNotebooksEnabledStateForAccount(): Promise { - const authType = window.authType as AuthType; - if ( - authType === AuthType.EncryptedToken || - authType === AuthType.ResourceToken || - authType === AuthType.MasterKey - ) { - this.isNotebooksEnabledForAccount(false); - return; - } - - const databaseAccount = this.databaseAccount(); - const databaseAccountLocation = databaseAccount && databaseAccount.location.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"); - this.isNotebooksEnabledForAccount(true); - return; - } - const isAccountInAllowedLocation = !disallowedLocations.some( - disallowedLocation => disallowedLocation === databaseAccountLocation - ); - this.isNotebooksEnabledForAccount(isAccountInAllowedLocation); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); - this.isNotebooksEnabledForAccount(false); - } - } - - public _refreshSparkEnabledStateForAccount = async (): Promise => { - const subscriptionId = userContext.subscriptionId; - const armEndpoint = configContext.ARM_ENDPOINT; - const authType = window.authType as AuthType; - if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { - // explorer is not aware of the database account yet - this.isSparkEnabledForAccount(false); - return; - } - - const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`; - const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); - try { - const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( - featureUri, - Constants.ArmApiVersions.armFeatures - ); - const isEnabled = - (sparkNotebooksFeature && - sparkNotebooksFeature.properties && - sparkNotebooksFeature.properties.state === "Registered") || - false; - this.isSparkEnabledForAccount(isEnabled); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); - this.isSparkEnabledForAccount(false); - } - }; - - public _isAfecFeatureRegistered = async (featureName: string): Promise => { - const subscriptionId = userContext.subscriptionId; - const armEndpoint = configContext.ARM_ENDPOINT; - const authType = window.authType as AuthType; - if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { - // explorer is not aware of the database account yet - return false; - } - - const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`; - const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); - try { - const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( - featureUri, - Constants.ArmApiVersions.armFeatures - ); - const isEnabled = - (featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false; - return isEnabled; - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); - return false; - } - }; - private refreshNotebookList = async (): Promise => { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - return; - } - - await this.resourceTree.initialize(); - this.notebookManager?.refreshPinnedRepos(); - if (this.notebookToImport) { - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - }; - - public deleteNotebookFile(item: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to delete notebook file, but notebook is not enabled"; - handleError(error, "Explorer/deleteNotebookFile"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); - } - ); - if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); - return Promise.reject(); - } - - if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { - this._dialogProps({ - isModal: true, - visible: true, - title: "Unable to delete file", - subText: "Directory is not empty.", - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: this._closeModalDialog, - onSecondaryButtonClick: undefined - }); - return Promise.reject(); - } - - return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( - () => { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`); - }, - (reason: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to delete "${item.path}": ${JSON.stringify(reason)}` - ); - } - ); - } - - /** - * This creates a new notebook file, then opens the notebook - */ - public onNewNotebookClicked(parent?: NotebookContentItem): void { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - - parent = parent || this.resourceTree.myNotebooksContentRoot; - - const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Creating new notebook in ${parent.path}` - ); - - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent) - .then((newFile: NotebookContentItem) => { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }, - startKey - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error: any) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - }) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId)); - } - - public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { - parent = parent || this.resourceTree.myNotebooksContentRoot; - - this.uploadFilePane.openWithOptions({ - paneTitle: "Upload file to notebook server", - selectFileInputLabel: "Select file to upload", - errorMessage: "Could not upload file", - inProgressMessage: "Uploading file to notebook server", - successMessage: "Successfully uploaded file to notebook server", - onSubmit: async (file: File): Promise => { - const readFileAsText = (inputFile: File): Promise => { - const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onerror = () => { - reader.abort(); - reject(`Problem parsing file: ${inputFile}`); - }; - reader.onload = () => { - resolve(reader.result as string); - }; - reader.readAsText(inputFile); - }); - }; - - const fileContent = await readFileAsText(file); - return this.uploadFile(file.name, fileContent, parent); - }, - extensions: undefined, - submitButtonLabel: "Upload" - }); - } - - public refreshContentItem(item: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to refresh notebook list, but notebook is not enabled"; - handleError(error, "Explorer/refreshContentItem"); - return Promise.reject(new Error(error)); - } - - return this.notebookManager?.notebookContentClient.updateItemChildren(item); - } - - public getNotebookBasePath(): string { - return this.notebookBasePath(); - } - - public openNotebookTerminal(kind: ViewModels.TerminalKind) { - let title: string; - let hashLocation: string; - - switch (kind) { - case ViewModels.TerminalKind.Default: - title = "Terminal"; - hashLocation = "terminal"; - break; - - case ViewModels.TerminalKind.Mongo: - title = "Mongo Shell"; - hashLocation = "mongo-shell"; - break; - - case ViewModels.TerminalKind.Cassandra: - title = "Cassandra Shell"; - hashLocation = "cassandra-shell"; - break; - - default: - throw new Error("Terminal kind: ${kind} not supported"); - } - - const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Terminal, - tab => tab.hashLocation() == hashLocation - ) as TerminalTab[]; - let terminalTab: TerminalTab = terminalTabs && terminalTabs[0]; - - if (terminalTab) { - this.tabsManager.activateTab(terminalTab); - } else { - const newTab = new TerminalTab({ - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.Terminal, - node: null, - title: title, - tabPath: title, - collection: null, - hashLocation: hashLocation, - isActive: ko.observable(false), - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - onUpdateTabsButtons: this.onUpdateTabsButtons, - container: this, - kind: kind - }); - - this.tabsManager.activateNewTab(newTab); - } - } - - public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) { - let title: string = "Gallery"; - let hashLocation: string = "gallery"; - - const galleryTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Gallery, - tab => tab.hashLocation() == hashLocation - ); - let galleryTab = galleryTabs && galleryTabs[0]; - - if (galleryTab) { - this.tabsManager.activateTab(galleryTab); - } else { - if (!this.galleryTab) { - this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab"); - } - - const newTab = new this.galleryTab.default({ - // GalleryTabOptions - account: userContext.databaseAccount, - container: this, - junoClient: this.notebookManager?.junoClient, - notebookUrl, - galleryItem, - isFavorite, - // TabOptions - tabKind: ViewModels.CollectionTabKind.Gallery, - title: title, - tabPath: title, - documentClientUtility: null, - isActive: ko.observable(false), - hashLocation: hashLocation, - onUpdateTabsButtons: this.onUpdateTabsButtons, - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null - }); - - this.tabsManager.activateNewTab(newTab); - } - } - - public async openNotebookViewer(notebookUrl: string) { - const title = path.basename(notebookUrl); - const hashLocation = notebookUrl; - - if (!this.notebookViewerTab) { - this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab"); - } - - const notebookViewerTabModule = this.notebookViewerTab; - - let isNotebookViewerOpen = (tab: TabsBase) => { - const notebookViewerTab = tab as typeof notebookViewerTabModule.default; - return notebookViewerTab.notebookUrl === notebookUrl; - }; - - const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, tab => { - return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab); - }); - let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0]; - - if (notebookViewerTab) { - this.tabsManager.activateNewTab(notebookViewerTab); - } else { - notebookViewerTab = new this.notebookViewerTab.default({ - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.NotebookViewer, - node: null, - title: title, - tabPath: title, - documentClientUtility: null, - collection: null, - hashLocation: hashLocation, - isActive: ko.observable(false), - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - onUpdateTabsButtons: this.onUpdateTabsButtons, - container: this, - notebookUrl - }); - - this.tabsManager.activateNewTab(notebookViewerTab); - } - } - - public onNewCollectionClicked(): void { - if (this.isPreferredApiCassandra()) { - this.cassandraAddCollectionPane.open(); - } else { - this.addCollectionPane.open(this.selectedDatabaseId()); - } - document.getElementById("linkAddCollection").focus(); - } - - private refreshCommandBarButtons(): void { - const activeTab = this.tabsManager.activeTab(); - if (activeTab) { - activeTab.onActivate(); // TODO only update tabs buttons? - } else { - this.onUpdateTabsButtons([]); - } - } - - private getTokenRefreshInterval(token: string): number { - let tokenRefreshInterval = Constants.ClientDefaults.arcadiaTokenRefreshInterval; - if (!token) { - return tokenRefreshInterval; - } - - try { - const tokenPayload = decryptJWTToken(this.arcadiaToken()); - if (tokenPayload && tokenPayload.hasOwnProperty("exp")) { - const expirationTime = tokenPayload.exp as number; // seconds since unix epoch - const now = new Date().getTime() / 1000; - const tokenExpirationIntervalInMs = (expirationTime - now) * 1000; - if (tokenExpirationIntervalInMs < tokenRefreshInterval) { - tokenRefreshInterval = - tokenExpirationIntervalInMs - Constants.ClientDefaults.arcadiaTokenRefreshIntervalPaddingMs; - } - } - return tokenRefreshInterval; - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval"); - return tokenRefreshInterval; - } - } - - private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") { - if (!text) { - return; - } - - const loadingText = document.getElementById("explorerLoadingStatusText"); - if (!loadingText) { - Logger.logError( - "getElementById('explorerLoadingStatusText') failed to find element", - "Explorer/_setLoadingStatusText" - ); - return; - } - loadingText.innerHTML = text; - - const loadingTitle = document.getElementById("explorerLoadingStatusTitle"); - if (!loadingTitle) { - Logger.logError( - "getElementById('explorerLoadingStatusTitle') failed to find element", - "Explorer/_setLoadingStatusText" - ); - } else { - loadingTitle.innerHTML = title; - } - } - - private _openSetupNotebooksPaneForQuickstart(): void { - const title = "Enable Notebooks (Preview)"; - const description = - "You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account."; - - this.setupNotebooksPane.openWithTitleAndDescription(title, description); - } - - public async handleOpenFileAction(path: string): Promise { - if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(this.databaseAccount()))) { - this.closeAllPanes(); - this._openSetupNotebooksPaneForQuickstart(); - } - - // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb - // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly - // calling GitHub. For now convert this url to a raw url and download content. - const gitHubInfo = fromContentUri(path); - if (gitHubInfo) { - const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); - const response = await fetch(rawUrl); - if (response.status === Constants.HttpStatusCodes.OK) { - this.notebookToImport = { - name: NotebookUtil.getName(path), - content: await response.text() - }; - - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - } - } - - public async loadSelectedDatabaseOffer(): Promise { - const database = this.findSelectedDatabase(); - await database?.loadOffer(); - } - - public async loadDatabaseOffers(): Promise { - await Promise.all( - this.databases()?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }) - ); - } - - public isFirstResourceCreated(): boolean { - const databases: ViewModels.Database[] = this.databases(); - - if (!databases || databases.length === 0) { - return false; - } - - return databases.some(database => { - // user has created at least one collection - if (database.collections()?.length > 0) { - return true; - } - // user has created a database with shared throughput - if (database.offer()) { - return true; - } - // use has created an empty database without shared throughput - return false; - }); - } -} +import * as ComponentRegisterer from "./ComponentRegisterer"; +import * as Constants from "../Common/Constants"; +import * as DataModels from "../Contracts/DataModels"; +import * as ko from "knockout"; +import * as MostRecentActivity from "./MostRecentActivity/MostRecentActivity"; +import * as path from "path"; +import * as SharedConstants from "../Shared/Constants"; +import * as ViewModels from "../Contracts/ViewModels"; +import _ from "underscore"; +import AddCollectionPane from "./Panes/AddCollectionPane"; +import AddDatabasePane from "./Panes/AddDatabasePane"; +import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; +import AuthHeadersUtil from "../Platform/Hosted/Authorization"; +import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; +import Database from "./Tree/Database"; +import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; +import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; +import { readCollection } from "../Common/dataAccess/readCollection"; +import { readDatabases } from "../Common/dataAccess/readDatabases"; +import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; +import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; +import GraphStylingPane from "./Panes/GraphStylingPane"; +import hasher from "hasher"; +import NewVertexPane from "./Panes/NewVertexPane"; +import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; +import Q from "q"; +import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; +import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; +import TerminalTab from "./Tabs/TerminalTab"; +import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; +import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts"; +import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager"; +import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; +import { AuthType } from "../AuthType"; +import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; +import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; +import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; +import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; +import { configContext, Platform, updateConfigContext } from "../ConfigContext"; +import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; +import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; +import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; +import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter"; +import { DialogProps, TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent"; +import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane"; +import { ExplorerMetrics } from "../Common/Constants"; +import { ExplorerSettings } from "../Shared/ExplorerSettings"; +import { FileSystemUtil } from "./Notebook/FileSystemUtil"; +import { handleOpenAction } from "./OpenActions"; +import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; +import { IGalleryItem } from "../Juno/JunoClient"; +import { LoadQueryPane } from "./Panes/LoadQueryPane"; +import * as Logger from "../Common/Logger"; +import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler"; +import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; +import { NotebookUtil } from "./Notebook/NotebookUtil"; +import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; +import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter"; +import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; +import { QueriesClient } from "../Common/QueriesClient"; +import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; +import { RenewAdHocAccessPane } from "./Panes/RenewAdHocAccessPane"; +import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; +import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; +import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; +import { RouteHandler } from "../RouteHandlers/RouteHandler"; +import { SaveQueryPane } from "./Panes/SaveQueryPane"; +import { SettingsPane } from "./Panes/SettingsPane"; +import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; +import { SplashScreenComponentAdapter } from "./SplashScreen/SplashScreenComponentApdapter"; +import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"; +import { StringInputPane } from "./Panes/StringInputPane"; +import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; +import { TabsManager } from "./Tabs/TabsManager"; +import { UploadFilePane } from "./Panes/UploadFilePane"; +import { UploadItemsPane } from "./Panes/UploadItemsPane"; +import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter"; +import { ReactAdapter } from "../Bindings/ReactBindingHandler"; +import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils"; +import UserDefinedFunction from "./Tree/UserDefinedFunction"; +import StoredProcedure from "./Tree/StoredProcedure"; +import Trigger from "./Tree/Trigger"; +import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; +import TabsBase from "./Tabs/TabsBase"; +import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; +import { updateUserContext, userContext } from "../UserContext"; +import { stringToBlob } from "../Utils/BlobUtils"; +import { IChoiceGroupProps } from "office-ui-fabric-react"; +import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils"; +import { SubscriptionType } from "../Contracts/SubscriptionType"; +import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter"; +import { SelfServeType } from "../SelfServe/SelfServeUtils"; +import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter"; + +BindingHandlersRegisterer.registerBindingHandlers(); +// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import +var tmp = ComponentRegisterer; + +enum ShareAccessToggleState { + ReadWrite, + Read, +} + +interface AdHocAccessData { + readWriteUrl: string; + readUrl: string; +} + +export default class Explorer { + public flight: ko.Observable = ko.observable( + SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight + ); + + public addCollectionText: ko.Observable; + public addDatabaseText: ko.Observable; + public collectionTitle: ko.Observable; + public deleteCollectionText: ko.Observable; + public deleteDatabaseText: ko.Observable; + public collectionTreeNodeAltText: ko.Observable; + public refreshTreeTitle: ko.Observable; + public hasWriteAccess: ko.Observable; + public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; + + public databaseAccount: ko.Observable; + public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; + public subscriptionType: ko.Observable; + public defaultExperience: ko.Observable; + public isPreferredApiDocumentDB: ko.Computed; + public isPreferredApiCassandra: ko.Computed; + public isPreferredApiMongoDB: ko.Computed; + public isPreferredApiGraph: ko.Computed; + public isPreferredApiTable: ko.Computed; + public isFixedCollectionWithSharedThroughputSupported: ko.Computed; + public isEnableMongoCapabilityPresent: ko.Computed; + public isServerlessEnabled: ko.Computed; + public isAccountReady: ko.Observable; + public selfServeType: ko.Observable; + public canSaveQueries: ko.Computed; + public features: ko.Observable; + public serverId: ko.Observable; + public isTryCosmosDBSubscription: ko.Observable; + public queriesClient: QueriesClient; + public tableDataClient: TableDataClient; + public splitter: Splitter; + public mostRecentActivity: MostRecentActivity.MostRecentActivity; + + // Notification Console + public notificationConsoleData: ko.ObservableArray; + public isNotificationConsoleExpanded: ko.Observable; + + // Panes + public contextPanes: ContextualPaneBase[]; + + // Resource Tree + public databases: ko.ObservableArray; + public nonSystemDatabases: ko.Computed; + public selectedDatabaseId: ko.Computed; + public selectedCollectionId: ko.Computed; + public isLeftPaneExpanded: ko.Observable; + public selectedNode: ko.Observable; + public isRefreshingExplorer: ko.Observable; + private resourceTree: ResourceTreeAdapter; + private selfServeComponentAdapter: SelfServeComponentAdapter; + + // Resource Token + public resourceTokenDatabaseId: ko.Observable; + public resourceTokenCollectionId: ko.Observable; + public resourceTokenCollection: ko.Observable; + public resourceTokenPartitionKey: ko.Observable; + public isAuthWithResourceToken: ko.Observable; + public isResourceTokenCollectionNodeSelected: ko.Computed; + private resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; + + // Tabs + public isTabsContentExpanded: ko.Observable; + public galleryTab: any; + public notebookViewerTab: any; + public tabsManager: TabsManager; + + // Contextual panes + public addDatabasePane: AddDatabasePane; + public addCollectionPane: AddCollectionPane; + public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane; + public deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane; + public graphStylingPane: GraphStylingPane; + public addTableEntityPane: AddTableEntityPane; + public editTableEntityPane: EditTableEntityPane; + public tableColumnOptionsPane: TableColumnOptionsPane; + public querySelectPane: QuerySelectPane; + public newVertexPane: NewVertexPane; + public cassandraAddCollectionPane: CassandraAddCollectionPane; + public settingsPane: SettingsPane; + public executeSprocParamsPane: ExecuteSprocParamsPane; + public renewAdHocAccessPane: RenewAdHocAccessPane; + public uploadItemsPane: UploadItemsPane; + public uploadItemsPaneAdapter: UploadItemsPaneAdapter; + public loadQueryPane: LoadQueryPane; + public saveQueryPane: ContextualPaneBase; + public browseQueriesPane: BrowseQueriesPane; + public uploadFilePane: UploadFilePane; + public stringInputPane: StringInputPane; + public setupNotebooksPane: SetupNotebooksPane; + public gitHubReposPane: ContextualPaneBase; + public publishNotebookPaneAdapter: ReactAdapter; + public copyNotebookPaneAdapter: ReactAdapter; + + // features + public isGalleryPublishEnabled: ko.Computed; + public isLinkInjectionEnabled: ko.Computed; + public isGitHubPaneEnabled: ko.Observable; + public isPublishNotebookPaneEnabled: ko.Observable; + public isCopyNotebookPaneEnabled: ko.Observable; + public isHostedDataExplorerEnabled: ko.Computed; + public isRightPanelV2Enabled: ko.Computed; + public isMongoIndexingEnabled: ko.Observable; + public canExceedMaximumValue: ko.Computed; + public isAutoscaleDefaultEnabled: ko.Observable; + + public shouldShowShareDialogContents: ko.Observable; + public shareAccessData: ko.Observable; + public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise; + public renewTokenError: ko.Observable; + public tokenForRenewal: ko.Observable; + public shareAccessToggleState: ko.Observable; + public shareAccessUrl: ko.Observable; + public shareUrlCopyHelperText: ko.Observable; + public shareTokenCopyHelperText: ko.Observable; + public shouldShowDataAccessExpiryDialog: ko.Observable; + public shouldShowContextSwitchPrompt: ko.Observable; + public isSchemaEnabled: ko.Computed; + + // Notebooks + public isNotebookEnabled: ko.Observable; + public isNotebooksEnabledForAccount: ko.Observable; + public notebookServerInfo: ko.Observable; + public notebookWorkspaceManager: NotebookWorkspaceManager; + public sparkClusterConnectionInfo: ko.Observable; + public isSparkEnabled: ko.Observable; + public isSparkEnabledForAccount: ko.Observable; + public arcadiaToken: ko.Observable; + public arcadiaWorkspaces: ko.ObservableArray; + public hasStorageAnalyticsAfecFeature: ko.Observable; + public isSynapseLinkUpdating: ko.Observable; + public memoryUsageInfo: ko.Observable; + public notebookManager?: any; // This is dynamically loaded + + private _panes: ContextualPaneBase[] = []; + private _importExplorerConfigComplete: boolean = false; + private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = (database) => false; + private _isInitializingNotebooks: boolean; + private _isInitializingSparkConnectionInfo: boolean; + private notebookBasePath: ko.Observable; + private _arcadiaManager: ArcadiaResourceManager; + private notebookToImport: { + name: string; + content: string; + }; + + // React adapters + private commandBarComponentAdapter: CommandBarComponentAdapter; + private splashScreenAdapter: SplashScreenComponentAdapter; + private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter; + private dialogComponentAdapter: DialogComponentAdapter; + private _dialogProps: ko.Observable; + private addSynapseLinkDialog: DialogComponentAdapter; + private _addSynapseLinkDialogProps: ko.Observable; + private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter; + + private static readonly MaxNbDatabasesToAutoExpand = 5; + + constructor() { + const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { + dataExplorerArea: Constants.Areas.ResourceTree, + }); + this.addCollectionText = ko.observable("New Collection"); + this.addDatabaseText = ko.observable("New Database"); + this.hasWriteAccess = ko.observable(true); + this.collectionTitle = ko.observable("Collections"); + this.collectionTreeNodeAltText = ko.observable("Collection"); + this.deleteCollectionText = ko.observable("Delete Collection"); + this.deleteDatabaseText = ko.observable("Delete Database"); + this.refreshTreeTitle = ko.observable("Refresh collections"); + + this.databaseAccount = ko.observable(); + this.subscriptionType = ko.observable(SharedConstants.CollectionCreation.DefaultSubscriptionType); + let firstInitialization = true; + this.isRefreshingExplorer = ko.observable(true); + this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { + if (!isRefreshing && firstInitialization) { + // set focus on first element + firstInitialization = false; + try { + document.getElementById("createNewContainerCommandButton").parentElement.parentElement.focus(); + } catch (e) { + Logger.logWarning( + "getElementById('createNewContainerCommandButton') failed to find element", + "Explorer/this.isRefreshingExplorer.subscribe" + ); + } + } + }); + this.isAccountReady = ko.observable(false); + this.selfServeType = ko.observable(undefined); + this._isInitializingNotebooks = false; + this._isInitializingSparkConnectionInfo = false; + this.arcadiaToken = ko.observable(); + this.arcadiaToken.subscribe((token: string) => { + if (token) { + const notebookTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2); + (notebookTabs || []).forEach((tab: NotebookV2Tab) => { + tab.reconfigureServiceEndpoints(); + }); + } + }); + this.isNotebooksEnabledForAccount = ko.observable(false); + this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); + this.isSparkEnabledForAccount = ko.observable(false); + this.isSparkEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); + this.hasStorageAnalyticsAfecFeature = ko.observable(false); + this.hasStorageAnalyticsAfecFeature.subscribe((enabled: boolean) => this.refreshCommandBarButtons()); + this.isSynapseLinkUpdating = ko.observable(false); + this.isAccountReady.subscribe(async (isAccountReady: boolean) => { + if (isAccountReady) { + this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); + RouteHandler.getInstance().initHandler(); + this.notebookWorkspaceManager = new NotebookWorkspaceManager(); + this.arcadiaWorkspaces = ko.observableArray(); + this._arcadiaManager = new ArcadiaResourceManager(); + this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then((isRegistered) => + this.hasStorageAnalyticsAfecFeature(isRegistered) + ); + Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then( + async () => { + this.isNotebookEnabled( + !this.isAuthWithResourceToken() && + ((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) || + this.isFeatureEnabled(Constants.Features.enableNotebooks)) + ); + + TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { + isNotebookEnabled: this.isNotebookEnabled(), + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + }); + + if (this.isNotebookEnabled()) { + await this.initNotebooks(this.databaseAccount()); + const workspaces = await this._getArcadiaWorkspaces(); + this.arcadiaWorkspaces(workspaces); + } else if (this.notebookToImport) { + // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane + this._openSetupNotebooksPaneForQuickstart(); + } + + this.isSparkEnabled( + (this.isNotebookEnabled() && + this.isSparkEnabledForAccount() && + this.arcadiaWorkspaces() && + this.arcadiaWorkspaces().length > 0) || + this.isFeatureEnabled(Constants.Features.enableSpark) + ); + if (this.isSparkEnabled()) { + const pollArcadiaTokenRefresh = async () => { + this.arcadiaToken(await this.getArcadiaToken()); + setTimeout(() => pollArcadiaTokenRefresh(), this.getTokenRefreshInterval(this.arcadiaToken())); + }; + await pollArcadiaTokenRefresh(); + } + } + ); + } + }); + this.memoryUsageInfo = ko.observable(); + + this.features = ko.observable(); + this.serverId = ko.observable(); + this.queriesClient = new QueriesClient(this); + this.isTryCosmosDBSubscription = ko.observable(false); + + this.resourceTokenDatabaseId = ko.observable(); + this.resourceTokenCollectionId = ko.observable(); + this.resourceTokenCollection = ko.observable(); + this.resourceTokenPartitionKey = ko.observable(); + this.isAuthWithResourceToken = ko.observable(false); + + this.shareAccessData = ko.observable({ + readWriteUrl: undefined, + readUrl: undefined, + }); + this.tokenForRenewal = ko.observable(""); + this.renewTokenError = ko.observable(""); + this.shareAccessUrl = ko.observable(); + this.shareUrlCopyHelperText = ko.observable("Click to copy"); + this.shareTokenCopyHelperText = ko.observable("Click to copy"); + this.shareAccessToggleState = ko.observable(ShareAccessToggleState.ReadWrite); + this.shareAccessToggleState.subscribe((toggleState: ShareAccessToggleState) => { + if (toggleState === ShareAccessToggleState.ReadWrite) { + this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readWriteUrl); + } else { + this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readUrl); + } + }); + this.shouldShowShareDialogContents = ko.observable(false); + this.shouldShowDataAccessExpiryDialog = ko.observable(false); + this.shouldShowContextSwitchPrompt = ko.observable(false); + this.isGalleryPublishEnabled = ko.computed(() => + this.isFeatureEnabled(Constants.Features.enableGalleryPublish) + ); + this.isLinkInjectionEnabled = ko.computed(() => + this.isFeatureEnabled(Constants.Features.enableLinkInjection) + ); + this.isGitHubPaneEnabled = ko.observable(false); + this.isMongoIndexingEnabled = ko.observable(false); + this.isPublishNotebookPaneEnabled = ko.observable(false); + this.isCopyNotebookPaneEnabled = ko.observable(false); + + this.canExceedMaximumValue = ko.computed(() => + this.isFeatureEnabled(Constants.Features.canExceedMaximumValue) + ); + + this.isSchemaEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSchema)); + this.isNotificationConsoleExpanded = ko.observable(false); + + this.isAutoscaleDefaultEnabled = ko.observable(false); + + this.databases = ko.observableArray(); + this.canSaveQueries = ko.computed(() => { + const savedQueriesDatabase: ViewModels.Database = _.find( + this.databases(), + (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName + ); + if (!savedQueriesDatabase) { + return false; + } + const savedQueriesCollection: ViewModels.Collection = + savedQueriesDatabase && + _.find( + savedQueriesDatabase.collections(), + (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName + ); + if (!savedQueriesCollection) { + return false; + } + return true; + }); + this.isLeftPaneExpanded = ko.observable(true); + this.selectedNode = ko.observable(); + this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { + // Make sure switching tabs restores tabs display + this.isTabsContentExpanded(false); + }); + this.isResourceTokenCollectionNodeSelected = ko.computed(() => { + return ( + this.selectedNode() && + this.resourceTokenCollection() && + this.selectedNode().id() === this.resourceTokenCollection().id() + ); + }); + + const splitterBounds: SplitterBounds = { + min: ExplorerMetrics.SplitterMinWidth, + max: ExplorerMetrics.SplitterMaxWidth, + }; + this.splitter = new Splitter({ + splitterId: "h_splitter1", + leftId: "resourcetree", + bounds: splitterBounds, + direction: SplitterDirection.Vertical, + }); + this.notificationConsoleData = ko.observableArray([]); + this.defaultExperience = ko.observable(); + this.databaseAccount.subscribe((databaseAccount) => { + const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount( + databaseAccount + ); + this.defaultExperience(defaultExperience); + updateUserContext({ + defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience), + }); + }); + + this.isPreferredApiDocumentDB = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.DocumentDB.toLowerCase(); + }); + + this.isPreferredApiCassandra = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase(); + }); + this.isPreferredApiGraph = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase(); + }); + + this.isPreferredApiTable = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase(); + }); + + this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { + if (this.isFeatureEnabled(Constants.Features.enableFixedCollectionWithSharedThroughput)) { + return true; + } + + if (this.databaseAccount && !this.databaseAccount()) { + return false; + } + + return this.isEnableMongoCapabilityPresent(); + }); + + this.isServerlessEnabled = ko.computed( + () => + this.databaseAccount && + this.databaseAccount()?.properties?.capabilities?.find( + (item) => item.name === Constants.CapabilityNames.EnableServerless + ) !== undefined + ); + + this.isPreferredApiMongoDB = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) { + return true; + } + + if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) { + return true; + } + + if ( + this.databaseAccount && + this.databaseAccount() && + this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB + ) { + return true; + } + + return false; + }); + + this.isEnableMongoCapabilityPresent = ko.computed(() => { + const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities; + if (!capabilities) { + return false; + } + + for (let i = 0; i < capabilities.length; i++) { + if (typeof capabilities[i] === "object" && capabilities[i].name === Constants.CapabilityNames.EnableMongo) { + return true; + } + } + + return false; + }); + + this.isHostedDataExplorerEnabled = ko.computed( + () => + configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() + ); + this.isRightPanelV2Enabled = ko.computed(() => + this.isFeatureEnabled(Constants.Features.enableRightPanelV2) + ); + this.defaultExperience.subscribe((defaultExperience: string) => { + if ( + defaultExperience && + defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase() + ) { + this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => { + return database.id() === "system"; + }; + } + }); + + this.selectedDatabaseId = ko.computed(() => { + const selectedNode = this.selectedNode(); + if (!selectedNode) { + return ""; + } + + switch (selectedNode.nodeKind) { + case "Collection": + return (selectedNode as ViewModels.CollectionBase).databaseId || ""; + case "Database": + return selectedNode.id() || ""; + case "DocumentId": + case "StoredProcedure": + case "Trigger": + case "UserDefinedFunction": + return selectedNode.collection.databaseId || ""; + default: + return ""; + } + }); + + this.nonSystemDatabases = ko.computed(() => { + return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database)); + }); + + this.addDatabasePane = new AddDatabasePane({ + id: "adddatabasepane", + visible: ko.observable(false), + + container: this, + }); + + this.addCollectionPane = new AddCollectionPane({ + isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), + id: "addcollectionpane", + visible: ko.observable(false), + + container: this, + }); + + this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({ + id: "deletecollectionconfirmationpane", + visible: ko.observable(false), + + container: this, + }); + + this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({ + id: "deletedatabaseconfirmationpane", + visible: ko.observable(false), + + container: this, + }); + + this.graphStylingPane = new GraphStylingPane({ + id: "graphstylingpane", + visible: ko.observable(false), + + container: this, + }); + + this.addTableEntityPane = new AddTableEntityPane({ + id: "addtableentitypane", + visible: ko.observable(false), + + container: this, + }); + + this.editTableEntityPane = new EditTableEntityPane({ + id: "edittableentitypane", + visible: ko.observable(false), + + container: this, + }); + + this.tableColumnOptionsPane = new TableColumnOptionsPane({ + id: "tablecolumnoptionspane", + visible: ko.observable(false), + + container: this, + }); + + this.querySelectPane = new QuerySelectPane({ + id: "queryselectpane", + visible: ko.observable(false), + + container: this, + }); + + this.newVertexPane = new NewVertexPane({ + id: "newvertexpane", + visible: ko.observable(false), + + container: this, + }); + + this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ + id: "cassandraaddcollectionpane", + visible: ko.observable(false), + + container: this, + }); + + this.settingsPane = new SettingsPane({ + id: "settingspane", + visible: ko.observable(false), + + container: this, + }); + + this.executeSprocParamsPane = new ExecuteSprocParamsPane({ + id: "executesprocparamspane", + visible: ko.observable(false), + + container: this, + }); + + this.renewAdHocAccessPane = new RenewAdHocAccessPane({ + id: "renewadhocaccesspane", + visible: ko.observable(false), + + container: this, + }); + + this.uploadItemsPane = new UploadItemsPane({ + id: "uploaditemspane", + visible: ko.observable(false), + + container: this, + }); + + this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); + this.selfServeComponentAdapter = new SelfServeComponentAdapter(this); + + this.loadQueryPane = new LoadQueryPane({ + id: "loadquerypane", + visible: ko.observable(false), + + container: this, + }); + + this.saveQueryPane = new SaveQueryPane({ + id: "savequerypane", + visible: ko.observable(false), + + container: this, + }); + + this.browseQueriesPane = new BrowseQueriesPane({ + id: "browsequeriespane", + visible: ko.observable(false), + + container: this, + }); + + this.uploadFilePane = new UploadFilePane({ + id: "uploadfilepane", + visible: ko.observable(false), + + container: this, + }); + + this.stringInputPane = new StringInputPane({ + id: "stringinputpane", + visible: ko.observable(false), + + container: this, + }); + + this.setupNotebooksPane = new SetupNotebooksPane({ + id: "setupnotebookspane", + visible: ko.observable(false), + + container: this, + }); + + this.tabsManager = new TabsManager(); + + this._panes = [ + this.addDatabasePane, + this.addCollectionPane, + this.deleteCollectionConfirmationPane, + this.deleteDatabaseConfirmationPane, + this.graphStylingPane, + this.addTableEntityPane, + this.editTableEntityPane, + this.tableColumnOptionsPane, + this.querySelectPane, + this.newVertexPane, + this.cassandraAddCollectionPane, + this.settingsPane, + this.executeSprocParamsPane, + this.renewAdHocAccessPane, + this.uploadItemsPane, + this.loadQueryPane, + this.saveQueryPane, + this.browseQueriesPane, + this.uploadFilePane, + this.stringInputPane, + this.setupNotebooksPane, + ]; + this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); + this.isTabsContentExpanded = ko.observable(false); + + document.addEventListener( + "contextmenu", + function (e) { + e.preventDefault(); + }, + false + ); + + $(function () { + $(document.body).click(() => $(".commandDropdownContainer").hide()); + }); + + // TODO move this to API customization class + this.defaultExperience.subscribe((defaultExperience) => { + const defaultExperienceNormalizedString = ( + defaultExperience || Constants.DefaultAccountExperience.Default + ).toLowerCase(); + + switch (defaultExperienceNormalizedString) { + case Constants.DefaultAccountExperience.DocumentDB.toLowerCase(): + this.addCollectionText("New Container"); + this.addDatabaseText("New Database"); + this.collectionTitle("SQL API"); + this.collectionTreeNodeAltText("Container"); + this.deleteCollectionText("Delete Container"); + this.deleteDatabaseText("Delete Database"); + this.addCollectionPane.title("Add Container"); + this.addCollectionPane.collectionIdTitle("Container id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle( + "Provision dedicated throughput for this container" + ); + this.deleteCollectionConfirmationPane.title("Delete Container"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); + this.refreshTreeTitle("Refresh containers"); + break; + case Constants.DefaultAccountExperience.MongoDB.toLowerCase(): + case Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase(): + this.addCollectionText("New Collection"); + this.addDatabaseText("New Database"); + this.collectionTitle("Collections"); + this.collectionTreeNodeAltText("Collection"); + this.deleteCollectionText("Delete Collection"); + this.deleteDatabaseText("Delete Database"); + this.addCollectionPane.title("Add Collection"); + this.addCollectionPane.collectionIdTitle("Collection id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle( + "Provision dedicated throughput for this collection" + ); + this.refreshTreeTitle("Refresh collections"); + break; + case Constants.DefaultAccountExperience.Graph.toLowerCase(): + this.addCollectionText("New Graph"); + this.addDatabaseText("New Database"); + this.deleteCollectionText("Delete Graph"); + this.deleteDatabaseText("Delete Database"); + this.collectionTitle("Gremlin API"); + this.collectionTreeNodeAltText("Graph"); + this.addCollectionPane.title("Add Graph"); + this.addCollectionPane.collectionIdTitle("Graph id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); + this.deleteCollectionConfirmationPane.title("Delete Graph"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id"); + this.refreshTreeTitle("Refresh graphs"); + break; + case Constants.DefaultAccountExperience.Table.toLowerCase(): + this.addCollectionText("New Table"); + this.addDatabaseText("New Database"); + this.deleteCollectionText("Delete Table"); + this.deleteDatabaseText("Delete Database"); + this.collectionTitle("Azure Table API"); + this.collectionTreeNodeAltText("Table"); + this.addCollectionPane.title("Add Table"); + this.addCollectionPane.collectionIdTitle("Table id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); + this.refreshTreeTitle("Refresh tables"); + this.addTableEntityPane.title("Add Table Entity"); + this.editTableEntityPane.title("Edit Table Entity"); + this.deleteCollectionConfirmationPane.title("Delete Table"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); + this.tableDataClient = new TablesAPIDataClient(); + break; + case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): + this.addCollectionText("New Table"); + this.addDatabaseText("New Keyspace"); + this.deleteCollectionText("Delete Table"); + this.deleteDatabaseText("Delete Keyspace"); + this.collectionTitle("Cassandra API"); + this.collectionTreeNodeAltText("Table"); + this.addCollectionPane.title("Add Table"); + this.addCollectionPane.collectionIdTitle("Table id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); + this.refreshTreeTitle("Refresh tables"); + this.addTableEntityPane.title("Add Table Row"); + this.editTableEntityPane.title("Edit Table Row"); + this.deleteCollectionConfirmationPane.title("Delete Table"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); + this.deleteDatabaseConfirmationPane.title("Delete Keyspace"); + this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id"); + this.tableDataClient = new CassandraAPIDataClient(); + break; + } + }); + + this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); + this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter(); + this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this); + + this._initSettings(); + + TelemetryProcessor.traceSuccess( + Action.InitializeDataExplorer, + { dataExplorerArea: Constants.Areas.ResourceTree }, + startKey + ); + + this.isNotebookEnabled = ko.observable(false); + this.isNotebookEnabled.subscribe(async () => { + if (!this.notebookManager) { + const notebookManagerModule = await import( + /* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager" + ); + this.notebookManager = new notebookManagerModule.default(); + this.notebookManager.initialize({ + container: this, + dialogProps: this._dialogProps, + notebookBasePath: this.notebookBasePath, + resourceTree: this.resourceTree, + refreshCommandBarButtons: () => this.refreshCommandBarButtons(), + refreshNotebookList: () => this.refreshNotebookList(), + }); + + this.gitHubReposPane = this.notebookManager.gitHubReposPane; + this.isGitHubPaneEnabled(true); + } + + this.refreshCommandBarButtons(); + this.refreshNotebookList(); + }); + + this.isSparkEnabled = ko.observable(false); + this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); + this.resourceTree = new ResourceTreeAdapter(this); + this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); + this.notebookServerInfo = ko.observable({ + notebookServerEndpoint: undefined, + authToken: undefined, + }); + this.notebookBasePath = ko.observable(Constants.Notebook.defaultBasePath); + this.sparkClusterConnectionInfo = ko.observable({ + userName: undefined, + password: undefined, + endpoints: [], + }); + + // Override notebook server parameters from URL parameters + const featureSubcription = this.features.subscribe((features) => { + const serverInfo = this.notebookServerInfo(); + if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { + serverInfo.notebookServerEndpoint = features[Constants.Features.notebookServerUrl]; + } + + if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { + serverInfo.authToken = features[Constants.Features.notebookServerToken]; + } + this.notebookServerInfo(serverInfo); + this.notebookServerInfo.valueHasMutated(); + + if (this.isFeatureEnabled(Constants.Features.notebookBasePath)) { + this.notebookBasePath(features[Constants.Features.notebookBasePath]); + } + + if (this.isFeatureEnabled(Constants.Features.livyEndpoint)) { + this.sparkClusterConnectionInfo({ + userName: undefined, + password: undefined, + endpoints: [ + { + endpoint: features[Constants.Features.livyEndpoint], + kind: DataModels.SparkClusterEndpointKind.Livy, + }, + ], + }); + this.sparkClusterConnectionInfo.valueHasMutated(); + } + + if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) { + updateUserContext({ useSDKOperations: true }); + } + + featureSubcription.dispose(); + }); + + this._dialogProps = ko.observable({ + isModal: false, + visible: false, + title: undefined, + subText: undefined, + primaryButtonText: undefined, + secondaryButtonText: undefined, + onPrimaryButtonClick: undefined, + onSecondaryButtonClick: undefined, + }); + this.dialogComponentAdapter = new DialogComponentAdapter(); + this.dialogComponentAdapter.parameters = this._dialogProps; + this.splashScreenAdapter = new SplashScreenComponentAdapter(this); + this.mostRecentActivity = new MostRecentActivity.MostRecentActivity(this); + + this._addSynapseLinkDialogProps = ko.observable({ + isModal: false, + visible: false, + title: undefined, + subText: undefined, + primaryButtonText: undefined, + secondaryButtonText: undefined, + onPrimaryButtonClick: undefined, + onSecondaryButtonClick: undefined, + }); + this.addSynapseLinkDialog = new DialogComponentAdapter(); + this.addSynapseLinkDialog.parameters = this._addSynapseLinkDialogProps; + } + + public openEnableSynapseLinkDialog(): void { + const addSynapseLinkDialogProps: DialogProps = { + linkProps: { + linkText: "Learn more", + linkUrl: "https://aka.ms/cosmosdb-synapselink", + }, + isModal: true, + visible: true, + title: `Enable Azure Synapse Link on your Cosmos DB account`, + subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads. + Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`, + primaryButtonText: "Enable Azure Synapse Link", + secondaryButtonText: "Cancel", + + onPrimaryButtonClick: async () => { + const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); + const logId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + "Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account." + ); + this.isSynapseLinkUpdating(true); + this._closeSynapseLinkModalDialog(); + + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id); + + try { + const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( + this.databaseAccount().id, + "2019-12-12", + { + properties: { + enableAnalyticalStorage: true, + }, + } + ); + NotificationConsoleUtils.clearInProgressMessageWithId(logId); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + "Enabled Azure Synapse Link for this account" + ); + TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime); + this.databaseAccount(databaseAccount); + } catch (error) { + NotificationConsoleUtils.clearInProgressMessageWithId(logId); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}` + ); + TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime); + } finally { + this.isSynapseLinkUpdating(false); + } + }, + + onSecondaryButtonClick: () => { + this._closeSynapseLinkModalDialog(); + TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink); + }, + }; + this._addSynapseLinkDialogProps(addSynapseLinkDialogProps); + TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); + + // TODO: return result + } + + public copyUrlLink(src: any, event: MouseEvent): void { + const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement; + urlLinkInput && urlLinkInput.select(); + document.execCommand("copy"); + this.shareUrlCopyHelperText("Copied"); + setTimeout(() => this.shareUrlCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); + + TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { + description: "Copy full screen URL", + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ShareDialog, + }); + } + + public onCopyUrlLinkKeyPress(src: any, event: KeyboardEvent): boolean { + if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { + this.copyUrlLink(src, null); + return false; + } + + return true; + } + + public copyToken(src: any, event: MouseEvent): void { + const tokenInput: HTMLInputElement = document.getElementById("shareToken") as HTMLInputElement; + tokenInput && tokenInput.select(); + document.execCommand("copy"); + this.shareTokenCopyHelperText("Copied"); + setTimeout(() => this.shareTokenCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); + } + + public onCopyTokenKeyPress(src: any, event: KeyboardEvent): boolean { + if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { + this.copyToken(src, null); + return false; + } + + return true; + } + + public renewToken = (): void => { + TelemetryProcessor.trace(Action.ConnectEncryptionToken); + this.renewTokenError(""); + const id: string = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + "Initiating connection to account" + ); + this.renewExplorerShareAccess(this, this.tokenForRenewal()) + .fail((error: any) => { + const stringifiedError: string = getErrorMessage(error); + this.renewTokenError("Invalid connection string specified"); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to initiate connection to account: ${stringifiedError}` + ); + }) + .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + }; + + public generateSharedAccessData(): void { + const id: string = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Generating share url"); + AuthHeadersUtil.generateEncryptedToken().then( + (tokenResponse: DataModels.GenerateTokenResponse) => { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully generated share url"); + this.shareAccessData({ + readWriteUrl: this._getShareAccessUrlForToken(tokenResponse.readWrite), + readUrl: this._getShareAccessUrlForToken(tokenResponse.read), + }); + !this.shareAccessData().readWriteUrl && this.shareAccessToggleState(ShareAccessToggleState.Read); // select read toggle by default for readers + this.shareAccessToggleState.valueHasMutated(); // to set initial url and token state + this.shareAccessData.valueHasMutated(); + this._openShareDialog(); + }, + (error: any) => { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to generate share url: ${getErrorMessage(error)}` + ); + console.error(error); + } + ); + } + + public renewShareAccess(token: string): Q.Promise { + if (!this.renewExplorerShareAccess) { + return Q.reject("Not implemented"); + } + + const deferred: Q.Deferred = Q.defer(); + const id: string = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + "Initiating connection to account" + ); + this.renewExplorerShareAccess(this, token) + .then( + () => { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Connection successful"); + this.renewAdHocAccessPane && this.renewAdHocAccessPane.close(); + deferred.resolve(); + }, + (error: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to connect: ${getErrorMessage(error)}` + ); + deferred.reject(error); + } + ) + .finally(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + }); + + return deferred.promise; + } + + public displayGuestAccessTokenRenewalPrompt(): void { + if (!$("#dataAccessTokenModal").dialog("instance")) { + const connectButton = { + text: "Connect", + class: "connectDialogButtons connectButton connectOkBtns", + click: () => { + this.renewAdHocAccessPane.open(); + $("#dataAccessTokenModal").dialog("close"); + }, + }; + const cancelButton = { + text: "Cancel", + class: "connectDialogButtons cancelBtn", + click: () => { + $("#dataAccessTokenModal").dialog("close"); + }, + }; + + $("#dataAccessTokenModal").dialog({ + autoOpen: false, + buttons: [connectButton, cancelButton], + closeOnEscape: false, + draggable: false, + dialogClass: "no-close", + height: 180, + modal: true, + position: { my: "center center", at: "center center", of: window }, + resizable: false, + title: "Temporary access expired", + width: 435, + close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false), + }); + $("#dataAccessTokenModal").dialog("option", "classes", { + "ui-dialog-titlebar": "connectTitlebar", + }); + } + this.shouldShowDataAccessExpiryDialog(true); + $("#dataAccessTokenModal").dialog("open"); + } + + public isConnectExplorerVisible(): boolean { + return $("#connectExplorer").is(":visible") || false; + } + + public displayContextSwitchPromptForConnectionString(connectionString: string): void { + const yesButton = { + text: "OK", + class: "connectDialogButtons okBtn connectOkBtns", + click: () => { + $("#contextSwitchPrompt").dialog("close"); + this.tabsManager.closeTabs(); // clear all tabs so we dont leave any tabs from previous session open + this.renewShareAccess(connectionString); + }, + }; + const noButton = { + text: "Cancel", + class: "connectDialogButtons cancelBtn", + click: () => { + $("#contextSwitchPrompt").dialog("close"); + }, + }; + + if (!$("#contextSwitchPrompt").dialog("instance")) { + $("#contextSwitchPrompt").dialog({ + autoOpen: false, + buttons: [yesButton, noButton], + closeOnEscape: false, + draggable: false, + dialogClass: "no-close", + height: 255, + modal: true, + position: { my: "center center", at: "center center", of: window }, + resizable: false, + title: "Switch account", + width: 440, + close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false), + }); + $("#contextSwitchPrompt").dialog("option", "classes", { + "ui-dialog-titlebar": "connectTitlebar", + }); + $("#contextSwitchPrompt").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => { + $(".ui-dialog ").css("z-index", 1001); + $("#contextSwitchPrompt").parent().siblings(".ui-widget-overlay").css("z-index", 1000); + }); + } + $("#contextSwitchPrompt").dialog("option", "buttons", [yesButton, noButton]); // rebind buttons so callbacks accept current connection string + this.shouldShowContextSwitchPrompt(true); + $("#contextSwitchPrompt").dialog("open"); + } + + public displayConnectExplorerForm(): void { + $("#divExplorer").hide(); + $("#connectExplorer").css("display", "flex"); + } + + public hideConnectExplorerForm(): void { + $("#connectExplorer").hide(); + $("#divExplorer").show(); + } + + public isReadWriteToggled: () => boolean = (): boolean => { + return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite; + }; + + public isReadToggled: () => boolean = (): boolean => { + return this.shareAccessToggleState() === ShareAccessToggleState.Read; + }; + + public toggleReadWrite: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { + this.shareAccessToggleState(ShareAccessToggleState.ReadWrite); + }; + + public toggleRead: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { + this.shareAccessToggleState(ShareAccessToggleState.Read); + }; + + public onToggleKeyDown: (src: any, event: KeyboardEvent) => boolean = (src: any, event: KeyboardEvent) => { + if (event.keyCode === Constants.KeyCodes.LeftArrow) { + this.toggleReadWrite(src, null); + return false; + } else if (event.keyCode === Constants.KeyCodes.RightArrow) { + this.toggleRead(src, null); + return false; + } + return true; + }; + + public isDatabaseNodeOrNoneSelected(): boolean { + return this.isNoneSelected() || this.isDatabaseNodeSelected(); + } + + public isDatabaseNodeSelected(): boolean { + return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false; + } + + public isNodeKindSelected(nodeKind: string): boolean { + return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false; + } + + public isNoneSelected(): boolean { + return this.selectedNode() == null; + } + + public isFeatureEnabled(feature: string): boolean { + const features = this.features(); + + if (!features) { + return false; + } + + if (feature in features && features[feature]) { + return true; + } + + return false; + } + + public logConsoleData(consoleData: ConsoleData): void { + this.notificationConsoleData.splice(0, 0, consoleData); + } + + public deleteInProgressConsoleDataWithId(id: string): void { + const updatedConsoleData = _.reject( + this.notificationConsoleData(), + (data: ConsoleData) => data.type === ConsoleDataType.InProgress && data.id === id + ); + this.notificationConsoleData(updatedConsoleData); + } + + public expandConsole(): void { + this.isNotificationConsoleExpanded(true); + } + + public collapseConsole(): void { + this.isNotificationConsoleExpanded(false); + } + + public toggleLeftPaneExpanded() { + this.isLeftPaneExpanded(!this.isLeftPaneExpanded()); + + if (this.isLeftPaneExpanded()) { + document.getElementById("expandToggleLeftPaneButton").focus(); + this.splitter.expandLeft(); + } else { + document.getElementById("collapseToggleLeftPaneButton").focus(); + this.splitter.collapseLeft(); + } + } + + public refreshDatabaseForResourceToken(): Q.Promise { + const databaseId = this.resourceTokenDatabaseId(); + const collectionId = this.resourceTokenCollectionId(); + if (!databaseId || !collectionId) { + return Q.reject(); + } + + const deferred: Q.Deferred = Q.defer(); + readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { + this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); + this.selectedNode(this.resourceTokenCollection()); + deferred.resolve(); + }); + + return deferred.promise; + } + + public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise { + this.isRefreshingExplorer(true); + const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + let resourceTreeStartKey: number = null; + if (isInitialLoad) { + resourceTreeStartKey = TelemetryProcessor.traceStart(Action.LoadResourceTree, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + } + + // TODO: Refactor + const deferred: Q.Deferred = Q.defer(); + this._setLoadingStatusText("Fetching databases..."); + readDatabases().then( + (databases: DataModels.Database[]) => { + this._setLoadingStatusText("Successfully fetched databases."); + TelemetryProcessor.traceSuccess( + Action.LoadDatabases, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }, + startKey + ); + const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); + const deltaDatabases = this.getDeltaDatabases(databases); + this.addDatabasesToList(deltaDatabases.toAdd); + this.deleteDatabasesFromList(deltaDatabases.toDelete); + this.selectedNode(currentlySelectedNode); + this._setLoadingStatusText("Fetching containers..."); + this.refreshAndExpandNewDatabases(deltaDatabases.toAdd) + .then( + () => { + this._setLoadingStatusText("Successfully fetched containers."); + deferred.resolve(); + }, + (reason) => { + this._setLoadingStatusText("Failed to fetch containers."); + deferred.reject(reason); + } + ) + .finally(() => this.isRefreshingExplorer(false)); + }, + (error) => { + this._setLoadingStatusText("Failed to fetch databases."); + this.isRefreshingExplorer(false); + deferred.reject(error); + const errorMessage = getErrorMessage(error); + TelemetryProcessor.traceFailure( + Action.LoadDatabases, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Error while refreshing databases: ${errorMessage}` + ); + } + ); + + return deferred.promise.then( + () => { + if (resourceTreeStartKey != null) { + TelemetryProcessor.traceSuccess( + Action.LoadResourceTree, + { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }, + resourceTreeStartKey + ); + } + }, + (error) => { + if (resourceTreeStartKey != null) { + TelemetryProcessor.traceFailure( + Action.LoadResourceTree, + { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + resourceTreeStartKey + ); + } + } + ); + } + + public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.onRefreshResourcesClick(source, null); + return false; + } + return true; + }; + + public onRefreshResourcesClick = (source: any, event: MouseEvent): void => { + const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { + description: "Refresh button clicked", + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + this.isRefreshingExplorer(true); + this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); + this.refreshNotebookList(); + }; + + public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.toggleLeftPaneExpanded(); + return false; + } + return true; + }; + + // Facade + public provideFeedbackEmail = () => { + window.open(Constants.Urls.feedbackEmail, "_self"); + }; + + public async getArcadiaToken(): Promise { + return new Promise((resolve: (token: string) => void, reject: (error: any) => void) => { + sendCachedDataMessage(MessageTypes.GetArcadiaToken, undefined /** params **/).then( + (token: string) => { + resolve(token); + }, + (error: any) => { + Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken"); + resolve(undefined); + } + ); + }); + } + + private async _getArcadiaWorkspaces(): Promise { + try { + const workspaces = await this._arcadiaManager.listWorkspacesAsync([userContext.subscriptionId]); + let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length); + const sparkPromises: Promise[] = []; + workspaces.forEach((workspace, i) => { + let promise = this._arcadiaManager.listSparkPoolsAsync(workspaces[i].id).then( + (sparkpools) => { + workspaceItems[i] = { ...workspace, sparkPools: sparkpools }; + }, + (error) => { + Logger.logError(getErrorMessage(error), "Explorer/this._arcadiaManager.listSparkPoolsAsync"); + } + ); + sparkPromises.push(promise); + }); + + return Promise.all(sparkPromises).then(() => workspaceItems); + } catch (error) { + handleError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed"); + return Promise.resolve([]); + } + } + + public async createWorkspace(): Promise { + return sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/); + } + + public async createSparkPool(workspaceId: string): Promise { + return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]); + } + + public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + throw new Error("No database account specified"); + } + + if (this._isInitializingNotebooks) { + return; + } + this._isInitializingNotebooks = true; + + await this.ensureNotebookWorkspaceRunning(); + let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: undefined, + notebookServerEndpoint: undefined, + }; + try { + connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync( + databaseAccount.id, + "default" + ); + } catch (error) { + this._isInitializingNotebooks = false; + handleError( + error, + "initNotebooks/getNotebookConnectionInfoAsync", + `Failed to get notebook workspace connection info: ${getErrorMessage(error)}` + ); + throw error; + } finally { + // Overwrite with feature flags + if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { + connectionInfo.notebookServerEndpoint = this.features()[Constants.Features.notebookServerUrl]; + } + + if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { + connectionInfo.authToken = this.features()[Constants.Features.notebookServerToken]; + } + + this.notebookServerInfo(connectionInfo); + this.notebookServerInfo.valueHasMutated(); + this.refreshNotebookList(); + } + + this._isInitializingNotebooks = false; + } + + public resetNotebookWorkspace() { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) { + handleError( + "Attempt to reset notebook workspace, but notebook is not enabled", + "Explorer/resetNotebookWorkspace" + ); + return; + } + const resetConfirmationDialogProps: DialogProps = { + isModal: true, + visible: true, + title: "Reset Workspace", + subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?", + primaryButtonText: "OK", + secondaryButtonText: "Cancel", + onPrimaryButtonClick: this._resetNotebookWorkspace, + onSecondaryButtonClick: this._closeModalDialog, + }; + this._dialogProps(resetConfirmationDialogProps); + } + + private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + return false; + } + + try { + const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id); + return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); + return false; + } + } + + private async ensureNotebookWorkspaceRunning() { + if (!this.databaseAccount()) { + return; + } + + let clearMessage; + try { + const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync( + this.databaseAccount().id, + "default" + ); + if ( + notebookWorkspace && + notebookWorkspace.properties && + notebookWorkspace.properties.status && + notebookWorkspace.properties.status.toLowerCase() === "stopped" + ) { + clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace"); + await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); + } + } catch (error) { + handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace"); + } finally { + clearMessage && clearMessage(); + } + } + + private _resetNotebookWorkspace = async () => { + this._closeModalDialog(); + const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace"); + try { + await this.notebookManager?.notebookClient.resetWorkspace(); + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace"); + TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); + } catch (error) { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`); + TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }); + throw error; + } finally { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + } + }; + + private _closeModalDialog = () => { + this._dialogProps().visible = false; + this._dialogProps.valueHasMutated(); + }; + + private _closeSynapseLinkModalDialog = () => { + this._addSynapseLinkDialogProps().visible = false; + this._addSynapseLinkDialogProps.valueHasMutated(); + }; + + private _shouldProcessMessage(event: MessageEvent): boolean { + if (typeof event.data !== "object") { + return false; + } + if (event.data["signature"] !== "pcIframe") { + return false; + } + if (!("data" in event.data)) { + return false; + } + if (typeof event.data["data"] !== "object") { + return false; + } + + // before initialization completed give exception + const message = event.data.data; + if (!this._importExplorerConfigComplete && message && message.type) { + const messageType = message.type; + switch (messageType) { + case MessageTypes.SendNotification: + case MessageTypes.ClearNotification: + case MessageTypes.LoadingStatus: + case MessageTypes.InitTestExplorer: + return true; + } + } + if (!("inputs" in event.data["data"]) && !this._importExplorerConfigComplete) { + return false; + } + return true; + } + + public handleMessage(event: MessageEvent) { + if (isInvalidParentFrameOrigin(event)) { + return; + } + + if (!this._shouldProcessMessage(event)) { + return; + } + + const message: any = event.data.data; + const inputs: ViewModels.DataExplorerInputsFrame = message.inputs; + + const isRunningInPortal = configContext.platform === Platform.Portal; + const isRunningInDevMode = process.env.NODE_ENV === "development"; + if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) { + inputs.extensionEndpoint = configContext.PROXY_PATH; + } + + this.initDataExplorerWithFrameInputs(inputs); + + const openAction: ActionContracts.DataExplorerAction = message.openAction; + if (!!openAction) { + if (this.isRefreshingExplorer()) { + const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { + handleOpenAction(openAction, this.nonSystemDatabases(), this); + subscription.dispose(); + }); + } else { + handleOpenAction(openAction, this.nonSystemDatabases(), this); + } + } + if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { + handleCachedDataMessage(message); + return; + } + if (message.type) { + switch (message.type) { + case MessageTypes.UpdateLocationHash: + if (!message.locationHash) { + break; + } + hasher.replaceHash(message.locationHash); + RouteHandler.getInstance().parseHash(message.locationHash); + break; + case MessageTypes.SendNotification: + if (!message.message) { + break; + } + NotificationConsoleUtils.logConsoleMessage( + message.consoleDataType || ConsoleDataType.Info, + message.message, + message.id + ); + break; + case MessageTypes.ClearNotification: + if (!message.id) { + break; + } + NotificationConsoleUtils.clearInProgressMessageWithId(message.id); + break; + case MessageTypes.LoadingStatus: + if (!message.text) { + break; + } + this._setLoadingStatusText(message.text, message.title); + break; + } + return; + } + + this.splashScreenAdapter.forceRender(); + } + + public findSelectedDatabase(): ViewModels.Database { + if (!this.selectedNode()) { + return null; + } + if (this.selectedNode().nodeKind === "Database") { + return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); + } + return this.findSelectedCollection().database; + } + + public findDatabaseWithId(databaseId: string): ViewModels.Database { + return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); + } + + public isLastNonEmptyDatabase(): boolean { + if (this.isLastDatabase() && this.databases()[0].collections && this.databases()[0].collections().length > 0) { + return true; + } + return false; + } + + public isLastDatabase(): boolean { + if (this.databases().length > 1) { + return false; + } + return true; + } + + public isSelectedDatabaseShared(): boolean { + const database = this.findSelectedDatabase(); + if (!!database) { + return database.offer && !!database.offer(); + } + + return false; + } + + public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void { + const selfServeFeature = inputs.features[Constants.Features.selfServeType]; + if (selfServeFeature) { + // self serve type received from query string + const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType]; + this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid); + } else if (inputs.selfServeType) { + // self serve type received from portal + this.selfServeType(inputs.selfServeType); + } else { + this.selfServeType(SelfServeType.none); + } + } + + public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void { + if (inputs != null) { + // In development mode, save the iframe message from the portal in session storage. + // This allows webpack hot reload to funciton properly + if (process.env.NODE_ENV === "development") { + sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); + } + + const authorizationToken = inputs.authorizationToken || ""; + const masterKey = inputs.masterKey || ""; + const databaseAccount = inputs.databaseAccount || null; + if (inputs.defaultCollectionThroughput) { + this.collectionCreationDefaults = inputs.defaultCollectionThroughput; + } + this.features(inputs.features); + this.serverId(inputs.serverId); + this.databaseAccount(databaseAccount); + this.subscriptionType(inputs.subscriptionType); + this.hasWriteAccess(inputs.hasWriteAccess); + this.flight(inputs.addCollectionDefaultFlight); + this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); + this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); + this.setFeatureFlagsFromFlights(inputs.flights); + this.setSelfServeType(inputs); + this._importExplorerConfigComplete = true; + + updateConfigContext({ + BACKEND_ENDPOINT: inputs.extensionEndpoint || "", + ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), + }); + + updateUserContext({ + authorizationToken, + masterKey, + databaseAccount, + resourceGroup: inputs.resourceGroup, + subscriptionId: inputs.subscriptionId, + subscriptionType: inputs.subscriptionType, + quotaId: inputs.quotaId, + }); + TelemetryProcessor.traceSuccess( + Action.LoadDatabaseAccount, + { + resourceId: this.databaseAccount && this.databaseAccount().id, + dataExplorerArea: Constants.Areas.ResourceTree, + databaseAccount: this.databaseAccount && this.databaseAccount(), + }, + inputs.loadDatabaseAccountTimestamp + ); + + this.isAccountReady(true); + } + } + + public setFeatureFlagsFromFlights(flights: readonly string[]): void { + if (!flights) { + return; + } + if (flights.indexOf(Constants.Flights.AutoscaleTest) !== -1) { + this.isAutoscaleDefaultEnabled(true); + } + if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { + this.isMongoIndexingEnabled(true); + } + } + + public findSelectedCollection(): ViewModels.Collection { + return (this.selectedNode().nodeKind === "Collection" + ? this.selectedNode() + : this.selectedNode().collection) as ViewModels.Collection; + } + + // TODO: Refactor below methods, minimize dependencies and add unit tests where necessary + public findSelectedStoredProcedure(): StoredProcedure { + const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); + return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => { + const openedSprocTab = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + (tab) => tab.node && tab.node.rid === storedProcedure.rid + ); + return ( + storedProcedure.rid === this.selectedNode().rid || + (!!openedSprocTab && openedSprocTab.length > 0 && openedSprocTab[0].isActive()) + ); + }); + } + + public findSelectedUDF(): UserDefinedFunction { + const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); + return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => { + const openedUdfTab = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.UserDefinedFunctions, + (tab) => tab.node && tab.node.rid === userDefinedFunction.rid + ); + return ( + userDefinedFunction.rid === this.selectedNode().rid || + (!!openedUdfTab && openedUdfTab.length > 0 && openedUdfTab[0].isActive()) + ); + }); + } + + public findSelectedTrigger(): Trigger { + const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); + return _.find(selectedCollection.triggers(), (trigger: Trigger) => { + const openedTriggerTab = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.Triggers, + (tab) => tab.node && tab.node.rid === trigger.rid + ); + return ( + trigger.rid === this.selectedNode().rid || + (!!openedTriggerTab && openedTriggerTab.length > 0 && openedTriggerTab[0].isActive()) + ); + }); + } + + public closeAllPanes(): void { + this._panes.forEach((pane: ContextualPaneBase) => pane.close()); + } + + public isRunningOnNationalCloud(): boolean { + return ( + this.serverId() === Constants.ServerIds.blackforest || + this.serverId() === Constants.ServerIds.fairfax || + this.serverId() === Constants.ServerIds.mooncake + ); + } + + public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void { + this.commandBarComponentAdapter.onUpdateTabsButtons(buttons); + } + + public signInAad = () => { + TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" }); + sendMessage({ + type: MessageTypes.AadSignIn, + }); + }; + + public onSwitchToConnectionString = () => { + $("#connectWithAad").hide(); + $("#connectWithConnectionString").show(); + }; + + public clickHostedAccountSwitch = () => { + sendMessage({ + type: MessageTypes.UpdateAccountSwitch, + click: true, + }); + }; + + public clickHostedDirectorySwitch = () => { + sendMessage({ + type: MessageTypes.UpdateDirectoryControl, + click: true, + }); + }; + + public refreshDatabaseAccount = () => { + sendMessage({ + type: MessageTypes.RefreshDatabaseAccount, + }); + }; + + private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise { + // we reload collections for all databases so the resource tree reflects any collection-level changes + // i.e addition of stored procedures, etc. + const deferred: Q.Deferred = Q.defer(); + let loadCollectionPromises: Q.Promise[] = []; + + // If the user has a lot of databases, only load expanded databases. + const databasesToLoad = + this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand + ? this.databases() + : this.databases().filter((db) => db.isDatabaseExpanded()); + + const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + databasesToLoad.forEach(async (database: ViewModels.Database) => { + await database.loadCollections(); + const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); + if (isNewDatabase) { + database.expandDatabase(); + } + this.tabsManager.refreshActiveTab((tab) => tab.collection && tab.collection.getDatabase().id() === database.id()); + }); + + Q.all(loadCollectionPromises).done( + () => { + deferred.resolve(); + TelemetryProcessor.traceSuccess( + Action.LoadCollections, + { dataExplorerArea: Constants.Areas.ResourceTree }, + startKey + ); + }, + (error: any) => { + deferred.reject(error); + TelemetryProcessor.traceFailure( + Action.LoadCollections, + { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + ); + return deferred.promise; + } + + // TODO: Abstract this elsewhere + private _openShareDialog: () => void = (): void => { + if (!$("#shareDataAccessFlyout").dialog("instance")) { + const accountMetadataInfo = { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ShareDialog, + }; + const openFullscreenButton = { + text: "Open", + class: "openFullScreenBtn openFullScreenCancelBtn", + click: () => { + TelemetryProcessor.trace( + Action.SelectItem, + ActionModifiers.Mark, + _.extend({}, { description: "Open full screen" }, accountMetadataInfo) + ); + + const hiddenAnchorElement: HTMLAnchorElement = document.createElement("a"); + hiddenAnchorElement.href = this.shareAccessUrl(); + hiddenAnchorElement.target = "_blank"; + $("#shareDataAccessFlyout").dialog("close"); + hiddenAnchorElement.click(); + }, + }; + const cancelButton = { + text: "Cancel", + class: "shareCancelButton openFullScreenCancelBtn", + click: () => { + TelemetryProcessor.trace( + Action.SelectItem, + ActionModifiers.Mark, + _.extend({}, { description: "Cancel open full screen" }, accountMetadataInfo) + ); + $("#shareDataAccessFlyout").dialog("close"); + }, + }; + $("#shareDataAccessFlyout").dialog({ + autoOpen: false, + buttons: [openFullscreenButton, cancelButton], + closeOnEscape: true, + draggable: false, + dialogClass: "no-close", + position: { my: "right top", at: "right bottom", of: $(".OpenFullScreen") }, + resizable: false, + title: "Open Full Screen", + width: 400, + close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowShareDialogContents(false), + }); + $("#shareDataAccessFlyout").dialog("option", "classes", { + "ui-widget-content": "shareUrlDialog", + "ui-widget-header": "shareUrlTitle", + "ui-dialog-titlebar-close": "shareClose", + "ui-button": "shareCloseIcon", + "ui-button-icon": "cancelIcon", + "ui-icon": "", + }); + $("#shareDataAccessFlyout").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => + $(".openFullScreenBtn").focus() + ); + } + $("#shareDataAccessFlyout").dialog("close"); + this.shouldShowShareDialogContents(true); + $("#shareDataAccessFlyout").dialog("open"); + }; + + private _getShareAccessUrlForToken(token: string): string { + if (!token) { + return undefined; + } + + const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`; + const currentActiveTab = this.tabsManager.activeTab(); + + return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`; + } + + private _initSettings() { + if (!ExplorerSettings.hasSettingsDefined()) { + ExplorerSettings.createDefaultSettings(); + } + } + + public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { + const database: ViewModels.Database = this.databases().find( + (database: ViewModels.Database) => database.id() === databaseId + ); + return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); + } + + public isLastCollection(): boolean { + let collectionCount = 0; + if (this.databases().length == 0) { + return false; + } + for (let i = 0; i < this.databases().length; i++) { + const database = this.databases()[i]; + collectionCount += database.collections().length; + if (collectionCount > 1) { + return false; + } + } + return true; + } + + private getDeltaDatabases( + updatedDatabaseList: DataModels.Database[] + ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { + const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { + const databaseExists = _.some( + this.databases(), + (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id + ); + return !databaseExists; + }); + const databasesToAdd: ViewModels.Database[] = newDatabases.map( + (newDatabase: DataModels.Database) => new Database(this, newDatabase) + ); + + let databasesToDelete: ViewModels.Database[] = []; + ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + const databasePresentInUpdatedList = _.some( + updatedDatabaseList, + (db: DataModels.Database) => db.id === database.id() + ); + if (!databasePresentInUpdatedList) { + databasesToDelete.push(database); + } + }); + + return { toAdd: databasesToAdd, toDelete: databasesToDelete }; + } + + private addDatabasesToList(databases: ViewModels.Database[]): void { + this.databases( + this.databases() + .concat(databases) + .sort((database1, database2) => database1.id().localeCompare(database2.id())) + ); + } + + private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { + const databasesToKeep: ViewModels.Database[] = []; + + ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); + if (!shouldRemoveDatabase) { + databasesToKeep.push(database); + } + }); + + this.databases(databasesToKeep); + } + + public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to upload notebook, but notebook is not enabled"; + handleError(error, "Explorer/uploadFile"); + throw new Error(error); + } + + const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); + promise + .then(() => this.resourceTree.triggerRender()) + .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); + return promise; + } + + public async importAndOpen(path: string): Promise { + const name = NotebookUtil.getName(path); + const item = NotebookUtil.createNotebookContentItem(name, path, "file"); + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { + const existingItem = _.find(parent.children, (node) => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const content = await this.readFile(item); + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + return Promise.resolve(false); + } + + public async importAndOpenContent(name: string, content: string): Promise { + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { + if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { + this.notebookToImport = undefined; // we don't want to try opening this notebook again + } + + const existingItem = _.find(parent.children, (node) => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + this.notebookToImport = { name, content }; // we'll try opening this notebook later on + return Promise.resolve(false); + } + + public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise { + if (this.notebookManager) { + await this.notebookManager.openPublishNotebookPane( + name, + content, + parentDomElement, + this.isLinkInjectionEnabled() + ); + this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; + this.isPublishNotebookPaneEnabled(true); + } + } + + public copyNotebook(name: string, content: string): void { + if (this.notebookManager) { + this.notebookManager.openCopyNotebookPane(name, content); + this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter; + this.isCopyNotebookPaneEnabled(true); + } + } + + public showOkModalDialog(title: string, msg: string): void { + this._dialogProps({ + isModal: true, + visible: true, + title, + subText: msg, + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: this._closeModalDialog, + onSecondaryButtonClick: undefined, + }); + } + + public showOkCancelModalDialog( + title: string, + msg: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + isPrimaryButtonDisabled?: boolean + ): void { + this._dialogProps({ + isModal: true, + visible: true, + title, + subText: msg, + primaryButtonText: okLabel, + secondaryButtonText: cancelLabel, + onPrimaryButtonClick: () => { + this._closeModalDialog(); + onOk && onOk(); + }, + onSecondaryButtonClick: () => { + this._closeModalDialog(); + onCancel && onCancel(); + }, + choiceGroupProps, + textFieldProps, + primaryButtonDisabled: isPrimaryButtonDisabled, + }); + } + + /** + * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. + * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. + * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder + * will not fetch its content if the children array exists (and has only one child which was manually created). + * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. + * + * @param name + * @param path + */ + public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { + return NotebookUtil.createNotebookContentItem(name, path, "file"); + } + + public async openNotebook(notebookContentItem: NotebookContentItem): Promise { + if (!notebookContentItem || !notebookContentItem.path) { + throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); + } + + const notebookTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab) => + (tab as NotebookV2Tab).notebookPath && + FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path) + ) as NotebookV2Tab[]; + let notebookTab = notebookTabs && notebookTabs[0]; + + if (notebookTab) { + this.tabsManager.activateTab(notebookTab); + } else { + const options: NotebookTabOptions = { + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.NotebookV2, + node: null, + title: notebookContentItem.name, + tabPath: notebookContentItem.path, + collection: null, + masterKey: userContext.masterKey || "", + hashLocation: "notebooks", + isActive: ko.observable(false), + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + onUpdateTabsButtons: this.onUpdateTabsButtons, + container: this, + notebookContentItem, + }; + + try { + const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); + notebookTab = new NotebookTabV2.default(options); + this.tabsManager.activateNewTab(notebookTab); + } catch (reason) { + console.error("Import NotebookV2Tab failed!", reason); + return false; + } + } + + return true; + } + + public renameNotebook(notebookFile: NotebookContentItem): Q.Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to rename notebook, but notebook is not enabled"; + handleError(error, "Explorer/renameNotebook"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); + } + ); + if (openedNotebookTabs.length > 0) { + this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); + return Q.reject(); + } + + const originalPath = notebookFile.path; + const result = this.stringInputPane + .openWithOptions({ + errorMessage: "Could not rename notebook", + inProgressMessage: "Renaming notebook to", + successMessage: "Renamed notebook to", + inputLabel: "Enter new notebook name", + paneTitle: "Rename Notebook", + submitButtonLabel: "Rename", + defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"), + onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input), + }) + .then((newNotebookFile) => { + const notebookTabs = this.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); + (tab as NotebookV2Tab).notebookPath(newNotebookFile.path); + }); + + return newNotebookFile; + }); + result.then(() => this.resourceTree.triggerRender()); + return result; + } + + public onCreateDirectory(parent: NotebookContentItem): Q.Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create notebook directory, but notebook is not enabled"; + handleError(error, "Explorer/onCreateDirectory"); + throw new Error(error); + } + + const result = this.stringInputPane.openWithOptions({ + errorMessage: "Could not create directory ", + inProgressMessage: "Creating directory ", + successMessage: "Created directory ", + inputLabel: "Enter new directory name", + paneTitle: "Create new directory", + submitButtonLabel: "Create", + defaultInput: "", + onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input), + }); + result.then(() => this.resourceTree.triggerRender()); + return result; + } + + public readFile(notebookFile: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to read file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); + } + + public downloadFile(notebookFile: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to download file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( + (content: string) => { + const blob = stringToBlob(content, "text/plain"); + if (navigator.msSaveBlob) { + // for IE and Edge + navigator.msSaveBlob(blob, notebookFile.name); + } else { + const downloadLink: HTMLAnchorElement = document.createElement("a"); + const url = URL.createObjectURL(blob); + downloadLink.href = url; + downloadLink.target = "_self"; + downloadLink.download = notebookFile.name; + + // for some reason, FF displays the download prompt only when + // the link is added to the dom so we add and remove it + document.body.appendChild(downloadLink); + downloadLink.click(); + downloadLink.remove(); + } + + clearMessage(); + }, + (error: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Could not download notebook ${getErrorMessage(error)}` + ); + + clearMessage(); + } + ); + } + + private async _refreshNotebooksEnabledStateForAccount(): Promise { + const authType = window.authType as AuthType; + if ( + authType === AuthType.EncryptedToken || + authType === AuthType.ResourceToken || + authType === AuthType.MasterKey + ) { + this.isNotebooksEnabledForAccount(false); + return; + } + + const databaseAccount = this.databaseAccount(); + const databaseAccountLocation = databaseAccount && databaseAccount.location.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"); + this.isNotebooksEnabledForAccount(true); + return; + } + const isAccountInAllowedLocation = !disallowedLocations.some( + (disallowedLocation) => disallowedLocation === databaseAccountLocation + ); + this.isNotebooksEnabledForAccount(isAccountInAllowedLocation); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); + this.isNotebooksEnabledForAccount(false); + } + } + + public _refreshSparkEnabledStateForAccount = async (): Promise => { + const subscriptionId = userContext.subscriptionId; + const armEndpoint = configContext.ARM_ENDPOINT; + const authType = window.authType as AuthType; + if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { + // explorer is not aware of the database account yet + this.isSparkEnabledForAccount(false); + return; + } + + const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`; + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); + try { + const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( + featureUri, + Constants.ArmApiVersions.armFeatures + ); + const isEnabled = + (sparkNotebooksFeature && + sparkNotebooksFeature.properties && + sparkNotebooksFeature.properties.state === "Registered") || + false; + this.isSparkEnabledForAccount(isEnabled); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); + this.isSparkEnabledForAccount(false); + } + }; + + public _isAfecFeatureRegistered = async (featureName: string): Promise => { + const subscriptionId = userContext.subscriptionId; + const armEndpoint = configContext.ARM_ENDPOINT; + const authType = window.authType as AuthType; + if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { + // explorer is not aware of the database account yet + return false; + } + + const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`; + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); + try { + const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( + featureUri, + Constants.ArmApiVersions.armFeatures + ); + const isEnabled = + (featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false; + return isEnabled; + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); + return false; + } + }; + private refreshNotebookList = async (): Promise => { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + return; + } + + await this.resourceTree.initialize(); + this.notebookManager?.refreshPinnedRepos(); + if (this.notebookToImport) { + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + }; + + public deleteNotebookFile(item: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to delete notebook file, but notebook is not enabled"; + handleError(error, "Explorer/deleteNotebookFile"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); + } + ); + if (openedNotebookTabs.length > 0) { + this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); + return Promise.reject(); + } + + if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { + this._dialogProps({ + isModal: true, + visible: true, + title: "Unable to delete file", + subText: "Directory is not empty.", + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: this._closeModalDialog, + onSecondaryButtonClick: undefined, + }); + return Promise.reject(); + } + + return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( + () => { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`); + }, + (reason: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to delete "${item.path}": ${JSON.stringify(reason)}` + ); + } + ); + } + + /** + * This creates a new notebook file, then opens the notebook + */ + public onNewNotebookClicked(parent?: NotebookContentItem): void { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create new notebook, but notebook is not enabled"; + handleError(error, "Explorer/onNewNotebookClicked"); + throw new Error(error); + } + + parent = parent || this.resourceTree.myNotebooksContentRoot; + + const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Creating new notebook in ${parent.path}` + ); + + const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + }); + + this.notebookManager?.notebookContentClient + .createNewNotebookFile(parent) + .then((newFile: NotebookContentItem) => { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`); + TelemetryProcessor.traceSuccess( + Action.CreateNewNotebook, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + }, + startKey + ); + return this.openNotebook(newFile); + }) + .then(() => this.resourceTree.triggerRender()) + .catch((error: any) => { + const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateNewNotebook, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + }) + .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId)); + } + + public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { + parent = parent || this.resourceTree.myNotebooksContentRoot; + + this.uploadFilePane.openWithOptions({ + paneTitle: "Upload file to notebook server", + selectFileInputLabel: "Select file to upload", + errorMessage: "Could not upload file", + inProgressMessage: "Uploading file to notebook server", + successMessage: "Successfully uploaded file to notebook server", + onSubmit: async (file: File): Promise => { + const readFileAsText = (inputFile: File): Promise => { + const reader = new FileReader(); + return new Promise((resolve, reject) => { + reader.onerror = () => { + reader.abort(); + reject(`Problem parsing file: ${inputFile}`); + }; + reader.onload = () => { + resolve(reader.result as string); + }; + reader.readAsText(inputFile); + }); + }; + + const fileContent = await readFileAsText(file); + return this.uploadFile(file.name, fileContent, parent); + }, + extensions: undefined, + submitButtonLabel: "Upload", + }); + } + + public refreshContentItem(item: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to refresh notebook list, but notebook is not enabled"; + handleError(error, "Explorer/refreshContentItem"); + return Promise.reject(new Error(error)); + } + + return this.notebookManager?.notebookContentClient.updateItemChildren(item); + } + + public getNotebookBasePath(): string { + return this.notebookBasePath(); + } + + public openNotebookTerminal(kind: ViewModels.TerminalKind) { + let title: string; + let hashLocation: string; + + switch (kind) { + case ViewModels.TerminalKind.Default: + title = "Terminal"; + hashLocation = "terminal"; + break; + + case ViewModels.TerminalKind.Mongo: + title = "Mongo Shell"; + hashLocation = "mongo-shell"; + break; + + case ViewModels.TerminalKind.Cassandra: + title = "Cassandra Shell"; + hashLocation = "cassandra-shell"; + break; + + default: + throw new Error("Terminal kind: ${kind} not supported"); + } + + const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.Terminal, + (tab) => tab.hashLocation() == hashLocation + ) as TerminalTab[]; + let terminalTab: TerminalTab = terminalTabs && terminalTabs[0]; + + if (terminalTab) { + this.tabsManager.activateTab(terminalTab); + } else { + const newTab = new TerminalTab({ + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.Terminal, + node: null, + title: title, + tabPath: title, + collection: null, + hashLocation: hashLocation, + isActive: ko.observable(false), + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + onUpdateTabsButtons: this.onUpdateTabsButtons, + container: this, + kind: kind, + }); + + this.tabsManager.activateNewTab(newTab); + } + } + + public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) { + let title: string = "Gallery"; + let hashLocation: string = "gallery"; + + const galleryTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.Gallery, + (tab) => tab.hashLocation() == hashLocation + ); + let galleryTab = galleryTabs && galleryTabs[0]; + + if (galleryTab) { + this.tabsManager.activateTab(galleryTab); + } else { + if (!this.galleryTab) { + this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab"); + } + + const newTab = new this.galleryTab.default({ + // GalleryTabOptions + account: userContext.databaseAccount, + container: this, + junoClient: this.notebookManager?.junoClient, + notebookUrl, + galleryItem, + isFavorite, + // TabOptions + tabKind: ViewModels.CollectionTabKind.Gallery, + title: title, + tabPath: title, + documentClientUtility: null, + isActive: ko.observable(false), + hashLocation: hashLocation, + onUpdateTabsButtons: this.onUpdateTabsButtons, + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + }); + + this.tabsManager.activateNewTab(newTab); + } + } + + public async openNotebookViewer(notebookUrl: string) { + const title = path.basename(notebookUrl); + const hashLocation = notebookUrl; + + if (!this.notebookViewerTab) { + this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab"); + } + + const notebookViewerTabModule = this.notebookViewerTab; + + let isNotebookViewerOpen = (tab: TabsBase) => { + const notebookViewerTab = tab as typeof notebookViewerTabModule.default; + return notebookViewerTab.notebookUrl === notebookUrl; + }; + + const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab) => { + return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab); + }); + let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0]; + + if (notebookViewerTab) { + this.tabsManager.activateNewTab(notebookViewerTab); + } else { + notebookViewerTab = new this.notebookViewerTab.default({ + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.NotebookViewer, + node: null, + title: title, + tabPath: title, + documentClientUtility: null, + collection: null, + hashLocation: hashLocation, + isActive: ko.observable(false), + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + onUpdateTabsButtons: this.onUpdateTabsButtons, + container: this, + notebookUrl, + }); + + this.tabsManager.activateNewTab(notebookViewerTab); + } + } + + public onNewCollectionClicked(): void { + if (this.isPreferredApiCassandra()) { + this.cassandraAddCollectionPane.open(); + } else { + this.addCollectionPane.open(this.selectedDatabaseId()); + } + document.getElementById("linkAddCollection").focus(); + } + + private refreshCommandBarButtons(): void { + const activeTab = this.tabsManager.activeTab(); + if (activeTab) { + activeTab.onActivate(); // TODO only update tabs buttons? + } else { + this.onUpdateTabsButtons([]); + } + } + + private getTokenRefreshInterval(token: string): number { + let tokenRefreshInterval = Constants.ClientDefaults.arcadiaTokenRefreshInterval; + if (!token) { + return tokenRefreshInterval; + } + + try { + const tokenPayload = decryptJWTToken(this.arcadiaToken()); + if (tokenPayload && tokenPayload.hasOwnProperty("exp")) { + const expirationTime = tokenPayload.exp as number; // seconds since unix epoch + const now = new Date().getTime() / 1000; + const tokenExpirationIntervalInMs = (expirationTime - now) * 1000; + if (tokenExpirationIntervalInMs < tokenRefreshInterval) { + tokenRefreshInterval = + tokenExpirationIntervalInMs - Constants.ClientDefaults.arcadiaTokenRefreshIntervalPaddingMs; + } + } + return tokenRefreshInterval; + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval"); + return tokenRefreshInterval; + } + } + + private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") { + if (!text) { + return; + } + + const loadingText = document.getElementById("explorerLoadingStatusText"); + if (!loadingText) { + Logger.logError( + "getElementById('explorerLoadingStatusText') failed to find element", + "Explorer/_setLoadingStatusText" + ); + return; + } + loadingText.innerHTML = text; + + const loadingTitle = document.getElementById("explorerLoadingStatusTitle"); + if (!loadingTitle) { + Logger.logError( + "getElementById('explorerLoadingStatusTitle') failed to find element", + "Explorer/_setLoadingStatusText" + ); + } else { + loadingTitle.innerHTML = title; + } + } + + private _openSetupNotebooksPaneForQuickstart(): void { + const title = "Enable Notebooks (Preview)"; + const description = + "You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account."; + + this.setupNotebooksPane.openWithTitleAndDescription(title, description); + } + + public async handleOpenFileAction(path: string): Promise { + if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(this.databaseAccount()))) { + this.closeAllPanes(); + this._openSetupNotebooksPaneForQuickstart(); + } + + // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb + // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly + // calling GitHub. For now convert this url to a raw url and download content. + const gitHubInfo = fromContentUri(path); + if (gitHubInfo) { + const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); + const response = await fetch(rawUrl); + if (response.status === Constants.HttpStatusCodes.OK) { + this.notebookToImport = { + name: NotebookUtil.getName(path), + content: await response.text(), + }; + + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + } + } + + public async loadSelectedDatabaseOffer(): Promise { + const database = this.findSelectedDatabase(); + await database?.loadOffer(); + } + + public async loadDatabaseOffers(): Promise { + await Promise.all( + this.databases()?.map(async (database: ViewModels.Database) => { + await database.loadOffer(); + }) + ); + } + + public isFirstResourceCreated(): boolean { + const databases: ViewModels.Database[] = this.databases(); + + if (!databases || databases.length === 0) { + return false; + } + + return databases.some((database) => { + // user has created at least one collection + if (database.collections()?.length > 0) { + return true; + } + // user has created a database with shared throughput + if (database.offer()) { + return true; + } + // use has created an empty database without shared throughput + return false; + }); + } +} diff --git a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts index 4b936d67b..1f434d577 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts @@ -1,158 +1,156 @@ -import * as sinon from "sinon"; -import { D3ForceGraph, LoadMoreDataAction, D3GraphNodeData } from "./D3ForceGraph"; -import { D3Node, D3Link, GraphData } from "../GraphExplorerComponent/GraphData"; -import GraphTab from "../../Tabs/GraphTab"; - -describe("D3ForceGraph", () => { - const v1Id = "v1"; - const l1: D3Link = { - id: "id1", - inV: v1Id, - outV: "v2", - label: "l1", - source: null, - target: null - }; - - it("should count neighbors", () => { - const l2: D3Link = { - id: "id1", - inV: "v2", - outV: v1Id, - label: "l2", - source: null, - target: null - }; - - const l3: D3Link = { - id: "id1", - inV: v1Id, - outV: "v3", - label: "l3", - source: null, - target: null - }; - - const links = [l1, l2, l3]; - const count = D3ForceGraph.countEdges(links); - - expect(count.get(v1Id)).toBe(3); - expect(count.get("v2")).toBe(2); - expect(count.get("v3")).toBe(1); - }); - - describe("Behavior", () => { - let forceGraph: D3ForceGraph; - let rootNode: SVGSVGElement; - - const newGraph: GraphData = new GraphData(); - newGraph.addVertex({ - id: v1Id, - label: "vlabel1", - _isRoot: true - }); - newGraph.addVertex({ - id: "v2", - label: "vlabel2" - }); - newGraph.addEdge(l1); - - beforeAll(() => { - rootNode = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - rootNode.setAttribute("class", "maingraph"); - }); - - afterAll(() => { - rootNode.remove(); - }); - - beforeEach(() => { - forceGraph = new D3ForceGraph({ - graphConfig: GraphTab.createGraphConfig(), - onHighlightedNode: sinon.spy(), - onLoadMoreData: (action: LoadMoreDataAction): void => {}, - - // parent to graph - onInitialized: sinon.spy(), - - // For unit testing purposes - onGraphUpdated: null - }); - - forceGraph.init(rootNode); - }); - - afterEach(() => { - forceGraph.destroy(); - }); - - it("should render graph d3 nodes and edges", done => { - forceGraph.params.onGraphUpdated = () => { - expect($(rootNode).find(".nodes").length).toBe(1); - expect($(rootNode).find(".links").length).toBe(1); - done(); - }; - - forceGraph.updateGraph(newGraph); - }); - - it("should render vertices (as circle)", done => { - forceGraph.params.onGraphUpdated = () => { - expect($(rootNode).find(".node circle").length).toBe(2); - done(); - }; - - forceGraph.updateGraph(newGraph); - }); - - it("should render vertex label", done => { - forceGraph.params.onGraphUpdated = () => { - expect($(rootNode).find(`text:contains(${v1Id})`).length).toBe(1); - done(); - }; - - forceGraph.updateGraph(newGraph); - }); - - it("should render root vertex", done => { - forceGraph.params.onGraphUpdated = () => { - expect($(rootNode).find(".node.root").length).toBe(1); - done(); - }; - - forceGraph.updateGraph(newGraph); - }); - - it("should render edge", done => { - forceGraph.params.onGraphUpdated = () => { - expect($(rootNode).find("path.link").length).toBe(1); - done(); - }; - - forceGraph.updateGraph(newGraph); - }); - - it("should call onInitialized callback", () => { - expect((forceGraph.params.onInitialized as sinon.SinonSpy).calledOnce).toBe(true); - }); - - it("should call onHighlightedNode callback when mouse hovering over node", () => { - forceGraph.params.onGraphUpdated = () => { - const mouseoverEvent = document.createEvent("Events"); - mouseoverEvent.initEvent("mouseover", true, false); - $(rootNode) - .find(".node")[0] - .dispatchEvent(mouseoverEvent); // [0] is v1 vertex - - // onHighlightedNode is always called once to clear the selection - expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true); - - const onHighlightedNode = (forceGraph.params.onHighlightedNode as sinon.SinonSpy).args[1][0] as D3GraphNodeData; - expect(onHighlightedNode).not.toBe(null); - expect(onHighlightedNode.id).toEqual(v1Id); - }; - - forceGraph.updateGraph(newGraph); - }); - }); -}); +import * as sinon from "sinon"; +import { D3ForceGraph, LoadMoreDataAction, D3GraphNodeData } from "./D3ForceGraph"; +import { D3Node, D3Link, GraphData } from "../GraphExplorerComponent/GraphData"; +import GraphTab from "../../Tabs/GraphTab"; + +describe("D3ForceGraph", () => { + const v1Id = "v1"; + const l1: D3Link = { + id: "id1", + inV: v1Id, + outV: "v2", + label: "l1", + source: null, + target: null, + }; + + it("should count neighbors", () => { + const l2: D3Link = { + id: "id1", + inV: "v2", + outV: v1Id, + label: "l2", + source: null, + target: null, + }; + + const l3: D3Link = { + id: "id1", + inV: v1Id, + outV: "v3", + label: "l3", + source: null, + target: null, + }; + + const links = [l1, l2, l3]; + const count = D3ForceGraph.countEdges(links); + + expect(count.get(v1Id)).toBe(3); + expect(count.get("v2")).toBe(2); + expect(count.get("v3")).toBe(1); + }); + + describe("Behavior", () => { + let forceGraph: D3ForceGraph; + let rootNode: SVGSVGElement; + + const newGraph: GraphData = new GraphData(); + newGraph.addVertex({ + id: v1Id, + label: "vlabel1", + _isRoot: true, + }); + newGraph.addVertex({ + id: "v2", + label: "vlabel2", + }); + newGraph.addEdge(l1); + + beforeAll(() => { + rootNode = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + rootNode.setAttribute("class", "maingraph"); + }); + + afterAll(() => { + rootNode.remove(); + }); + + beforeEach(() => { + forceGraph = new D3ForceGraph({ + graphConfig: GraphTab.createGraphConfig(), + onHighlightedNode: sinon.spy(), + onLoadMoreData: (action: LoadMoreDataAction): void => {}, + + // parent to graph + onInitialized: sinon.spy(), + + // For unit testing purposes + onGraphUpdated: null, + }); + + forceGraph.init(rootNode); + }); + + afterEach(() => { + forceGraph.destroy(); + }); + + it("should render graph d3 nodes and edges", (done) => { + forceGraph.params.onGraphUpdated = () => { + expect($(rootNode).find(".nodes").length).toBe(1); + expect($(rootNode).find(".links").length).toBe(1); + done(); + }; + + forceGraph.updateGraph(newGraph); + }); + + it("should render vertices (as circle)", (done) => { + forceGraph.params.onGraphUpdated = () => { + expect($(rootNode).find(".node circle").length).toBe(2); + done(); + }; + + forceGraph.updateGraph(newGraph); + }); + + it("should render vertex label", (done) => { + forceGraph.params.onGraphUpdated = () => { + expect($(rootNode).find(`text:contains(${v1Id})`).length).toBe(1); + done(); + }; + + forceGraph.updateGraph(newGraph); + }); + + it("should render root vertex", (done) => { + forceGraph.params.onGraphUpdated = () => { + expect($(rootNode).find(".node.root").length).toBe(1); + done(); + }; + + forceGraph.updateGraph(newGraph); + }); + + it("should render edge", (done) => { + forceGraph.params.onGraphUpdated = () => { + expect($(rootNode).find("path.link").length).toBe(1); + done(); + }; + + forceGraph.updateGraph(newGraph); + }); + + it("should call onInitialized callback", () => { + expect((forceGraph.params.onInitialized as sinon.SinonSpy).calledOnce).toBe(true); + }); + + it("should call onHighlightedNode callback when mouse hovering over node", () => { + forceGraph.params.onGraphUpdated = () => { + const mouseoverEvent = document.createEvent("Events"); + mouseoverEvent.initEvent("mouseover", true, false); + $(rootNode).find(".node")[0].dispatchEvent(mouseoverEvent); // [0] is v1 vertex + + // onHighlightedNode is always called once to clear the selection + expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true); + + const onHighlightedNode = (forceGraph.params.onHighlightedNode as sinon.SinonSpy).args[1][0] as D3GraphNodeData; + expect(onHighlightedNode).not.toBe(null); + expect(onHighlightedNode.id).toEqual(v1Id); + }; + + forceGraph.updateGraph(newGraph); + }); + }); +}); diff --git a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts index 6504e7794..efe403eec 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts @@ -1,1353 +1,1324 @@ -import * as ko from "knockout"; -import Q from "q"; -import { schemeCategory10 } from "d3-scale-chromatic"; -import { selectAll, select } from "d3-selection"; -import { zoom, zoomIdentity } from "d3-zoom"; -import { scaleOrdinal } from "d3-scale"; -import { forceSimulation, forceLink, forceCollide, forceManyBody } from "d3-force"; -import { interpolateNumber, interpolate } from "d3-interpolate"; -import { map as d3Map } from "d3-collection"; -import { drag, D3DragEvent } from "d3-drag"; - -import _ from "underscore"; -import { NeighborType } from "../../../Contracts/ViewModels"; -import { GraphData, D3Node, D3Link } from "./GraphData"; -import { HashMap } from "../../../Common/HashMap"; -import { BaseType } from "d3"; -import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; -import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; -import { GraphConfig } from "../../Tabs/GraphTab"; -import { GraphExplorer } from "./GraphExplorer"; -import * as Constants from "../../../Common/Constants"; - -export interface D3GraphIconMap { - [key: string]: { data: string; format: string }; -} - -export interface D3GraphNodeData { - g: any; // TODO needs this? - id: string; -} - -export enum PAGE_ACTION { - FIRST_PAGE, - PREVIOUS_PAGE, - NEXT_PAGE -} - -export interface LoadMoreDataAction { - nodeId: string; - pageAction: PAGE_ACTION; -} - -interface Point2D { - x: number; - y: number; -} - -interface ZoomTransform extends Point2D { - k: number; -} - -export interface D3ForceGraphParameters { - // Graph to parent - graphConfig: GraphConfig; - onHighlightedNode: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph - onLoadMoreData: (action: LoadMoreDataAction) => void; - - // parent to graph - onInitialized: (instance: GraphRenderer) => void; - - // For unit testing purposes - onGraphUpdated: (timestamp: number) => void; -} - -export interface GraphRenderer { - selectNode(id: string): void; - resetZoom(): void; - updateGraph(graphData: GraphData): void; - enableHighlight(enable: boolean): void; -} - -/** This is the custom Knockout handler for the d3 graph */ -export class D3ForceGraph implements GraphRenderer { - // Some constants - private static readonly GRAPH_WIDTH_PX = 900; - private static readonly GRAPH_HEIGHT_PX = 700; - private static readonly TEXT_DX = 12; - private static readonly FORCE_COLLIDE_RADIUS = 40; - private static readonly FORCE_COLLIDE_STRENGTH = 0.2; - private static readonly FORCE_COLLIDE_ITERATIONS = 1; - private static readonly NODE_LABEL_MAX_CHAR_LENGTH = 16; - private static readonly FORCE_LINK_DISTANCE = 100; - private static readonly FORCE_LINK_STRENGTH = 0.005; - private static readonly INITIAL_POSITION_RADIUS = 150; - private static readonly TRANSITION_STEP1_MS = 1000; - private static readonly TRANSITION_STEP2_MS = 1000; - private static readonly TRANSITION_STEP3_MS = 700; - private static readonly PAGINATION_LINE1_Y_OFFSET_PX = 4; - private static readonly PAGINATION_LINE2_Y_OFFSET_PX = 14; - - // We limit the number of different colors to 20 - private static readonly COLOR_SCHEME = scaleOrdinal(schemeCategory10); - private static readonly MAX_COLOR_NB = 20; - - // Some state variables - private static instanceCount = 1; - private instanceIndex: number; - private svg: d3.Selection; - private g: d3.Selection; - private simulation: d3.Simulation; - private width: number; - private height: number; - private selectedNode: d3.BaseType; - private isDragging: boolean; - private rootVertex: D3Node; - private nodeSelection: any; - private linkSelection: any; - private zoomTransform: ZoomTransform; - private zoom: d3.ZoomBehavior; - private zoomBackground: d3.Selection; - private viewCenter: Point2D; - - // Map a property to a graph node attribute (such as color) - private uniqueValues: (string | number)[]; // keep track of unique values - private graphDataWrapper: GraphData; - - // Communication with outside - // Graph -> outside - public params: D3ForceGraphParameters; - public errorMsgs: ko.ObservableArray; // errors happen in graph - - // outside -> Graph - private idToSelect: ko.Observable; // Programmatically select node by id outside graph - private isHighlightDisabled: boolean; - - public constructor(params: D3ForceGraphParameters) { - this.params = params; - this.idToSelect = ko.observable(null); - this.errorMsgs = ko.observableArray([]); - this.graphDataWrapper = null; - - this.width = D3ForceGraph.GRAPH_WIDTH_PX; - this.height = D3ForceGraph.GRAPH_HEIGHT_PX; - - this.rootVertex = null; - this.isHighlightDisabled = false; - this.zoomTransform = { x: 0, y: 0, k: 1 }; - this.viewCenter = { x: this.width / 2, y: this.height / 2 }; - - this.instanceIndex = D3ForceGraph.instanceCount++; - } - - public init(element: Element): void { - this.initializeGraph(element); - this.params.onInitialized(this); - } - - public destroy(): void { - this.simulation.stop(); - this.simulation = null; - this.graphDataWrapper = null; - this.linkSelection = null; - this.nodeSelection = null; - this.g.remove(); - } - - public updateGraph(newGraph: GraphData): void { - if (!newGraph || !this.simulation) { - return; - } - // Build new GraphData object from it - this.graphDataWrapper = new GraphData(); - this.graphDataWrapper.setData(newGraph); - - const key = this.params.graphConfig.nodeColorKey(); - if (key !== GraphExplorer.NONE_CHOICE) { - this.updateUniqueValues(key); - } - - // d3 expects source and target properties for each link (edge) - $.each(this.graphDataWrapper.edges, (i, e) => { - e.target = e.inV; - e.source = e.outV; - }); - - this.onGraphDataUpdate(this.graphDataWrapper); - } - - public resetZoom(): void { - this.zoomBackground.call(this.zoom.transform, zoomIdentity); - this.viewCenter = { x: this.width / 2, y: this.height / 2 }; - } - - public enableHighlight(enable: boolean): void { - this.isHighlightDisabled = enable; - } - - public selectNode(id: string): void { - this.idToSelect(id); - } - - public getArrowHeadSymbolId(): string { - return `triangle-${this.instanceIndex}`; - } - - /** - * Count edges and store in a hashmap: vertex id <--> number of links - * @param linkSelection - */ - public static countEdges(links: D3Link[]): HashMap { - const countMap = new HashMap(); - links.forEach((l: D3Link) => { - let val = countMap.get(l.inV) || 0; - val += 1; - countMap.set(l.inV, val); - - val = countMap.get(l.outV) || 0; - val += 1; - countMap.set(l.outV, val); - }); - - return countMap; - } - - /** - * Construct the graph - * @param graphData - */ - private initializeGraph(element: Element): void { - this.zoom = zoom() - .scaleExtent([1 / 2, 4]) - .on("zoom", this.zoomed.bind(this)); - - this.svg = select(element) - .attr("viewBox", `0 0 ${this.width} ${this.height}`) - .attr("preserveAspectRatio", "xMidYMid slice"); - - this.zoomBackground = this.svg.call(this.zoom); - - element.addEventListener("click", (e: Event) => { - // IE 11 doesn't support el.classList and there's no polyfill for it - // Don't auto-deselect when not clicking on a node - if (!(e.target).classList) { - return; - } - - if ( - !D3ForceGraph.closest(e.target, (el: any) => { - return !!el && el !== document && el.classList.contains("node"); - }) - ) { - this.deselectNode(); - } - }); - - this.g = this.svg.append("g"); - this.linkSelection = this.g - .append("g") - .attr("class", "links") - .selectAll(".link"); - this.nodeSelection = this.g - .append("g") - .attr("class", "nodes") - .selectAll(".node"); - - // Reset state variables - this.selectedNode = null; - this.isDragging = false; - - this.idToSelect.subscribe(newVal => { - if (!newVal) { - this.deselectNode(); - return; - } - - var self = this; - // Select this node id - selectAll(".node") - .filter(function(d: D3Node, i) { - return d.id === newVal; - }) - .each(function(d: D3Node) { - self.onNodeClicked(this, d); - }); - }); - - // Redraw if any of these configs change - this.params.graphConfig.nodeColor.subscribe(this.redrawGraph.bind(this)); - this.params.graphConfig.nodeColorKey.subscribe((key: string) => { - // Compute colormap - this.uniqueValues = []; - this.updateUniqueValues(key); - this.redrawGraph(); - }); - this.params.graphConfig.linkColor.subscribe(() => this.redrawGraph()); - this.params.graphConfig.showNeighborType.subscribe(() => this.redrawGraph()); - this.params.graphConfig.nodeCaption.subscribe(() => this.redrawGraph()); - this.params.graphConfig.nodeSize.subscribe(() => this.redrawGraph()); - this.params.graphConfig.linkWidth.subscribe(() => this.redrawGraph()); - this.params.graphConfig.nodeIconKey.subscribe(() => this.redrawGraph()); - this.instantiateSimulation(); - } // initialize - - private updateUniqueValues(key: string) { - for (var i = 0; i < this.graphDataWrapper.vertices.length; i++) { - let vertex = this.graphDataWrapper.vertices[i]; - - let props = D3ForceGraph.getNodeProperties(vertex); - if (props.indexOf(key) === -1) { - // Vertex doesn't have the property - continue; - } - let val = GraphData.getNodePropValue(vertex, key); - if (typeof val !== "string" && typeof val !== "number") { - // Not a type we can map - continue; - } - - // Map this value if new - if (this.uniqueValues.indexOf(val) === -1) { - this.uniqueValues.push(val); - } - - if (this.uniqueValues.length === D3ForceGraph.MAX_COLOR_NB) { - this.errorMsgs.push( - `Number of unique values for property ${key} exceeds maximum (${D3ForceGraph.MAX_COLOR_NB})` - ); - // ignore rest of values - break; - } - } - } - - /** - * Retrieve all node properties - * NOTE: This is DocDB specific. We expect to have 'id' and 'label' and a bunch of 'properties' - * @param node - */ - private static getNodeProperties(node: D3Node): string[] { - let props = ["id", "label"]; - - if (node.hasOwnProperty("properties")) { - props = props.concat(Object.keys(node.properties)); - } - return props; - } - - // Click on non-nodes deselects - private static closest(el: any, predicate: any) { - do { - if (predicate(el)) { - return el; - } - } while ((el = el && el.parentNode)); - } - - private zoomed(event: any) { - this.zoomTransform = { - x: event.transform.x, - y: event.transform.y, - k: event.transform.k - }; - this.g.attr("transform", event.transform); - } - - private instantiateSimulation() { - this.simulation = forceSimulation() - .force( - "link", - forceLink() - .id((d: D3Node) => { - return d.id; - }) - .distance(D3ForceGraph.FORCE_LINK_DISTANCE) - .strength(D3ForceGraph.FORCE_LINK_STRENGTH) - ) - .force("charge", forceManyBody()) - .force( - "collide", - forceCollide(D3ForceGraph.FORCE_COLLIDE_RADIUS) - .strength(D3ForceGraph.FORCE_COLLIDE_STRENGTH) - .iterations(D3ForceGraph.FORCE_COLLIDE_ITERATIONS) - ); - } - - /** - * Shift graph to make this targetPosition as new center - * @param targetPosition - * @return promise with shift offset - */ - private shiftGraph(targetPosition: Point2D): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - const offset = { x: this.width / 2 - targetPosition.x, y: this.height / 2 - targetPosition.y }; - this.viewCenter = targetPosition; - - if (Math.abs(offset.x) > 0.5 && Math.abs(offset.y) > 0.5) { - const transform = () => { - return zoomIdentity - .translate(this.width / 2, this.height / 2) - .scale(this.zoomTransform.k) - .translate(-targetPosition.x, -targetPosition.y); - }; - - this.zoomBackground - .transition() - .duration(D3ForceGraph.TRANSITION_STEP1_MS) - .call(this.zoom.transform, transform) - .on("end", () => { - deferred.resolve(offset); - }); - } else { - deferred.resolve(null); - } - - return deferred.promise; - } - - private onGraphDataUpdate(graph: GraphData) { - // shift all nodes so that new clicked on node is in the center - this.isHighlightDisabled = true; - this.unhighlightNode(); - this.simulation.stop(); - - // Find root node position - const rootId = graph.findRootNodeId(); - - // Remember nodes current position - const posMap = new HashMap(); - this.simulation.nodes().forEach((d: D3Node) => { - if (d.x == undefined || d.y == undefined) { - return; - } - posMap.set(d.id, { x: d.x, y: d.y }); - }); - - const restartSimulation = () => { - this.restartSimulation(graph, posMap); - this.isHighlightDisabled = false; - }; - - if (rootId && posMap.has(rootId)) { - this.shiftGraph(posMap.get(rootId)).then(restartSimulation); - } else { - restartSimulation(); - } - } - - private animateRemoveExitSelections(): Q.Promise { - const deferred1 = Q.defer(); - const deferred2 = Q.defer(); - const linkExitSelection = this.linkSelection.exit(); - - let linkCounter = linkExitSelection.size(); - - if (linkCounter > 0) { - if (D3ForceGraph.useSvgMarkerEnd()) { - this.svg - .select(`#${this.getArrowHeadSymbolId()}-marker`) - .transition() - .duration(D3ForceGraph.TRANSITION_STEP2_MS) - .attr("fill-opacity", 0) - .attr("stroke-opacity", 0); - } else { - this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).classed("hidden", true); - } - - linkExitSelection - .select(".link") - .transition() - .duration(D3ForceGraph.TRANSITION_STEP2_MS) - .attr("stroke-width", 0); - linkExitSelection - .transition() - .delay(D3ForceGraph.TRANSITION_STEP2_MS) - .remove() - .on("end", () => { - if (--linkCounter <= 0) { - deferred1.resolve(); - } - }); - } else { - deferred1.resolve(); - } - - const nodeExitSelection = this.nodeSelection.exit(); - let nodeCounter = nodeExitSelection.size(); - if (nodeCounter > 0) { - nodeExitSelection - .selectAll("circle") - .transition() - .duration(D3ForceGraph.TRANSITION_STEP2_MS) - .attr("r", 0); - nodeExitSelection - .selectAll(".iconContainer") - .transition() - .duration(D3ForceGraph.TRANSITION_STEP2_MS) - .attr("opacity", 0); - nodeExitSelection - .selectAll(".loadmore") - .transition() - .duration(D3ForceGraph.TRANSITION_STEP2_MS) - .attr("opacity", 0); - nodeExitSelection - .selectAll("text") - .transition() - .duration(D3ForceGraph.TRANSITION_STEP2_MS) - .style("opacity", 0); - nodeExitSelection - .transition() - .delay(D3ForceGraph.TRANSITION_STEP2_MS) - .remove() - .on("end", () => { - if (--nodeCounter <= 0) { - deferred2.resolve(); - } - }); - } else { - deferred2.resolve(); - } - - return Q.allSettled([deferred1.promise, deferred2.promise]).then(undefined); - } - - /** - * All non-fixed nodes start from the center and spread them away from the center like fireworks. - * Then fade in the nodes labels and Load More icon. - * @param nodes - * @param newNodes - */ - private animateBigBang(nodes: D3Node[], newNodes: d3.Selection) { - if (!nodes || nodes.length === 0) { - return; - } - const nodeFinalPositionMap = new HashMap(); - - const viewCenter = this.viewCenter; - const nonFixedNodes = _.filter(nodes, (node: D3Node) => { - return !node._isFixedPosition && node.x === viewCenter.x && node.y === viewCenter.y; - }); - - const n = nonFixedNodes.length; - const da = (Math.PI * 2) / n; - const daOffset = Math.random() * Math.PI * 2; - for (let i = 0; i < n; i++) { - const d = nonFixedNodes[i]; - const angle = da * i + daOffset; - - d.fx = viewCenter.x; - d.fy = viewCenter.y; - const x = viewCenter.x + Math.cos(angle) * D3ForceGraph.INITIAL_POSITION_RADIUS; - const y = viewCenter.y + Math.sin(angle) * D3ForceGraph.INITIAL_POSITION_RADIUS; - nodeFinalPositionMap.set(d.id, { x: x, y: y }); - } - - // Animate nodes - newNodes - .transition() - .duration(D3ForceGraph.TRANSITION_STEP3_MS) - .attrTween("transform", (d: D3Node) => { - const finalPos = nodeFinalPositionMap.get(d.id) || { x: viewCenter.x, y: viewCenter.y }; - const ix = interpolateNumber(viewCenter.x, finalPos.x); - const iy = interpolateNumber(viewCenter.y, finalPos.y); - return (t: number) => { - d.fx = ix(t); - d.fy = iy(t); - return this.positionNode(d); - }; - }) - .on("end", (d: D3Node) => { - if (!d._isFixedPosition) { - d.fx = null; - d.fy = null; - } - }); - - // Delay appearance of text and loadMore - newNodes - .selectAll(".caption") - .attr("fill", "#ffffff") - .transition() - .delay(D3ForceGraph.TRANSITION_STEP3_MS - 100) - .duration(D3ForceGraph.TRANSITION_STEP3_MS) - .attrTween("fill", (t: any) => { - const ic = interpolate("#ffffff", "#000000"); - return (t: number) => { - return ic(t); - }; - }); - newNodes - .selectAll(".loadmore") - .attr("visibility", "hidden") - .transition() - .delay(600) - .attr("visibility", "visible"); - } - - private restartSimulation(graph: GraphData, posMap: HashMap) { - if (!graph) { - return; - } - const viewCenter = this.viewCenter; - - // Distribute nodes initial position before simulation - const nodes = graph.vertices; - for (let i = 0; i < nodes.length; i++) { - let v = nodes[i]; - - if (v._isRoot) { - this.rootVertex = v; - } - - if (v._isFixedPosition && posMap.has(v.id)) { - const pos = posMap.get(v.id); - v.fx = pos.x; - v.fy = pos.y; - } else if (v._isRoot) { - v.fx = viewCenter.x; - v.fy = viewCenter.y; - } else if (posMap.has(v.id)) { - const pos = posMap.get(v.id); - v.x = pos.x; - v.y = pos.y; - } else { - v.x = viewCenter.x; - v.y = viewCenter.y; - } - } - - const nodeById = d3Map(nodes, (d: D3Node) => { - return d.id; - }); - const links = graph.edges; - - links.forEach((link: D3Link) => { - link.source = nodeById.get(link.source); - link.target = nodeById.get(link.target); - }); - - this.linkSelection = this.linkSelection.data(links, (l: D3Link) => { - return `${(l.source).id}_${(l.target).id}`; - }); - this.nodeSelection = this.nodeSelection.data(nodes, (d: D3Node) => { - return d.id; - }); - - const removePromise = this.animateRemoveExitSelections(); - - const self = this; - - this.simulation.nodes(nodes).on("tick", ticked); - - this.simulation.force>("link").links(graph.edges); - - removePromise.then(() => { - if (D3ForceGraph.useSvgMarkerEnd()) { - this.svg - .select(`#${this.getArrowHeadSymbolId()}-marker`) - .attr("fill-opacity", 1) - .attr("stroke-opacity", 1); - } else { - this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).classed("hidden", false); - } - const newNodes = this.addNewNodes(); - this.updateLoadMore(this.nodeSelection); - - this.addNewLinks(); - - const nodes = this.simulation.nodes(); - this.redrawGraph(); - - this.animateBigBang(nodes, newNodes); - - this.simulation.alpha(1).restart(); - this.params.onGraphUpdated(new Date().getTime()); - }); - - function ticked() { - self.linkSelection.select(".link").attr("d", (l: D3Link) => { - return self.positionLink(l); - }); - if (!D3ForceGraph.useSvgMarkerEnd()) { - self.linkSelection.select(".markerEnd").attr("transform", (l: D3Link) => { - return self.positionLinkEnd(l); - }); - } - self.nodeSelection.attr("transform", (d: D3Node) => { - return self.positionNode(d); - }); - } - } - - private addNewLinks(): d3.Selection { - const newLinks = this.linkSelection - .enter() - .append("g") - .attr("class", "markerEndContainer"); - - const line = newLinks - .append("path") - .attr("class", "link") - .attr("fill", "none") - .attr("stroke-width", this.params.graphConfig.linkWidth()) - .attr("stroke", this.params.graphConfig.linkColor()); - - if (D3ForceGraph.useSvgMarkerEnd()) { - line.attr("marker-end", `url(#${this.getArrowHeadSymbolId()}-marker)`); - } else { - newLinks - .append("g") - .append("use") - .attr("xlink:href", `#${this.getArrowHeadSymbolId()}-nonMarker`) - .attr("class", "markerEnd link") - .attr("fill", this.params.graphConfig.linkColor()) - .classed(`${this.getArrowHeadSymbolId()}`, true); - } - - this.linkSelection = newLinks.merge(this.linkSelection); - return newLinks; - } - - private addNewNodes(): d3.Selection { - var self = this; - - const newNodes = this.nodeSelection - .enter() - .append("g") - .attr("class", (d: D3Node) => { - return d._isRoot ? "node root" : "node"; - }) - .call( - drag() - .on("start", ((e: D3DragEvent, d: D3Node) => { - return this.dragstarted(d, e); - }) as any) - .on("drag", ((e: D3DragEvent, d: D3Node) => { - return this.dragged(d, e); - }) as any) - .on("end", ((e: D3DragEvent, d: D3Node) => { - return this.dragended(d, e); - }) as any) - ) - .on("mouseover", (_: MouseEvent, d: D3Node) => { - if (this.isHighlightDisabled || this.selectedNode || this.isDragging) { - return; - } - - this.highlightNode(this, d); - this.simulation.stop(); - }) - .on("mouseout", (_: MouseEvent, d: D3Node) => { - if (this.isHighlightDisabled || this.selectedNode || this.isDragging) { - return; - } - - this.unhighlightNode(); - - this.simulation.restart(); - }) - .each((d: D3Node) => { - // Initial position for nodes. This prevents blinking as following the tween transition doesn't always start right away - d.x = self.viewCenter.x; - d.y = self.viewCenter.y; - }); - - newNodes - .append("circle") - .attr("fill", this.getNodeColor.bind(this)) - .attr("class", "main") - .attr("r", this.params.graphConfig.nodeSize()); - - var iconGroup = newNodes - .append("g") - .attr("class", "iconContainer") - .attr("tabindex", 0) - .attr("aria-label", (d: D3Node) => { - return this.retrieveNodeCaption(d); - }) - .on("dblclick", function(_: MouseEvent, d: D3Node) { - // this is the element - self.onNodeClicked(this.parentNode, d); - }) - .on("click", function(_: MouseEvent, d: D3Node) { - // this is the element - self.onNodeClicked(this.parentNode, d); - }) - .on("keypress", function(event: KeyboardEvent, d: D3Node) { - if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { - event.stopPropagation(); - // this is the element - self.onNodeClicked(this.parentNode, d); - } - }); - var nodeSize = this.params.graphConfig.nodeSize(); - var bgsize = nodeSize + 1; - - iconGroup - .append("rect") - .attr("x", -bgsize) - .attr("y", -bgsize) - .attr("width", bgsize * 2) - .attr("height", bgsize * 2) - .attr("fill-opacity", (d: D3Node) => { - return this.params.graphConfig.nodeIconKey() ? 1 : 0; - }) - .attr("class", "icon-background"); - - // Possible icon: if xlink:href is undefined, the image won't show - iconGroup - .append("svg:image") - .attr("xlink:href", (d: D3Node) => { - return D3ForceGraph.computeImageData(d, this.params.graphConfig); - }) - .attr("x", -nodeSize) - .attr("y", -nodeSize) - .attr("height", nodeSize * 2) - .attr("width", nodeSize * 2) - .attr("class", "icon"); - - newNodes - .append("text") - .attr("class", "caption") - .attr("dx", D3ForceGraph.TEXT_DX) - .attr("dy", ".35em") - .text((d: D3Node) => { - return this.retrieveNodeCaption(d); - }); - - this.nodeSelection = newNodes.merge(this.nodeSelection); - - return newNodes; - } - - /** - * Pagination display and buttons - * @param parent - * @param nodeSize - */ - private createPaginationControl(parent: d3.Selection, nodeSize: number) { - const self = this; - const gaugeWidth = 50; - const btnXOffset = gaugeWidth / 2; - const yOffset = 10 + nodeSize; - const gaugeYOffset = yOffset + 3; - const gaugeHeight = 14; - parent - .append("line") - .attr("x1", 0) - .attr("y1", nodeSize) - .attr("x2", 0) - .attr("y2", gaugeYOffset) - .style("stroke-width", 1) - .style("stroke", this.params.graphConfig.linkColor()); - parent - .append("use") - .attr("xlink:href", "#triangleRight") - .attr("class", "pageButton") - .attr("y", yOffset) - .attr("x", btnXOffset) - .attr("aria-label", (d: D3Node) => { - return `Next page of nodes for ${this.retrieveNodeCaption(d)}`; - }) - .attr("tabindex", 0) - .on("click", ((_: MouseEvent, d: D3Node) => { - self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE); - }) as any) - .on("dblclick", ((_: MouseEvent, d: D3Node) => { - self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE); - }) as any) - .on("keypress", ((event: KeyboardEvent, d: D3Node) => { - if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { - event.stopPropagation(); - self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE); - } - }) as any) - .on("mouseover", ((e: MouseEvent, d: D3Node) => { - select(e.target as any).classed("active", true); - }) as any) - .on("mouseout", ((e: MouseEvent, d: D3Node) => { - select(e.target as any).classed("active", false); - }) as any) - .attr("visibility", (d: D3Node) => (!d._outEAllLoaded || !d._inEAllLoaded ? "visible" : "hidden")); - parent - .append("use") - .attr("xlink:href", "#triangleRight") - .attr("class", "pageButton") - .attr("y", yOffset) - .attr("transform", `translate(${-btnXOffset}), scale(-1, 1)`) - .attr("aria-label", (d: D3Node) => { - return `Previous page of nodes for ${this.retrieveNodeCaption(d)}`; - }) - .attr("tabindex", 0) - .on("click", ((_: MouseEvent, d: D3Node) => { - self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE); - }) as any) - .on("dblclick", ((_: MouseEvent, d: D3Node) => { - self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE); - }) as any) - .on("keypress", ((event: KeyboardEvent, d: D3Node) => { - if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { - event.stopPropagation(); - self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE); - } - }) as any) - .on("mouseover", ((e: MouseEvent, d: D3Node) => { - select(e.target as any).classed("active", true); - }) as any) - .on("mouseout", ((e: MouseEvent, d: D3Node) => { - select(e.target as any).classed("active", false); - }) as any) - .attr("visibility", (d: D3Node) => - !d._pagination || d._pagination.currentPage.start !== 0 ? "visible" : "hidden" - ); - parent - .append("rect") - .attr("x", -btnXOffset) - .attr("y", gaugeYOffset) - .attr("width", gaugeWidth) - .attr("height", gaugeHeight) - .style("fill", "white") - .style("stroke-width", 1) - .style("stroke", this.params.graphConfig.linkColor()); - parent - .append("rect") - .attr("x", (d: D3Node) => { - const pageInfo = d._pagination; - return pageInfo && pageInfo.total - ? -btnXOffset + (gaugeWidth * pageInfo.currentPage.start) / pageInfo.total - : 0; - }) - .attr("y", gaugeYOffset) - .attr("width", (d: D3Node) => { - const pageInfo = d._pagination; - return pageInfo && pageInfo.total - ? (gaugeWidth * (pageInfo.currentPage.end - pageInfo.currentPage.start)) / pageInfo.total - : 0; - }) - .attr("height", gaugeHeight) - .style("fill", this.params.graphConfig.nodeColor()) - .attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden")); - parent - .append("text") - .attr("x", 0) - .attr("y", gaugeYOffset + gaugeHeight / 2 + D3ForceGraph.PAGINATION_LINE1_Y_OFFSET_PX) - .text((d: D3Node) => { - const pageInfo = d._pagination; - /* - * pageInfo is zero-based but for the purpose of user display, current page start and end are 1-based. - * current page end is the upper-bound (not included), but for the purpose user display we show included upper-bound - * For example: start = 0, end = 11 will display 1-10. - */ - // pageInfo is zero-based, but for the purpose of user display, current page start and end are 1-based. - // - return `${pageInfo.currentPage.start + 1}-${pageInfo.currentPage.end}`; - }) - .attr("text-anchor", "middle") - .style("font-size", "10px"); - parent - .append("text") - .attr("x", 0) - .attr( - "y", - gaugeYOffset + - gaugeHeight / 2 + - D3ForceGraph.PAGINATION_LINE1_Y_OFFSET_PX + - D3ForceGraph.PAGINATION_LINE2_Y_OFFSET_PX - ) - .text((d: D3Node) => { - const pageInfo = d._pagination; - return `total: ${pageInfo.total}`; - }) - .attr("text-anchor", "middle") - .style("font-size", "10px") - .attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden")); - } - - private createLoadMoreControl(parent: d3.Selection, nodeSize: number) { - const self = this; - parent - .append("use") - .attr("class", "loadMoreIcon") - .attr("xlink:href", "#loadMoreIcon") - .attr("x", -15) - .attr("y", nodeSize) - .attr("aria-label", (d: D3Node) => { - return `Load adjacent nodes for ${this.retrieveNodeCaption(d)}`; - }) - .attr("tabindex", 0) - .on("click", ((_: MouseEvent, d: D3Node) => { - self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE); - }) as any) - .on("dblclick", ((_: MouseEvent, d: D3Node) => { - self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE); - }) as any) - .on("keypress", ((event: KeyboardEvent, d: D3Node) => { - if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { - event.stopPropagation(); - self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE); - } - }) as any) - .on("mouseover", ((e: MouseEvent, d: D3Node) => { - select(e.target as any).classed("active", true); - }) as any) - .on("mouseout", ((e: MouseEvent, d: D3Node) => { - select(e.target as any).classed("active", false); - }) as any); - } - - /** - * Remove LoadMore subassembly for existing nodes that show all their children in the graph - */ - private updateLoadMore(nodeSelection: d3.Selection) { - const self = this; - nodeSelection.selectAll(".loadmore").remove(); - - var nodeSize = this.params.graphConfig.nodeSize(); - const rootSelectionG = nodeSelection - .filter((d: D3Node) => { - return !!d._isRoot && !!d._pagination; - }) - .append("g") - .attr("class", "loadmore"); - this.createPaginationControl(rootSelectionG, nodeSize); - - const nodeNeighborMap = D3ForceGraph.countEdges(this.linkSelection.data()); - const missingNeighborNonRootG = nodeSelection - .filter((d: D3Node) => { - return !( - d._isRoot || - (d._outEAllLoaded && - d._inEAllLoaded && - nodeNeighborMap.get(d.id) >= d._outEdgeIds.length + d._inEdgeIds.length) - ); - }) - .append("g") - .attr("class", "loadmore"); - this.createLoadMoreControl(missingNeighborNonRootG, nodeSize); - - // Don't color icons individually, just the definitions - this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); - } - - /** - * Load neighbors of this node - */ - private loadNeighbors(v: D3Node, pageAction: PAGE_ACTION) { - if (!this.graphDataWrapper.hasVertexId(v.id)) { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Clicked node not in graph data. id: ${v.id}`); - return; - } - - this.params.onLoadMoreData({ - nodeId: v.id, - pageAction: pageAction - }); - } - - /** - * If not mapped, return max Color - * @param key - */ - private lookupColorFromKey(key: string): string { - let index = this.uniqueValues.indexOf(key); - if (index < 0 || index >= D3ForceGraph.MAX_COLOR_NB) { - index = D3ForceGraph.MAX_COLOR_NB - 1; - } - return D3ForceGraph.COLOR_SCHEME(index.toString()); - } - - /** - * Get node color - * If nodeColorKey is defined, lookup the node color from uniqueStrings. - * Otherwise use nodeColor. - * @param d - */ - private getNodeColor(d: D3Node): string { - if (this.params.graphConfig.nodeColorKey()) { - const val = GraphData.getNodePropValue(d, this.params.graphConfig.nodeColorKey()); - return this.lookupColorFromKey(val); - } else { - return this.params.graphConfig.nodeColor(); - } - } - - private dragstarted(d: D3Node, event: D3DragEvent) { - this.isDragging = true; - if (!event.active) { - this.simulation.alphaTarget(0.3).restart(); - } - d.fx = d.x; - d.fy = d.y; - } - - private dragged(d: D3Node, event: D3DragEvent) { - d.fx = event.x; - d.fy = event.y; - } - - private dragended(d: D3Node, event: D3DragEvent) { - this.isDragging = false; - if (!event.active) { - this.simulation.alphaTarget(0); - } - - d.fx = null; - d.fy = null; - } - - private highlightNode(g: any, d: D3Node) { - this.fadeNonNeighbors(d.id); - this.params.onHighlightedNode({ g: g, id: d.id }); - } - - private unhighlightNode() { - this.g.selectAll(".node").classed("inactive", false); - this.g.selectAll(".link").classed("inactive", false); - this.params.onHighlightedNode(null); - - this.setRootAsHighlighted(); - } - - /** - * Set the root node as highlighted, but don't fade neighbors. - * We use this to show the root properties - */ - private setRootAsHighlighted() { - if (!this.rootVertex) { - return; - } - - this.params.onHighlightedNode({ g: null, id: this.rootVertex.id }); - } - - private fadeNonNeighbors(nodeId: string) { - this.g.selectAll(".node").classed("inactive", (d: D3Node) => { - var neighbors = (showNeighborType => { - switch (showNeighborType) { - case NeighborType.SOURCES_ONLY: - return this.graphDataWrapper.getSourcesForId(nodeId); - case NeighborType.TARGETS_ONLY: - return this.graphDataWrapper.getTargetsForId(nodeId); - default: - case NeighborType.BOTH: - return (this.graphDataWrapper.getSourcesForId(nodeId) || []).concat( - this.graphDataWrapper.getTargetsForId(nodeId) - ); - } - })(this.params.graphConfig.showNeighborType()); - return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId; - }); - - this.g.selectAll(".link").classed("inactive", (l: D3Link) => { - switch (this.params.graphConfig.showNeighborType()) { - case NeighborType.SOURCES_ONLY: - return (l.target).id !== nodeId; - case NeighborType.TARGETS_ONLY: - return (l.source).id !== nodeId; - default: - case NeighborType.BOTH: - return (l.target).id !== nodeId && (l.source).id !== nodeId; - } - }); - } - - private onNodeClicked(g: d3.BaseType, d: D3Node) { - if (this.isHighlightDisabled) { - return; - } - - if (g === this.selectedNode) { - this.deselectNode(); - return; - } - - // unselect old none - select(this.selectedNode).classed("selected", false); - this.unhighlightNode(); - - // select new one - select(g).classed("selected", true); - this.selectedNode = g; - this.highlightNode(g, d); - } - - private deselectNode() { - if (!this.selectedNode) { - return; - } - - // Unselect - select(this.selectedNode).classed("selected", false); - this.selectedNode = null; - this.unhighlightNode(); - } - - private retrieveNodeCaption(d: D3Node) { - let key = this.params.graphConfig.nodeCaption(); - let value: string = d.id || d.label; - if (key) { - value = GraphData.getNodePropValue(d, key) || ""; - } - - // Manually ellipsize - if (value.length > D3ForceGraph.NODE_LABEL_MAX_CHAR_LENGTH) { - value = value.substr(0, D3ForceGraph.NODE_LABEL_MAX_CHAR_LENGTH) + "\u2026"; - } - return value; - } - - private static calculateClosestPIOver2(angle: number): number { - const CURVATURE_FACTOR = 40; - const result = Math.atan(CURVATURE_FACTOR * (angle - Math.PI / 4)) / 2 + Math.PI / 4; - return result; - } - - private static calculateClosestPIOver4(angle: number): number { - const CURVATURE_FACTOR = 100; - const result = Math.atan(CURVATURE_FACTOR * (angle - Math.PI / 8)) / 4 + Math.PI / 8; - return result; - } - - private static calculateControlPoint(start: Point2D, end: Point2D): Point2D { - const alpha = Math.atan2(end.y - start.y, end.x - start.x); - const n = Math.floor(alpha / (Math.PI / 2)); - const reducedAlpha = alpha - (n * Math.PI) / 2; - const reducedBeta = D3ForceGraph.calculateClosestPIOver2(reducedAlpha); - const beta = reducedBeta + (n * Math.PI) / 2; - - const length = Math.sqrt((end.y - start.y) * (end.y - start.y) + (end.x - start.x) * (end.x - start.x)) / 2; - const result = { - x: start.x + Math.cos(beta) * length, - y: start.y + Math.sin(beta) * length - }; - - return result; - } - - private positionLinkEnd(l: D3Link) { - const source: Point2D = { x: (l.source).x, y: (l.source).y }; - const target: Point2D = { x: (l.target).x, y: (l.target).y }; - const d1 = D3ForceGraph.calculateControlPoint(source, target); - var radius = this.params.graphConfig.nodeSize() + 3; - - // End - const dx = target.x - d1.x; - const dy = target.y - d1.y; - const angle = Math.atan2(dy, dx); - var ux = target.x - Math.cos(angle) * radius; - var uy = target.y - Math.sin(angle) * radius; - - return `translate(${ux},${uy}) rotate(${(angle * 180) / Math.PI})`; - } - - private positionLink(l: D3Link) { - const source: Point2D = { x: (l.source).x, y: (l.source).y }; - const target: Point2D = { x: (l.target).x, y: (l.target).y }; - const d1 = D3ForceGraph.calculateControlPoint(source, target); - var radius = this.params.graphConfig.nodeSize() + 3; - - // Start - var dx = d1.x - source.x; - var dy = d1.y - source.y; - var angle = Math.atan2(dy, dx); - var tx = source.x + Math.cos(angle) * radius; - var ty = source.y + Math.sin(angle) * radius; - - // End - dx = target.x - d1.x; - dy = target.y - d1.y; - angle = Math.atan2(dy, dx); - var ux = target.x - Math.cos(angle) * radius; - var uy = target.y - Math.sin(angle) * radius; - - return "M" + tx + "," + ty + "S" + d1.x + "," + d1.y + " " + ux + "," + uy; - } - - private positionNode(d: D3Node) { - return "translate(" + d.x + "," + d.y + ")"; - } - - private redrawGraph() { - if (!this.simulation) { - return; - } - - this.g.selectAll(".node").attr("class", (d: D3Node) => { - return d._isRoot ? "node root" : "node"; - }); - - this.applyConfig(this.params.graphConfig); - } - - private static computeImageData(d: D3Node, config: GraphConfig): string { - let propValue = GraphData.getNodePropValue(d, config.nodeIconKey()) || ""; - // Trim leading and trailing spaces to make comparison more forgiving. - let value = config.iconsMap()[propValue.trim()]; - if (!value) { - return undefined; - } - return `data:image/${value.format};base64,${value.data}`; - } - - /** - * Update graph according to configuration or use default - */ - private applyConfig(config: GraphConfig) { - if (config.nodeIconKey()) { - this.g - .selectAll(".node .icon") - .attr("xlink:href", (d: D3Node) => { - return D3ForceGraph.computeImageData(d, config); - }) - .attr("x", -config.nodeSize()) - .attr("y", -config.nodeSize()) - .attr("height", config.nodeSize() * 2) - .attr("width", config.nodeSize() * 2) - .attr("class", "icon"); - } else { - // clear icons - this.g.selectAll(".node .icon").attr("xlink:href", undefined); - } - this.g.selectAll(".node .icon-background").attr("fill-opacity", (d: D3Node) => { - return config.nodeIconKey() ? 1 : 0; - }); - - this.g.selectAll(".node text.caption").text((d: D3Node) => { - return this.retrieveNodeCaption(d); - }); - - this.g.selectAll(".node circle.main").attr("r", config.nodeSize()); - this.g.selectAll(".node text.caption").attr("dx", config.nodeSize() + 2); - - this.g.selectAll(".node circle").attr("fill", this.getNodeColor.bind(this)); - - // Can't color nodes individually if using defs - this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); - - this.g.selectAll(".link").attr("stroke-width", config.linkWidth()); - - this.g.selectAll(".link").attr("stroke", config.linkColor()); - if (D3ForceGraph.useSvgMarkerEnd()) { - this.svg - .select(`#${this.getArrowHeadSymbolId()}-marker`) - .attr("fill", config.linkColor()) - .attr("stroke", config.linkColor()); - } else { - this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor()); - } - - // Reset highlight - this.g.selectAll(".node circle").attr("opacity", null); - } - - /** - * On Edge browsers, there's a bug when using the marker-end attribute. - * It make the whole app reload when zooming and panning. - * So we draw the arrow heads manually and must also reposition manually. - * In this case, the UX is slightly degraded (no animation when removing arrow) - */ - private static useSvgMarkerEnd(): boolean { - // Use for all browsers except Edge - return window.navigator.userAgent.indexOf("Edge") === -1; - } -} +import * as ko from "knockout"; +import Q from "q"; +import { schemeCategory10 } from "d3-scale-chromatic"; +import { selectAll, select } from "d3-selection"; +import { zoom, zoomIdentity } from "d3-zoom"; +import { scaleOrdinal } from "d3-scale"; +import { forceSimulation, forceLink, forceCollide, forceManyBody } from "d3-force"; +import { interpolateNumber, interpolate } from "d3-interpolate"; +import { map as d3Map } from "d3-collection"; +import { drag, D3DragEvent } from "d3-drag"; + +import _ from "underscore"; +import { NeighborType } from "../../../Contracts/ViewModels"; +import { GraphData, D3Node, D3Link } from "./GraphData"; +import { HashMap } from "../../../Common/HashMap"; +import { BaseType } from "d3"; +import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; +import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; +import { GraphConfig } from "../../Tabs/GraphTab"; +import { GraphExplorer } from "./GraphExplorer"; +import * as Constants from "../../../Common/Constants"; + +export interface D3GraphIconMap { + [key: string]: { data: string; format: string }; +} + +export interface D3GraphNodeData { + g: any; // TODO needs this? + id: string; +} + +export enum PAGE_ACTION { + FIRST_PAGE, + PREVIOUS_PAGE, + NEXT_PAGE, +} + +export interface LoadMoreDataAction { + nodeId: string; + pageAction: PAGE_ACTION; +} + +interface Point2D { + x: number; + y: number; +} + +interface ZoomTransform extends Point2D { + k: number; +} + +export interface D3ForceGraphParameters { + // Graph to parent + graphConfig: GraphConfig; + onHighlightedNode: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph + onLoadMoreData: (action: LoadMoreDataAction) => void; + + // parent to graph + onInitialized: (instance: GraphRenderer) => void; + + // For unit testing purposes + onGraphUpdated: (timestamp: number) => void; +} + +export interface GraphRenderer { + selectNode(id: string): void; + resetZoom(): void; + updateGraph(graphData: GraphData): void; + enableHighlight(enable: boolean): void; +} + +/** This is the custom Knockout handler for the d3 graph */ +export class D3ForceGraph implements GraphRenderer { + // Some constants + private static readonly GRAPH_WIDTH_PX = 900; + private static readonly GRAPH_HEIGHT_PX = 700; + private static readonly TEXT_DX = 12; + private static readonly FORCE_COLLIDE_RADIUS = 40; + private static readonly FORCE_COLLIDE_STRENGTH = 0.2; + private static readonly FORCE_COLLIDE_ITERATIONS = 1; + private static readonly NODE_LABEL_MAX_CHAR_LENGTH = 16; + private static readonly FORCE_LINK_DISTANCE = 100; + private static readonly FORCE_LINK_STRENGTH = 0.005; + private static readonly INITIAL_POSITION_RADIUS = 150; + private static readonly TRANSITION_STEP1_MS = 1000; + private static readonly TRANSITION_STEP2_MS = 1000; + private static readonly TRANSITION_STEP3_MS = 700; + private static readonly PAGINATION_LINE1_Y_OFFSET_PX = 4; + private static readonly PAGINATION_LINE2_Y_OFFSET_PX = 14; + + // We limit the number of different colors to 20 + private static readonly COLOR_SCHEME = scaleOrdinal(schemeCategory10); + private static readonly MAX_COLOR_NB = 20; + + // Some state variables + private static instanceCount = 1; + private instanceIndex: number; + private svg: d3.Selection; + private g: d3.Selection; + private simulation: d3.Simulation; + private width: number; + private height: number; + private selectedNode: d3.BaseType; + private isDragging: boolean; + private rootVertex: D3Node; + private nodeSelection: any; + private linkSelection: any; + private zoomTransform: ZoomTransform; + private zoom: d3.ZoomBehavior; + private zoomBackground: d3.Selection; + private viewCenter: Point2D; + + // Map a property to a graph node attribute (such as color) + private uniqueValues: (string | number)[]; // keep track of unique values + private graphDataWrapper: GraphData; + + // Communication with outside + // Graph -> outside + public params: D3ForceGraphParameters; + public errorMsgs: ko.ObservableArray; // errors happen in graph + + // outside -> Graph + private idToSelect: ko.Observable; // Programmatically select node by id outside graph + private isHighlightDisabled: boolean; + + public constructor(params: D3ForceGraphParameters) { + this.params = params; + this.idToSelect = ko.observable(null); + this.errorMsgs = ko.observableArray([]); + this.graphDataWrapper = null; + + this.width = D3ForceGraph.GRAPH_WIDTH_PX; + this.height = D3ForceGraph.GRAPH_HEIGHT_PX; + + this.rootVertex = null; + this.isHighlightDisabled = false; + this.zoomTransform = { x: 0, y: 0, k: 1 }; + this.viewCenter = { x: this.width / 2, y: this.height / 2 }; + + this.instanceIndex = D3ForceGraph.instanceCount++; + } + + public init(element: Element): void { + this.initializeGraph(element); + this.params.onInitialized(this); + } + + public destroy(): void { + this.simulation.stop(); + this.simulation = null; + this.graphDataWrapper = null; + this.linkSelection = null; + this.nodeSelection = null; + this.g.remove(); + } + + public updateGraph(newGraph: GraphData): void { + if (!newGraph || !this.simulation) { + return; + } + // Build new GraphData object from it + this.graphDataWrapper = new GraphData(); + this.graphDataWrapper.setData(newGraph); + + const key = this.params.graphConfig.nodeColorKey(); + if (key !== GraphExplorer.NONE_CHOICE) { + this.updateUniqueValues(key); + } + + // d3 expects source and target properties for each link (edge) + $.each(this.graphDataWrapper.edges, (i, e) => { + e.target = e.inV; + e.source = e.outV; + }); + + this.onGraphDataUpdate(this.graphDataWrapper); + } + + public resetZoom(): void { + this.zoomBackground.call(this.zoom.transform, zoomIdentity); + this.viewCenter = { x: this.width / 2, y: this.height / 2 }; + } + + public enableHighlight(enable: boolean): void { + this.isHighlightDisabled = enable; + } + + public selectNode(id: string): void { + this.idToSelect(id); + } + + public getArrowHeadSymbolId(): string { + return `triangle-${this.instanceIndex}`; + } + + /** + * Count edges and store in a hashmap: vertex id <--> number of links + * @param linkSelection + */ + public static countEdges(links: D3Link[]): HashMap { + const countMap = new HashMap(); + links.forEach((l: D3Link) => { + let val = countMap.get(l.inV) || 0; + val += 1; + countMap.set(l.inV, val); + + val = countMap.get(l.outV) || 0; + val += 1; + countMap.set(l.outV, val); + }); + + return countMap; + } + + /** + * Construct the graph + * @param graphData + */ + private initializeGraph(element: Element): void { + this.zoom = zoom() + .scaleExtent([1 / 2, 4]) + .on("zoom", this.zoomed.bind(this)); + + this.svg = select(element) + .attr("viewBox", `0 0 ${this.width} ${this.height}`) + .attr("preserveAspectRatio", "xMidYMid slice"); + + this.zoomBackground = this.svg.call(this.zoom); + + element.addEventListener("click", (e: Event) => { + // IE 11 doesn't support el.classList and there's no polyfill for it + // Don't auto-deselect when not clicking on a node + if (!(e.target).classList) { + return; + } + + if ( + !D3ForceGraph.closest(e.target, (el: any) => { + return !!el && el !== document && el.classList.contains("node"); + }) + ) { + this.deselectNode(); + } + }); + + this.g = this.svg.append("g"); + this.linkSelection = this.g.append("g").attr("class", "links").selectAll(".link"); + this.nodeSelection = this.g.append("g").attr("class", "nodes").selectAll(".node"); + + // Reset state variables + this.selectedNode = null; + this.isDragging = false; + + this.idToSelect.subscribe((newVal) => { + if (!newVal) { + this.deselectNode(); + return; + } + + var self = this; + // Select this node id + selectAll(".node") + .filter(function (d: D3Node, i) { + return d.id === newVal; + }) + .each(function (d: D3Node) { + self.onNodeClicked(this, d); + }); + }); + + // Redraw if any of these configs change + this.params.graphConfig.nodeColor.subscribe(this.redrawGraph.bind(this)); + this.params.graphConfig.nodeColorKey.subscribe((key: string) => { + // Compute colormap + this.uniqueValues = []; + this.updateUniqueValues(key); + this.redrawGraph(); + }); + this.params.graphConfig.linkColor.subscribe(() => this.redrawGraph()); + this.params.graphConfig.showNeighborType.subscribe(() => this.redrawGraph()); + this.params.graphConfig.nodeCaption.subscribe(() => this.redrawGraph()); + this.params.graphConfig.nodeSize.subscribe(() => this.redrawGraph()); + this.params.graphConfig.linkWidth.subscribe(() => this.redrawGraph()); + this.params.graphConfig.nodeIconKey.subscribe(() => this.redrawGraph()); + this.instantiateSimulation(); + } // initialize + + private updateUniqueValues(key: string) { + for (var i = 0; i < this.graphDataWrapper.vertices.length; i++) { + let vertex = this.graphDataWrapper.vertices[i]; + + let props = D3ForceGraph.getNodeProperties(vertex); + if (props.indexOf(key) === -1) { + // Vertex doesn't have the property + continue; + } + let val = GraphData.getNodePropValue(vertex, key); + if (typeof val !== "string" && typeof val !== "number") { + // Not a type we can map + continue; + } + + // Map this value if new + if (this.uniqueValues.indexOf(val) === -1) { + this.uniqueValues.push(val); + } + + if (this.uniqueValues.length === D3ForceGraph.MAX_COLOR_NB) { + this.errorMsgs.push( + `Number of unique values for property ${key} exceeds maximum (${D3ForceGraph.MAX_COLOR_NB})` + ); + // ignore rest of values + break; + } + } + } + + /** + * Retrieve all node properties + * NOTE: This is DocDB specific. We expect to have 'id' and 'label' and a bunch of 'properties' + * @param node + */ + private static getNodeProperties(node: D3Node): string[] { + let props = ["id", "label"]; + + if (node.hasOwnProperty("properties")) { + props = props.concat(Object.keys(node.properties)); + } + return props; + } + + // Click on non-nodes deselects + private static closest(el: any, predicate: any) { + do { + if (predicate(el)) { + return el; + } + } while ((el = el && el.parentNode)); + } + + private zoomed(event: any) { + this.zoomTransform = { + x: event.transform.x, + y: event.transform.y, + k: event.transform.k, + }; + this.g.attr("transform", event.transform); + } + + private instantiateSimulation() { + this.simulation = forceSimulation() + .force( + "link", + forceLink() + .id((d: D3Node) => { + return d.id; + }) + .distance(D3ForceGraph.FORCE_LINK_DISTANCE) + .strength(D3ForceGraph.FORCE_LINK_STRENGTH) + ) + .force("charge", forceManyBody()) + .force( + "collide", + forceCollide(D3ForceGraph.FORCE_COLLIDE_RADIUS) + .strength(D3ForceGraph.FORCE_COLLIDE_STRENGTH) + .iterations(D3ForceGraph.FORCE_COLLIDE_ITERATIONS) + ); + } + + /** + * Shift graph to make this targetPosition as new center + * @param targetPosition + * @return promise with shift offset + */ + private shiftGraph(targetPosition: Point2D): Q.Promise { + const deferred: Q.Deferred = Q.defer(); + const offset = { x: this.width / 2 - targetPosition.x, y: this.height / 2 - targetPosition.y }; + this.viewCenter = targetPosition; + + if (Math.abs(offset.x) > 0.5 && Math.abs(offset.y) > 0.5) { + const transform = () => { + return zoomIdentity + .translate(this.width / 2, this.height / 2) + .scale(this.zoomTransform.k) + .translate(-targetPosition.x, -targetPosition.y); + }; + + this.zoomBackground + .transition() + .duration(D3ForceGraph.TRANSITION_STEP1_MS) + .call(this.zoom.transform, transform) + .on("end", () => { + deferred.resolve(offset); + }); + } else { + deferred.resolve(null); + } + + return deferred.promise; + } + + private onGraphDataUpdate(graph: GraphData) { + // shift all nodes so that new clicked on node is in the center + this.isHighlightDisabled = true; + this.unhighlightNode(); + this.simulation.stop(); + + // Find root node position + const rootId = graph.findRootNodeId(); + + // Remember nodes current position + const posMap = new HashMap(); + this.simulation.nodes().forEach((d: D3Node) => { + if (d.x == undefined || d.y == undefined) { + return; + } + posMap.set(d.id, { x: d.x, y: d.y }); + }); + + const restartSimulation = () => { + this.restartSimulation(graph, posMap); + this.isHighlightDisabled = false; + }; + + if (rootId && posMap.has(rootId)) { + this.shiftGraph(posMap.get(rootId)).then(restartSimulation); + } else { + restartSimulation(); + } + } + + private animateRemoveExitSelections(): Q.Promise { + const deferred1 = Q.defer(); + const deferred2 = Q.defer(); + const linkExitSelection = this.linkSelection.exit(); + + let linkCounter = linkExitSelection.size(); + + if (linkCounter > 0) { + if (D3ForceGraph.useSvgMarkerEnd()) { + this.svg + .select(`#${this.getArrowHeadSymbolId()}-marker`) + .transition() + .duration(D3ForceGraph.TRANSITION_STEP2_MS) + .attr("fill-opacity", 0) + .attr("stroke-opacity", 0); + } else { + this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).classed("hidden", true); + } + + linkExitSelection.select(".link").transition().duration(D3ForceGraph.TRANSITION_STEP2_MS).attr("stroke-width", 0); + linkExitSelection + .transition() + .delay(D3ForceGraph.TRANSITION_STEP2_MS) + .remove() + .on("end", () => { + if (--linkCounter <= 0) { + deferred1.resolve(); + } + }); + } else { + deferred1.resolve(); + } + + const nodeExitSelection = this.nodeSelection.exit(); + let nodeCounter = nodeExitSelection.size(); + if (nodeCounter > 0) { + nodeExitSelection.selectAll("circle").transition().duration(D3ForceGraph.TRANSITION_STEP2_MS).attr("r", 0); + nodeExitSelection + .selectAll(".iconContainer") + .transition() + .duration(D3ForceGraph.TRANSITION_STEP2_MS) + .attr("opacity", 0); + nodeExitSelection + .selectAll(".loadmore") + .transition() + .duration(D3ForceGraph.TRANSITION_STEP2_MS) + .attr("opacity", 0); + nodeExitSelection.selectAll("text").transition().duration(D3ForceGraph.TRANSITION_STEP2_MS).style("opacity", 0); + nodeExitSelection + .transition() + .delay(D3ForceGraph.TRANSITION_STEP2_MS) + .remove() + .on("end", () => { + if (--nodeCounter <= 0) { + deferred2.resolve(); + } + }); + } else { + deferred2.resolve(); + } + + return Q.allSettled([deferred1.promise, deferred2.promise]).then(undefined); + } + + /** + * All non-fixed nodes start from the center and spread them away from the center like fireworks. + * Then fade in the nodes labels and Load More icon. + * @param nodes + * @param newNodes + */ + private animateBigBang(nodes: D3Node[], newNodes: d3.Selection) { + if (!nodes || nodes.length === 0) { + return; + } + const nodeFinalPositionMap = new HashMap(); + + const viewCenter = this.viewCenter; + const nonFixedNodes = _.filter(nodes, (node: D3Node) => { + return !node._isFixedPosition && node.x === viewCenter.x && node.y === viewCenter.y; + }); + + const n = nonFixedNodes.length; + const da = (Math.PI * 2) / n; + const daOffset = Math.random() * Math.PI * 2; + for (let i = 0; i < n; i++) { + const d = nonFixedNodes[i]; + const angle = da * i + daOffset; + + d.fx = viewCenter.x; + d.fy = viewCenter.y; + const x = viewCenter.x + Math.cos(angle) * D3ForceGraph.INITIAL_POSITION_RADIUS; + const y = viewCenter.y + Math.sin(angle) * D3ForceGraph.INITIAL_POSITION_RADIUS; + nodeFinalPositionMap.set(d.id, { x: x, y: y }); + } + + // Animate nodes + newNodes + .transition() + .duration(D3ForceGraph.TRANSITION_STEP3_MS) + .attrTween("transform", (d: D3Node) => { + const finalPos = nodeFinalPositionMap.get(d.id) || { x: viewCenter.x, y: viewCenter.y }; + const ix = interpolateNumber(viewCenter.x, finalPos.x); + const iy = interpolateNumber(viewCenter.y, finalPos.y); + return (t: number) => { + d.fx = ix(t); + d.fy = iy(t); + return this.positionNode(d); + }; + }) + .on("end", (d: D3Node) => { + if (!d._isFixedPosition) { + d.fx = null; + d.fy = null; + } + }); + + // Delay appearance of text and loadMore + newNodes + .selectAll(".caption") + .attr("fill", "#ffffff") + .transition() + .delay(D3ForceGraph.TRANSITION_STEP3_MS - 100) + .duration(D3ForceGraph.TRANSITION_STEP3_MS) + .attrTween("fill", (t: any) => { + const ic = interpolate("#ffffff", "#000000"); + return (t: number) => { + return ic(t); + }; + }); + newNodes.selectAll(".loadmore").attr("visibility", "hidden").transition().delay(600).attr("visibility", "visible"); + } + + private restartSimulation(graph: GraphData, posMap: HashMap) { + if (!graph) { + return; + } + const viewCenter = this.viewCenter; + + // Distribute nodes initial position before simulation + const nodes = graph.vertices; + for (let i = 0; i < nodes.length; i++) { + let v = nodes[i]; + + if (v._isRoot) { + this.rootVertex = v; + } + + if (v._isFixedPosition && posMap.has(v.id)) { + const pos = posMap.get(v.id); + v.fx = pos.x; + v.fy = pos.y; + } else if (v._isRoot) { + v.fx = viewCenter.x; + v.fy = viewCenter.y; + } else if (posMap.has(v.id)) { + const pos = posMap.get(v.id); + v.x = pos.x; + v.y = pos.y; + } else { + v.x = viewCenter.x; + v.y = viewCenter.y; + } + } + + const nodeById = d3Map(nodes, (d: D3Node) => { + return d.id; + }); + const links = graph.edges; + + links.forEach((link: D3Link) => { + link.source = nodeById.get(link.source); + link.target = nodeById.get(link.target); + }); + + this.linkSelection = this.linkSelection.data(links, (l: D3Link) => { + return `${(l.source).id}_${(l.target).id}`; + }); + this.nodeSelection = this.nodeSelection.data(nodes, (d: D3Node) => { + return d.id; + }); + + const removePromise = this.animateRemoveExitSelections(); + + const self = this; + + this.simulation.nodes(nodes).on("tick", ticked); + + this.simulation.force>("link").links(graph.edges); + + removePromise.then(() => { + if (D3ForceGraph.useSvgMarkerEnd()) { + this.svg.select(`#${this.getArrowHeadSymbolId()}-marker`).attr("fill-opacity", 1).attr("stroke-opacity", 1); + } else { + this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).classed("hidden", false); + } + const newNodes = this.addNewNodes(); + this.updateLoadMore(this.nodeSelection); + + this.addNewLinks(); + + const nodes = this.simulation.nodes(); + this.redrawGraph(); + + this.animateBigBang(nodes, newNodes); + + this.simulation.alpha(1).restart(); + this.params.onGraphUpdated(new Date().getTime()); + }); + + function ticked() { + self.linkSelection.select(".link").attr("d", (l: D3Link) => { + return self.positionLink(l); + }); + if (!D3ForceGraph.useSvgMarkerEnd()) { + self.linkSelection.select(".markerEnd").attr("transform", (l: D3Link) => { + return self.positionLinkEnd(l); + }); + } + self.nodeSelection.attr("transform", (d: D3Node) => { + return self.positionNode(d); + }); + } + } + + private addNewLinks(): d3.Selection { + const newLinks = this.linkSelection.enter().append("g").attr("class", "markerEndContainer"); + + const line = newLinks + .append("path") + .attr("class", "link") + .attr("fill", "none") + .attr("stroke-width", this.params.graphConfig.linkWidth()) + .attr("stroke", this.params.graphConfig.linkColor()); + + if (D3ForceGraph.useSvgMarkerEnd()) { + line.attr("marker-end", `url(#${this.getArrowHeadSymbolId()}-marker)`); + } else { + newLinks + .append("g") + .append("use") + .attr("xlink:href", `#${this.getArrowHeadSymbolId()}-nonMarker`) + .attr("class", "markerEnd link") + .attr("fill", this.params.graphConfig.linkColor()) + .classed(`${this.getArrowHeadSymbolId()}`, true); + } + + this.linkSelection = newLinks.merge(this.linkSelection); + return newLinks; + } + + private addNewNodes(): d3.Selection { + var self = this; + + const newNodes = this.nodeSelection + .enter() + .append("g") + .attr("class", (d: D3Node) => { + return d._isRoot ? "node root" : "node"; + }) + .call( + drag() + .on("start", ((e: D3DragEvent, d: D3Node) => { + return this.dragstarted(d, e); + }) as any) + .on("drag", ((e: D3DragEvent, d: D3Node) => { + return this.dragged(d, e); + }) as any) + .on("end", ((e: D3DragEvent, d: D3Node) => { + return this.dragended(d, e); + }) as any) + ) + .on("mouseover", (_: MouseEvent, d: D3Node) => { + if (this.isHighlightDisabled || this.selectedNode || this.isDragging) { + return; + } + + this.highlightNode(this, d); + this.simulation.stop(); + }) + .on("mouseout", (_: MouseEvent, d: D3Node) => { + if (this.isHighlightDisabled || this.selectedNode || this.isDragging) { + return; + } + + this.unhighlightNode(); + + this.simulation.restart(); + }) + .each((d: D3Node) => { + // Initial position for nodes. This prevents blinking as following the tween transition doesn't always start right away + d.x = self.viewCenter.x; + d.y = self.viewCenter.y; + }); + + newNodes + .append("circle") + .attr("fill", this.getNodeColor.bind(this)) + .attr("class", "main") + .attr("r", this.params.graphConfig.nodeSize()); + + var iconGroup = newNodes + .append("g") + .attr("class", "iconContainer") + .attr("tabindex", 0) + .attr("aria-label", (d: D3Node) => { + return this.retrieveNodeCaption(d); + }) + .on("dblclick", function (_: MouseEvent, d: D3Node) { + // this is the element + self.onNodeClicked(this.parentNode, d); + }) + .on("click", function (_: MouseEvent, d: D3Node) { + // this is the element + self.onNodeClicked(this.parentNode, d); + }) + .on("keypress", function (event: KeyboardEvent, d: D3Node) { + if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { + event.stopPropagation(); + // this is the element + self.onNodeClicked(this.parentNode, d); + } + }); + var nodeSize = this.params.graphConfig.nodeSize(); + var bgsize = nodeSize + 1; + + iconGroup + .append("rect") + .attr("x", -bgsize) + .attr("y", -bgsize) + .attr("width", bgsize * 2) + .attr("height", bgsize * 2) + .attr("fill-opacity", (d: D3Node) => { + return this.params.graphConfig.nodeIconKey() ? 1 : 0; + }) + .attr("class", "icon-background"); + + // Possible icon: if xlink:href is undefined, the image won't show + iconGroup + .append("svg:image") + .attr("xlink:href", (d: D3Node) => { + return D3ForceGraph.computeImageData(d, this.params.graphConfig); + }) + .attr("x", -nodeSize) + .attr("y", -nodeSize) + .attr("height", nodeSize * 2) + .attr("width", nodeSize * 2) + .attr("class", "icon"); + + newNodes + .append("text") + .attr("class", "caption") + .attr("dx", D3ForceGraph.TEXT_DX) + .attr("dy", ".35em") + .text((d: D3Node) => { + return this.retrieveNodeCaption(d); + }); + + this.nodeSelection = newNodes.merge(this.nodeSelection); + + return newNodes; + } + + /** + * Pagination display and buttons + * @param parent + * @param nodeSize + */ + private createPaginationControl(parent: d3.Selection, nodeSize: number) { + const self = this; + const gaugeWidth = 50; + const btnXOffset = gaugeWidth / 2; + const yOffset = 10 + nodeSize; + const gaugeYOffset = yOffset + 3; + const gaugeHeight = 14; + parent + .append("line") + .attr("x1", 0) + .attr("y1", nodeSize) + .attr("x2", 0) + .attr("y2", gaugeYOffset) + .style("stroke-width", 1) + .style("stroke", this.params.graphConfig.linkColor()); + parent + .append("use") + .attr("xlink:href", "#triangleRight") + .attr("class", "pageButton") + .attr("y", yOffset) + .attr("x", btnXOffset) + .attr("aria-label", (d: D3Node) => { + return `Next page of nodes for ${this.retrieveNodeCaption(d)}`; + }) + .attr("tabindex", 0) + .on("click", ((_: MouseEvent, d: D3Node) => { + self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE); + }) as any) + .on("dblclick", ((_: MouseEvent, d: D3Node) => { + self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE); + }) as any) + .on("keypress", ((event: KeyboardEvent, d: D3Node) => { + if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { + event.stopPropagation(); + self.loadNeighbors(d, PAGE_ACTION.NEXT_PAGE); + } + }) as any) + .on("mouseover", ((e: MouseEvent, d: D3Node) => { + select(e.target as any).classed("active", true); + }) as any) + .on("mouseout", ((e: MouseEvent, d: D3Node) => { + select(e.target as any).classed("active", false); + }) as any) + .attr("visibility", (d: D3Node) => (!d._outEAllLoaded || !d._inEAllLoaded ? "visible" : "hidden")); + parent + .append("use") + .attr("xlink:href", "#triangleRight") + .attr("class", "pageButton") + .attr("y", yOffset) + .attr("transform", `translate(${-btnXOffset}), scale(-1, 1)`) + .attr("aria-label", (d: D3Node) => { + return `Previous page of nodes for ${this.retrieveNodeCaption(d)}`; + }) + .attr("tabindex", 0) + .on("click", ((_: MouseEvent, d: D3Node) => { + self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE); + }) as any) + .on("dblclick", ((_: MouseEvent, d: D3Node) => { + self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE); + }) as any) + .on("keypress", ((event: KeyboardEvent, d: D3Node) => { + if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { + event.stopPropagation(); + self.loadNeighbors(d, PAGE_ACTION.PREVIOUS_PAGE); + } + }) as any) + .on("mouseover", ((e: MouseEvent, d: D3Node) => { + select(e.target as any).classed("active", true); + }) as any) + .on("mouseout", ((e: MouseEvent, d: D3Node) => { + select(e.target as any).classed("active", false); + }) as any) + .attr("visibility", (d: D3Node) => + !d._pagination || d._pagination.currentPage.start !== 0 ? "visible" : "hidden" + ); + parent + .append("rect") + .attr("x", -btnXOffset) + .attr("y", gaugeYOffset) + .attr("width", gaugeWidth) + .attr("height", gaugeHeight) + .style("fill", "white") + .style("stroke-width", 1) + .style("stroke", this.params.graphConfig.linkColor()); + parent + .append("rect") + .attr("x", (d: D3Node) => { + const pageInfo = d._pagination; + return pageInfo && pageInfo.total + ? -btnXOffset + (gaugeWidth * pageInfo.currentPage.start) / pageInfo.total + : 0; + }) + .attr("y", gaugeYOffset) + .attr("width", (d: D3Node) => { + const pageInfo = d._pagination; + return pageInfo && pageInfo.total + ? (gaugeWidth * (pageInfo.currentPage.end - pageInfo.currentPage.start)) / pageInfo.total + : 0; + }) + .attr("height", gaugeHeight) + .style("fill", this.params.graphConfig.nodeColor()) + .attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden")); + parent + .append("text") + .attr("x", 0) + .attr("y", gaugeYOffset + gaugeHeight / 2 + D3ForceGraph.PAGINATION_LINE1_Y_OFFSET_PX) + .text((d: D3Node) => { + const pageInfo = d._pagination; + /* + * pageInfo is zero-based but for the purpose of user display, current page start and end are 1-based. + * current page end is the upper-bound (not included), but for the purpose user display we show included upper-bound + * For example: start = 0, end = 11 will display 1-10. + */ + // pageInfo is zero-based, but for the purpose of user display, current page start and end are 1-based. + // + return `${pageInfo.currentPage.start + 1}-${pageInfo.currentPage.end}`; + }) + .attr("text-anchor", "middle") + .style("font-size", "10px"); + parent + .append("text") + .attr("x", 0) + .attr( + "y", + gaugeYOffset + + gaugeHeight / 2 + + D3ForceGraph.PAGINATION_LINE1_Y_OFFSET_PX + + D3ForceGraph.PAGINATION_LINE2_Y_OFFSET_PX + ) + .text((d: D3Node) => { + const pageInfo = d._pagination; + return `total: ${pageInfo.total}`; + }) + .attr("text-anchor", "middle") + .style("font-size", "10px") + .attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden")); + } + + private createLoadMoreControl(parent: d3.Selection, nodeSize: number) { + const self = this; + parent + .append("use") + .attr("class", "loadMoreIcon") + .attr("xlink:href", "#loadMoreIcon") + .attr("x", -15) + .attr("y", nodeSize) + .attr("aria-label", (d: D3Node) => { + return `Load adjacent nodes for ${this.retrieveNodeCaption(d)}`; + }) + .attr("tabindex", 0) + .on("click", ((_: MouseEvent, d: D3Node) => { + self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE); + }) as any) + .on("dblclick", ((_: MouseEvent, d: D3Node) => { + self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE); + }) as any) + .on("keypress", ((event: KeyboardEvent, d: D3Node) => { + if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { + event.stopPropagation(); + self.loadNeighbors(d, PAGE_ACTION.FIRST_PAGE); + } + }) as any) + .on("mouseover", ((e: MouseEvent, d: D3Node) => { + select(e.target as any).classed("active", true); + }) as any) + .on("mouseout", ((e: MouseEvent, d: D3Node) => { + select(e.target as any).classed("active", false); + }) as any); + } + + /** + * Remove LoadMore subassembly for existing nodes that show all their children in the graph + */ + private updateLoadMore(nodeSelection: d3.Selection) { + const self = this; + nodeSelection.selectAll(".loadmore").remove(); + + var nodeSize = this.params.graphConfig.nodeSize(); + const rootSelectionG = nodeSelection + .filter((d: D3Node) => { + return !!d._isRoot && !!d._pagination; + }) + .append("g") + .attr("class", "loadmore"); + this.createPaginationControl(rootSelectionG, nodeSize); + + const nodeNeighborMap = D3ForceGraph.countEdges(this.linkSelection.data()); + const missingNeighborNonRootG = nodeSelection + .filter((d: D3Node) => { + return !( + d._isRoot || + (d._outEAllLoaded && + d._inEAllLoaded && + nodeNeighborMap.get(d.id) >= d._outEdgeIds.length + d._inEdgeIds.length) + ); + }) + .append("g") + .attr("class", "loadmore"); + this.createLoadMoreControl(missingNeighborNonRootG, nodeSize); + + // Don't color icons individually, just the definitions + this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); + } + + /** + * Load neighbors of this node + */ + private loadNeighbors(v: D3Node, pageAction: PAGE_ACTION) { + if (!this.graphDataWrapper.hasVertexId(v.id)) { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Clicked node not in graph data. id: ${v.id}`); + return; + } + + this.params.onLoadMoreData({ + nodeId: v.id, + pageAction: pageAction, + }); + } + + /** + * If not mapped, return max Color + * @param key + */ + private lookupColorFromKey(key: string): string { + let index = this.uniqueValues.indexOf(key); + if (index < 0 || index >= D3ForceGraph.MAX_COLOR_NB) { + index = D3ForceGraph.MAX_COLOR_NB - 1; + } + return D3ForceGraph.COLOR_SCHEME(index.toString()); + } + + /** + * Get node color + * If nodeColorKey is defined, lookup the node color from uniqueStrings. + * Otherwise use nodeColor. + * @param d + */ + private getNodeColor(d: D3Node): string { + if (this.params.graphConfig.nodeColorKey()) { + const val = GraphData.getNodePropValue(d, this.params.graphConfig.nodeColorKey()); + return this.lookupColorFromKey(val); + } else { + return this.params.graphConfig.nodeColor(); + } + } + + private dragstarted(d: D3Node, event: D3DragEvent) { + this.isDragging = true; + if (!event.active) { + this.simulation.alphaTarget(0.3).restart(); + } + d.fx = d.x; + d.fy = d.y; + } + + private dragged(d: D3Node, event: D3DragEvent) { + d.fx = event.x; + d.fy = event.y; + } + + private dragended(d: D3Node, event: D3DragEvent) { + this.isDragging = false; + if (!event.active) { + this.simulation.alphaTarget(0); + } + + d.fx = null; + d.fy = null; + } + + private highlightNode(g: any, d: D3Node) { + this.fadeNonNeighbors(d.id); + this.params.onHighlightedNode({ g: g, id: d.id }); + } + + private unhighlightNode() { + this.g.selectAll(".node").classed("inactive", false); + this.g.selectAll(".link").classed("inactive", false); + this.params.onHighlightedNode(null); + + this.setRootAsHighlighted(); + } + + /** + * Set the root node as highlighted, but don't fade neighbors. + * We use this to show the root properties + */ + private setRootAsHighlighted() { + if (!this.rootVertex) { + return; + } + + this.params.onHighlightedNode({ g: null, id: this.rootVertex.id }); + } + + private fadeNonNeighbors(nodeId: string) { + this.g.selectAll(".node").classed("inactive", (d: D3Node) => { + var neighbors = ((showNeighborType) => { + switch (showNeighborType) { + case NeighborType.SOURCES_ONLY: + return this.graphDataWrapper.getSourcesForId(nodeId); + case NeighborType.TARGETS_ONLY: + return this.graphDataWrapper.getTargetsForId(nodeId); + default: + case NeighborType.BOTH: + return (this.graphDataWrapper.getSourcesForId(nodeId) || []).concat( + this.graphDataWrapper.getTargetsForId(nodeId) + ); + } + })(this.params.graphConfig.showNeighborType()); + return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId; + }); + + this.g.selectAll(".link").classed("inactive", (l: D3Link) => { + switch (this.params.graphConfig.showNeighborType()) { + case NeighborType.SOURCES_ONLY: + return (l.target).id !== nodeId; + case NeighborType.TARGETS_ONLY: + return (l.source).id !== nodeId; + default: + case NeighborType.BOTH: + return (l.target).id !== nodeId && (l.source).id !== nodeId; + } + }); + } + + private onNodeClicked(g: d3.BaseType, d: D3Node) { + if (this.isHighlightDisabled) { + return; + } + + if (g === this.selectedNode) { + this.deselectNode(); + return; + } + + // unselect old none + select(this.selectedNode).classed("selected", false); + this.unhighlightNode(); + + // select new one + select(g).classed("selected", true); + this.selectedNode = g; + this.highlightNode(g, d); + } + + private deselectNode() { + if (!this.selectedNode) { + return; + } + + // Unselect + select(this.selectedNode).classed("selected", false); + this.selectedNode = null; + this.unhighlightNode(); + } + + private retrieveNodeCaption(d: D3Node) { + let key = this.params.graphConfig.nodeCaption(); + let value: string = d.id || d.label; + if (key) { + value = GraphData.getNodePropValue(d, key) || ""; + } + + // Manually ellipsize + if (value.length > D3ForceGraph.NODE_LABEL_MAX_CHAR_LENGTH) { + value = value.substr(0, D3ForceGraph.NODE_LABEL_MAX_CHAR_LENGTH) + "\u2026"; + } + return value; + } + + private static calculateClosestPIOver2(angle: number): number { + const CURVATURE_FACTOR = 40; + const result = Math.atan(CURVATURE_FACTOR * (angle - Math.PI / 4)) / 2 + Math.PI / 4; + return result; + } + + private static calculateClosestPIOver4(angle: number): number { + const CURVATURE_FACTOR = 100; + const result = Math.atan(CURVATURE_FACTOR * (angle - Math.PI / 8)) / 4 + Math.PI / 8; + return result; + } + + private static calculateControlPoint(start: Point2D, end: Point2D): Point2D { + const alpha = Math.atan2(end.y - start.y, end.x - start.x); + const n = Math.floor(alpha / (Math.PI / 2)); + const reducedAlpha = alpha - (n * Math.PI) / 2; + const reducedBeta = D3ForceGraph.calculateClosestPIOver2(reducedAlpha); + const beta = reducedBeta + (n * Math.PI) / 2; + + const length = Math.sqrt((end.y - start.y) * (end.y - start.y) + (end.x - start.x) * (end.x - start.x)) / 2; + const result = { + x: start.x + Math.cos(beta) * length, + y: start.y + Math.sin(beta) * length, + }; + + return result; + } + + private positionLinkEnd(l: D3Link) { + const source: Point2D = { x: (l.source).x, y: (l.source).y }; + const target: Point2D = { x: (l.target).x, y: (l.target).y }; + const d1 = D3ForceGraph.calculateControlPoint(source, target); + var radius = this.params.graphConfig.nodeSize() + 3; + + // End + const dx = target.x - d1.x; + const dy = target.y - d1.y; + const angle = Math.atan2(dy, dx); + var ux = target.x - Math.cos(angle) * radius; + var uy = target.y - Math.sin(angle) * radius; + + return `translate(${ux},${uy}) rotate(${(angle * 180) / Math.PI})`; + } + + private positionLink(l: D3Link) { + const source: Point2D = { x: (l.source).x, y: (l.source).y }; + const target: Point2D = { x: (l.target).x, y: (l.target).y }; + const d1 = D3ForceGraph.calculateControlPoint(source, target); + var radius = this.params.graphConfig.nodeSize() + 3; + + // Start + var dx = d1.x - source.x; + var dy = d1.y - source.y; + var angle = Math.atan2(dy, dx); + var tx = source.x + Math.cos(angle) * radius; + var ty = source.y + Math.sin(angle) * radius; + + // End + dx = target.x - d1.x; + dy = target.y - d1.y; + angle = Math.atan2(dy, dx); + var ux = target.x - Math.cos(angle) * radius; + var uy = target.y - Math.sin(angle) * radius; + + return "M" + tx + "," + ty + "S" + d1.x + "," + d1.y + " " + ux + "," + uy; + } + + private positionNode(d: D3Node) { + return "translate(" + d.x + "," + d.y + ")"; + } + + private redrawGraph() { + if (!this.simulation) { + return; + } + + this.g.selectAll(".node").attr("class", (d: D3Node) => { + return d._isRoot ? "node root" : "node"; + }); + + this.applyConfig(this.params.graphConfig); + } + + private static computeImageData(d: D3Node, config: GraphConfig): string { + let propValue = GraphData.getNodePropValue(d, config.nodeIconKey()) || ""; + // Trim leading and trailing spaces to make comparison more forgiving. + let value = config.iconsMap()[propValue.trim()]; + if (!value) { + return undefined; + } + return `data:image/${value.format};base64,${value.data}`; + } + + /** + * Update graph according to configuration or use default + */ + private applyConfig(config: GraphConfig) { + if (config.nodeIconKey()) { + this.g + .selectAll(".node .icon") + .attr("xlink:href", (d: D3Node) => { + return D3ForceGraph.computeImageData(d, config); + }) + .attr("x", -config.nodeSize()) + .attr("y", -config.nodeSize()) + .attr("height", config.nodeSize() * 2) + .attr("width", config.nodeSize() * 2) + .attr("class", "icon"); + } else { + // clear icons + this.g.selectAll(".node .icon").attr("xlink:href", undefined); + } + this.g.selectAll(".node .icon-background").attr("fill-opacity", (d: D3Node) => { + return config.nodeIconKey() ? 1 : 0; + }); + + this.g.selectAll(".node text.caption").text((d: D3Node) => { + return this.retrieveNodeCaption(d); + }); + + this.g.selectAll(".node circle.main").attr("r", config.nodeSize()); + this.g.selectAll(".node text.caption").attr("dx", config.nodeSize() + 2); + + this.g.selectAll(".node circle").attr("fill", this.getNodeColor.bind(this)); + + // Can't color nodes individually if using defs + this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); + + this.g.selectAll(".link").attr("stroke-width", config.linkWidth()); + + this.g.selectAll(".link").attr("stroke", config.linkColor()); + if (D3ForceGraph.useSvgMarkerEnd()) { + this.svg + .select(`#${this.getArrowHeadSymbolId()}-marker`) + .attr("fill", config.linkColor()) + .attr("stroke", config.linkColor()); + } else { + this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor()); + } + + // Reset highlight + this.g.selectAll(".node circle").attr("opacity", null); + } + + /** + * On Edge browsers, there's a bug when using the marker-end attribute. + * It make the whole app reload when zooming and panning. + * So we draw the arrow heads manually and must also reposition manually. + * In this case, the UX is slightly degraded (no animation when removing arrow) + */ + private static useSvgMarkerEnd(): boolean { + // Use for all browsers except Edge + return window.navigator.userAgent.indexOf("Edge") === -1; + } +} diff --git a/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx index 5422a01b2..58ba8def1 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx @@ -101,7 +101,7 @@ export class EditorNeighborsComponent extends React.Component - Delete this.removeAddedEdgeToNeighbor(index)} /> + Delete this.removeAddedEdgeToNeighbor(index)} /> diff --git a/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx index 20cb5e7a9..c8bd96c47 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx @@ -14,7 +14,7 @@ describe("", () => { readOnlyProperties: [ { key: "singlevalueprop", - values: [{ value: "abcd", type: "string" }] + values: [{ value: "abcd", type: "string" }], }, { key: "multivaluesprop", @@ -24,14 +24,14 @@ describe("", () => { { value: true, type: "boolean" }, { value: false, type: "boolean" }, { value: undefined, type: "null" }, - { value: null, type: "null" } - ] - } + { value: null, type: "null" }, + ], + }, ], existingProperties: [ { key: "singlevalueprop2", - values: [{ value: "ijkl", type: "string" }] + values: [{ value: "ijkl", type: "string" }], }, { key: "multivaluesprop2", @@ -41,14 +41,14 @@ describe("", () => { { value: true, type: "boolean" }, { value: false, type: "boolean" }, { value: undefined, type: "null" }, - { value: null, type: "null" } - ] - } + { value: null, type: "null" }, + ], + }, ], addedProperties: [], - droppedKeys: [] + droppedKeys: [], }, - onUpdateProperties: (editedProperties: EditedProperties): void => {} + onUpdateProperties: (editedProperties: EditedProperties): void => {}, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -61,27 +61,27 @@ describe("", () => { readOnlyProperties: [ { key: "unicode1", - values: [{ value: "Véronique", type: "string" }] + values: [{ value: "Véronique", type: "string" }], }, { key: "unicode2", - values: [{ value: "亜妃子", type: "string" }] - } + values: [{ value: "亜妃子", type: "string" }], + }, ], existingProperties: [ { key: "unicode1", - values: [{ value: "André", type: "string" }] + values: [{ value: "André", type: "string" }], }, { key: "unicode2", - values: [{ value: "あきら, アキラ,安喜良", type: "string" }] - } + values: [{ value: "あきら, アキラ,安喜良", type: "string" }], + }, ], addedProperties: [], - droppedKeys: [] + droppedKeys: [], }, - onUpdateProperties: (editedProperties: EditedProperties): void => {} + onUpdateProperties: (editedProperties: EditedProperties): void => {}, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx index 3ae1ea143..eadf9ee36 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx @@ -79,7 +79,7 @@ export class EditorNodePropertiesComponent extends React.Component ReadOnlyNodePropertiesComponent.renderReadOnlyPropertyKeyPair( nodeProp.key, - nodeProp.values.map(val => val.value) + nodeProp.values.map((val) => val.value) ) )} @@ -112,7 +112,7 @@ export class EditorNodePropertiesComponent extends React.Component { + onChange={(e) => { singleValue.value = e.target.value; this.props.onUpdateProperties(this.props.editedProperties); }} @@ -123,7 +123,7 @@ export class EditorNodePropertiesComponent extends React.Component { + onChange={(e) => { singleValue.type = e.target.value as ViewModels.InputPropertyValueTypeString; if (singleValue.type === "null") { singleValue.value = null; @@ -144,7 +144,7 @@ export class EditorNodePropertiesComponent extends React.Component this.removeExistingProperty(key)} + onActivated={(e) => this.removeExistingProperty(key)} > Delete @@ -157,14 +157,16 @@ export class EditorNodePropertiesComponent extends React.Component {nodeProp.key} - {nodeProp.values.map(value => ReadOnlyNodePropertiesComponent.renderSinglePropertyValue(value.value))} + + {nodeProp.values.map((value) => ReadOnlyNodePropertiesComponent.renderSinglePropertyValue(value.value))} + this.removeExistingProperty(nodeProp.key)} + onActivated={(e) => this.removeExistingProperty(nodeProp.key)} > Delete @@ -188,7 +190,7 @@ export class EditorNodePropertiesComponent extends React.Component { + onChange={(e) => { addedProperty.key = e.target.value; this.props.onUpdateProperties(this.props.editedProperties); }} @@ -201,7 +203,7 @@ export class EditorNodePropertiesComponent extends React.Component { + onChange={(e) => { firstValue.value = e.target.value; if (firstValue.type === "null") { firstValue.value = null; @@ -215,7 +217,7 @@ export class EditorNodePropertiesComponent extends React.Component { + onChange={(e) => { firstValue.type = e.target.value as ViewModels.InputPropertyValueTypeString; this.props.onUpdateProperties(this.props.editedProperties); }} @@ -233,7 +235,7 @@ export class EditorNodePropertiesComponent extends React.Component this.removeAddedProperty(index)} + onActivated={(e) => this.removeAddedProperty(index)} > Delete diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphData.test.ts b/src/Explorer/Graph/GraphExplorerComponent/GraphData.test.ts index 279c708d1..11b50ea7c 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphData.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphData.test.ts @@ -69,8 +69,8 @@ describe("Graph Data", () => { id: "id", label: "label", properties: { - testString: [{ id: "123", value: stringValue }] - } + testString: [{ id: "123", value: stringValue }], + }, }, "testString" ); @@ -85,8 +85,8 @@ describe("Graph Data", () => { id: "id", label: "label", properties: { - testString: [{ id: "123", value: numberValue }] - } + testString: [{ id: "123", value: numberValue }], + }, }, "testString" ); @@ -101,8 +101,8 @@ describe("Graph Data", () => { id: "id", label: "label", properties: { - testString: [{ id: "123", value: booleanValue }] - } + testString: [{ id: "123", value: booleanValue }], + }, }, "testString" ); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphData.ts b/src/Explorer/Graph/GraphExplorerComponent/GraphData.ts index 9cbf502bd..6b7bbd458 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphData.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphData.ts @@ -152,7 +152,7 @@ export class GraphData { const v = this.getVertexById(e.outV); GraphData.addOutE(v, p, { id: e.id, - inV: e.outV + inV: e.outV, }); } }); @@ -165,7 +165,7 @@ export class GraphData { const v = this.getVertexById(e.inV); GraphData.addInE(v, p, { id: e.id, - outV: e.inV + outV: e.inV, }); } }); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx index c381dc8c8..cee9c4c76 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx @@ -52,8 +52,8 @@ describe("Check whether query result is edge-vertex array", () => { GraphExplorer.isEdgeVertexPairArray([ { e: { id: "ide", type: "edge" }, - v: { id: "idv", type: "vertex" } - } + v: { id: "idv", type: "vertex" }, + }, ]) ).toBe(true); }); @@ -75,7 +75,7 @@ describe("getPkIdFromDocumentId", () => { _self: "_self", _etag: "_etag", _ts: 1234, - ...override + ...override, }); it("should create pkid pair from non-partitioned graph", () => { @@ -171,7 +171,7 @@ describe("GraphExplorer", () => { /* TODO Figure out how to make this Knockout-free */ graphConfigUiData: graphConfigUi, - graphConfig: graphConfig + graphConfig: graphConfig, }; }; @@ -244,7 +244,7 @@ describe("GraphExplorer", () => { selectNode: sinon.spy(), resetZoom: sinon.spy(), updateGraph: sinon.stub().callsFake(() => complete()), - enableHighlight: sinon.spy() + enableHighlight: sinon.spy(), }; graphExplorer.d3ForceGraph = mockGraphRenderer; }); @@ -268,7 +268,7 @@ describe("GraphExplorer", () => { client.params.successCallback({ requestId: requestId, data: backendResponse.response, - requestCharge: gremlinRU + requestCharge: gremlinRU, }); if (backendResponse.isLast) { @@ -305,7 +305,7 @@ describe("GraphExplorer", () => { _query: query, nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {}, hasMoreResults: () => false, - executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {} + executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}, }; }); (queryDocumentsPage as jest.Mock).mockImplementation( @@ -318,7 +318,7 @@ describe("GraphExplorer", () => { documents: docDBResponse.response, activityId: "", headers: [] as any[], - requestCharge: gVRU + requestCharge: gVRU, }); } ); @@ -343,11 +343,11 @@ describe("GraphExplorer", () => { }); describe("Load Graph button", () => { - beforeEach(async done => { + beforeEach(async (done) => { const backendResponses: BackendResponses = {}; backendResponses["g.V()"] = backendResponses["g.V('1')"] = { response: [{ id: "1", type: "vertex" }], - isLast: false + isLast: false, }; backendResponses[createFetchOutEQuery("1", GraphExplorer.LOAD_PAGE_SIZE + 1)] = { response: [], isLast: false }; backendResponses[createFetchInEQuery("1", GraphExplorer.LOAD_PAGE_SIZE + 1)] = { response: [], isLast: true }; @@ -370,7 +370,7 @@ describe("GraphExplorer", () => { it("should submit g.V() as docdb query with proper parameters", () => { expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, { maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, - enableCrossPartitionQuery: true + enableCrossPartitionQuery: true, }); }); @@ -380,11 +380,11 @@ describe("GraphExplorer", () => { }); describe("Execute Gremlin Query button", () => { - beforeEach(done => { + beforeEach((done) => { const backendResponses: BackendResponses = {}; backendResponses["g.V()"] = backendResponses["g.V('2')"] = { response: [{ id: "2", type: "vertex" }], - isLast: false + isLast: false, }; backendResponses[createFetchOutEQuery("2", GraphExplorer.LOAD_PAGE_SIZE + 1)] = { response: [], isLast: false }; backendResponses[createFetchInEQuery("2", GraphExplorer.LOAD_PAGE_SIZE + 1)] = { response: [], isLast: true }; @@ -407,7 +407,7 @@ describe("GraphExplorer", () => { it("should submit g.V() as docdb query with proper parameters", () => { expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, { maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, - enableCrossPartitionQuery: true + enableCrossPartitionQuery: true, }); }); @@ -435,10 +435,10 @@ describe("GraphExplorer", () => { inV: node2Id, outV: node1Id, label: linkLabel, - type: "edge" + type: "edge", }; - beforeEach(done => { + beforeEach((done) => { const backendResponses: BackendResponses = {}; // TODO Make this less dependent on spaces, order and quotes backendResponses["g.V()"] = backendResponses[`g.V('${node1Id}','${node2Id}')`] = { @@ -447,15 +447,15 @@ describe("GraphExplorer", () => { id: node1Id, label: label1, type: "vertex", - properties: { prop1Id: [{ id: "id123", value: prop1Val1 }] } + properties: { prop1Id: [{ id: "id123", value: prop1Val1 }] }, }, { id: node2Id, label: label2, - type: "vertex" - } + type: "vertex", + }, ], - isLast: false + isLast: false, }; backendResponses[createFetchOutEQuery(node1Id, GraphExplorer.LOAD_PAGE_SIZE + 1)] = { @@ -465,17 +465,17 @@ describe("GraphExplorer", () => { v: { id: node2Id, label: label2, - type: "vertex" - } - } + type: "vertex", + }, + }, ], - isLast: false + isLast: false, }; backendResponses[createFetchInEQuery(node1Id, GraphExplorer.LOAD_PAGE_SIZE)] = { response: [], isLast: true }; backendResponses[createFetchOutEQuery(node2Id, GraphExplorer.LOAD_PAGE_SIZE + 1)] = { response: [], - isLast: false + isLast: false, }; backendResponses[createFetchInEQuery(node2Id, GraphExplorer.LOAD_PAGE_SIZE + 1)] = { response: [ @@ -485,16 +485,16 @@ describe("GraphExplorer", () => { inV: node2Id, outV: node1Id, label: linkLabel, - type: "edge" + type: "edge", }, v: { id: node1Id, label: label1, - type: "vertex" - } - } + type: "vertex", + }, + }, ], - isLast: true + isLast: true, }; const docDBResponse: AjaxResponse = { response: [{ id: node1Id }, { id: node2Id }], isLast: false }; @@ -581,7 +581,7 @@ describe("GraphExplorer", () => { describe("Select root node", () => { let loadNeighborsPageStub: sinon.SinonSpy; - beforeEach(done => { + beforeEach((done) => { loadNeighborsPageStub = sinon.stub(graphExplorerInstance, "loadNeighborsPage").callsFake(() => { return Q.resolve(); }); @@ -660,12 +660,12 @@ describe("GraphExplorer", () => { let processGremlinQueryResultsStub: sinon.SinonSpy; let graphExplorerInstance: GraphExplorer; - beforeEach(done => { + beforeEach((done) => { const backendResponses: BackendResponses = {}; // TODO Make this less dependent on spaces, order and quotes backendResponses["g.V()"] = { response: "invalid response", - isLast: true + isLast: true, }; const docDBResponse: AjaxResponse = { response: [], isLast: false }; @@ -695,11 +695,11 @@ describe("GraphExplorer", () => { }); describe("when isGraphAutoVizDisabled setting is true (autoviz disabled)", () => { - beforeEach(done => { + beforeEach((done) => { const backendResponses: BackendResponses = {}; backendResponses["g.V()"] = backendResponses["g.V('3')"] = { response: [{ id: "3", type: "vertex" }], - isLast: true + isLast: true, }; const docDBResponse: AjaxResponse = { response: [{ id: "3" }], isLast: false }; diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index af2d90059..7ebd38b6e 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -93,7 +93,7 @@ enum FilterQueryStatus { GraphResult, Loading, NonGraphResult, - ErrorResult + ErrorResult, } interface GraphExplorerState { @@ -159,7 +159,7 @@ enum ResultDisplay { None, Graph, Json, - Stats + Stats, } interface UserQueryResult { @@ -237,7 +237,7 @@ export class GraphExplorer extends React.Component this.renderResultAsJson() + render: () => this.renderResultAsJson(), }, - isVisible: () => true + isVisible: () => true, }, { title: "Graph", content: { className: "graphTabContent", - render: () => this.renderResultAsGraph() + render: () => this.renderResultAsGraph(), }, - isVisible: () => this.state.filterQueryStatus === FilterQueryStatus.GraphResult + isVisible: () => this.state.filterQueryStatus === FilterQueryStatus.GraphResult, }, { title: GraphExplorer.QUERY_STATS_BUTTON_LABEL, content: { className: "graphTabContent", - render: () => this.renderResultStats() + render: () => this.renderResultStats(), }, - isVisible: () => true - } + isVisible: () => true, + }, ]; this.queryRawData = null; @@ -286,7 +286,7 @@ export class GraphExplorer extends React.Component { + this.props.graphConfigUiData.nodeCaptionChoice.subscribe((key) => { this.props.graphConfig.nodeCaption(key); const selectedNode = this.state.highlightedNode; if (selectedNode) { @@ -295,20 +295,20 @@ export class GraphExplorer extends React.Component { + this.props.graphConfigUiData.nodeColorKeyChoice.subscribe((val) => { this.props.graphConfig.nodeColorKey(val === GraphExplorer.NONE_CHOICE ? null : val); this.render(); }); - this.props.graphConfigUiData.showNeighborType.subscribe(val => { + this.props.graphConfigUiData.showNeighborType.subscribe((val) => { this.props.graphConfig.showNeighborType(val); this.render(); }); - this.props.graphConfigUiData.nodeIconChoice.subscribe(val => { + this.props.graphConfigUiData.nodeIconChoice.subscribe((val) => { this.updateNodeIcons(val, this.props.graphConfigUiData.nodeIconSet()); this.render(); }); - this.props.graphConfigUiData.nodeIconSet.subscribe(val => { + this.props.graphConfigUiData.nodeIconSet.subscribe((val) => { this.updateNodeIcons(this.props.graphConfigUiData.nodeIconChoice(), val); this.render(); }); @@ -316,7 +316,7 @@ export class GraphExplorer extends React.Component { + finalProperties.forEach((p) => { // Partition key cannot be updated if (p.key === partitionKeyProperty) { return; @@ -370,7 +370,7 @@ export class GraphExplorer extends React.Component { newIconsMap[doc["_graph_icon_property_value"]] = { data: doc["icon"], - format: doc["format"] + format: doc["format"], }; }); @@ -1172,7 +1172,7 @@ export class GraphExplorer extends React.Component { const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; - const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() || - "id"}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`; + const q = `SELECT c.id, c["${ + this.props.graphConfigUiData.nodeCaptionChoice() || "id" + }"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`; return this.executeNonPagedDocDbQuery(q).then( (documents: DataModels.DocumentId[]) => { let possibleVertices = [] as PossibleVertex[]; @@ -1414,13 +1415,13 @@ export class GraphExplorer extends React.Component gremlinProperty.value); + props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value); } // update neighbors @@ -1564,7 +1565,7 @@ export class GraphExplorer extends React.Component[], - targets: targets //[] + targets: targets, //[] }; // Update KO @@ -1627,7 +1628,7 @@ export class GraphExplorer extends React.Component { return { caption: value, value: value }; } - ) + ), }); } @@ -1687,11 +1688,11 @@ export class GraphExplorer extends React.Component this.onMiddlePaneInitialized(instance), - onGraphUpdated: this.onGraphUpdated.bind(this) + onGraphUpdated: this.onGraphUpdated.bind(this), }; const graphVizProp: GraphVizComponentProps = { - forceGraphParams: forceGraphParams + forceGraphParams: forceGraphParams, }; return ( @@ -1741,13 +1742,13 @@ export class GraphExplorer extends React.Component { id: "id", label: "label", inE: { - inEdge: [{ id: "id1", outV: "outV1" }] - } + inEdge: [{ id: "id1", outV: "outV1" }], + }, }; GraphUtil.createEdgesfromNode(v, graphData); const expectedEdge: GremlinEdge = { id: "id1", inV: "id", outV: "outV1", label: "inEdge" }; @@ -33,8 +33,8 @@ describe("Process Gremlin vertex", () => { id: "id", label: "label", outE: { - outEdge: [{ id: "id2", inV: "inV2" }] - } + outEdge: [{ id: "id2", inV: "inV2" }], + }, }; GraphUtil.createEdgesfromNode(v, graphData); const expectedEdge: GremlinEdge = { id: "id2", inV: "inV2", outV: "id", label: "outEdge" }; @@ -47,14 +47,14 @@ describe("Process Gremlin vertex", () => { id: "id", label: "label", inE: { - inEdge: [{ id: "id1", outV: "outV1" }] + inEdge: [{ id: "id1", outV: "outV1" }], }, outE: { outEdge: [ { id: "id2", inV: "inV2" }, - { id: "id3", inV: "inV3" } - ] - } + { id: "id3", inV: "inV3" }, + ], + }, }; const newNodes = {}; GraphUtil.createEdgesfromNode(v, graphData, newNodes); @@ -83,7 +83,7 @@ describe("getLimitedArrayString()", () => { it("should handle nth element makes it exceed max limit", () => { const expected = { result: "'1','2'", - consumedCount: 2 + consumedCount: 2, }; expect(GraphUtil.getLimitedArrayString(["1", "2", "12345", "4", "5"], 10)).toEqual(expected); }); @@ -91,7 +91,7 @@ describe("getLimitedArrayString()", () => { it("should consume all elements if limit never exceeding limit", () => { const expected = { result: "'1','22','3'", - consumedCount: 3 + consumedCount: 3, }; expect(GraphUtil.getLimitedArrayString(["1", "22", "3"], 12)).toEqual(expected); }); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts index 9b7bfd92d..7467e11bc 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts @@ -32,7 +32,7 @@ export class GraphUtil { id: edge.id, label: label, inV: edge.inV, - outV: vertex.id + outV: vertex.id, }; graphData.addEdge(e); @@ -51,7 +51,7 @@ export class GraphUtil { id: edge.id, label: label, inV: vertex.id, - outV: edge.outV + outV: edge.outV, }; graphData.addEdge(e); @@ -89,7 +89,7 @@ export class GraphUtil { return { result: output, - consumedCount: i + 1 + consumedCount: i + 1, }; } @@ -113,8 +113,9 @@ export class GraphUtil { }().as('v').select('e', 'v')`; } else { const start = startIndex - joined.consumedCount; - gremlinQuery = `g.V(${pkid}).${outE ? "outE" : "inE"}()${hasWithoutStep}.range(${start},${start + - pageSize}).as('e').${outE ? "inV" : "outV"}().as('v').select('e', 'v')`; + gremlinQuery = `g.V(${pkid}).${outE ? "outE" : "inE"}()${hasWithoutStep}.range(${start},${ + start + pageSize + }).as('e').${outE ? "inV" : "outV"}().as('v').select('e', 'v')`; } } else { gremlinQuery = `g.V(${pkid}).${outE ? "outE" : "inE"}().limit(${pageSize}).as('e').${ diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts index 5947c5439..86c918521 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts @@ -9,7 +9,7 @@ describe("Gremlin Client", () => { collectionId: null, databaseId: null, masterKey: null, - maxResultSize: 10000 + maxResultSize: 10000, }; it("should use databaseId, collectionId and masterKey to authenticate", () => { @@ -23,7 +23,7 @@ describe("Gremlin Client", () => { collectionId, databaseId, masterKey, - maxResultSize: 0 + maxResultSize: 0, }); // User must includes these values @@ -32,7 +32,7 @@ describe("Gremlin Client", () => { expect(gremlinClient.client.params.password).toEqual(masterKey); }); - it("should aggregate RU charges across multiple responses", done => { + it("should aggregate RU charges across multiple responses", (done) => { const gremlinClient = new GremlinClient(); const ru1 = 1; const ru2 = 2; @@ -42,23 +42,23 @@ describe("Gremlin Client", () => { sinon.stub(gremlinClient.client, "executeGremlinQuery").callsFake((query: string): string => requestId); gremlinClient .execute("fake query") - .then(result => expect(result.totalRequestCharge).toBe(ru1 + ru2 + ru3)) + .then((result) => expect(result.totalRequestCharge).toBe(ru1 + ru2 + ru3)) .finally(done); gremlinClient.client.params.progressCallback({ data: ["data1"], requestCharge: ru1, - requestId: requestId + requestId: requestId, }); gremlinClient.client.params.progressCallback({ data: ["data2"], requestCharge: ru2, - requestId: requestId + requestId: requestId, }); gremlinClient.client.params.successCallback({ data: ["data3"], requestCharge: ru3, - requestId: requestId + requestId: requestId, }); }); @@ -83,7 +83,7 @@ describe("Gremlin Client", () => { gremlinClient.client.params.successCallback({ data: ["data1"], requestCharge: ru1, - requestId: requestId + requestId: requestId, }); }, 0); return requestId; @@ -103,7 +103,7 @@ describe("Gremlin Client", () => { gremlinClient.client.params.successCallback({ data: ["data1"], requestCharge: 1, - requestId: "unknownId" + requestId: "unknownId", }); expect(logConsoleSpy.called).toBe(true); @@ -121,7 +121,7 @@ describe("Gremlin Client", () => { expect(GremlinClient.getRequestChargeString("123")).not.toEqual(emptyResult); }); - it("should not aggregate RU if not a number and reset totalRequestCharge to undefined", done => { + it("should not aggregate RU if not a number and reset totalRequestCharge to undefined", (done) => { const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError"); const logErrorSpy = sinon.spy(Logger, "logError"); @@ -135,7 +135,7 @@ describe("Gremlin Client", () => { gremlinClient .execute("fake query") .then( - result => { + (result) => { try { expect(result.totalRequestCharge).toBe(undefined); expect(logConsoleSpy.called).toBe(true); @@ -145,7 +145,7 @@ describe("Gremlin Client", () => { done(e); } }, - error => done.fail(error) + (error) => done.fail(error) ) .finally(() => { logConsoleSpy.restore(); @@ -155,16 +155,16 @@ describe("Gremlin Client", () => { gremlinClient.client.params.progressCallback({ data: ["data1"], requestCharge: ru1, - requestId: requestId + requestId: requestId, }); gremlinClient.client.params.successCallback({ data: ["data2"], requestCharge: ru2 as any, - requestId: requestId + requestId: requestId, }); }); - it("should not aggregate RU if undefined and reset totalRequestCharge to undefined", done => { + it("should not aggregate RU if undefined and reset totalRequestCharge to undefined", (done) => { const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError"); const logErrorSpy = sinon.spy(Logger, "logError"); @@ -178,7 +178,7 @@ describe("Gremlin Client", () => { gremlinClient .execute("fake query") .then( - result => { + (result) => { try { expect(result.totalRequestCharge).toBe(undefined); expect(logConsoleSpy.called).toBe(true); @@ -188,7 +188,7 @@ describe("Gremlin Client", () => { done(e); } }, - error => done.fail(error) + (error) => done.fail(error) ) .finally(() => { logConsoleSpy.restore(); @@ -198,16 +198,16 @@ describe("Gremlin Client", () => { gremlinClient.client.params.progressCallback({ data: ["data1"], requestCharge: ru1, - requestId: requestId + requestId: requestId, }); gremlinClient.client.params.successCallback({ data: ["data2"], requestCharge: ru2, - requestId: requestId + requestId: requestId, }); }); - it("should track RUs even on failure", done => { + it("should track RUs even on failure", (done) => { const gremlinClient = new GremlinClient(); const requestId = "id"; const RU = 1234; @@ -217,8 +217,8 @@ describe("Gremlin Client", () => { sinon.stub(gremlinClient.client, "executeGremlinQuery").callsFake((query: string): string => requestId); const abortPendingRequestSpy = sinon.spy(gremlinClient, "abortPendingRequest"); gremlinClient.execute("fake query").then( - result => done.fail(`Unexpectedly succeeded with ${result}`), - error => { + (result) => done.fail(`Unexpectedly succeeded with ${result}`), + (error) => { try { expect(abortPendingRequestSpy.calledWith(requestId, error, RU)).toBe(true); done(); @@ -232,13 +232,13 @@ describe("Gremlin Client", () => { { data: null, requestCharge: RU, - requestId: requestId + requestId: requestId, }, error ); }); - it("should abort all pending requests if requestId from failure response", done => { + it("should abort all pending requests if requestId from failure response", (done) => { const gremlinClient = new GremlinClient(); const requestId = "id"; const error = "Some error"; @@ -258,7 +258,7 @@ describe("Gremlin Client", () => { { data: null, requestCharge: undefined, - requestId: undefined + requestId: undefined, }, error ); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts index e66ae5cf2..6daf7ea23 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts @@ -79,7 +79,7 @@ export class GremlinClient { }, infoCallback: (msg: string) => { NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg); - } + }, }); } @@ -89,13 +89,13 @@ export class GremlinClient { this.pendingResults.set(requestId, { result: { data: [] as any[], - isIncomplete: false + isIncomplete: false, }, deferred: deferred, timeoutId: window.setTimeout( () => this.abortPendingRequest(requestId, GremlinClient.TIMEOUT_ERROR_MSG, null), GremlinClient.PENDING_REQUEST_TIMEOUT_MS - ) + ), }); return deferred.promise; } diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts index 4afec7c49..9cdee55a7 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts @@ -9,7 +9,7 @@ import { GremlinSimpleClientParameters, Result, GremlinRequestMessage, - GremlinResponseMessage + GremlinResponseMessage, } from "./GremlinSimpleClient"; describe("Gremlin Simple Client", () => { @@ -24,7 +24,7 @@ describe("Gremlin Simple Client", () => { successCallback: (result: Result) => {}, progressCallback: (result: Result) => {}, failureCallback: (result: Result, error: string) => {}, - infoCallback: (msg: string) => {} + infoCallback: (msg: string) => {}, }; }; @@ -41,10 +41,10 @@ describe("Gremlin Simple Client", () => { } => ({ attributes: { "x-ms-request-charge": requestCharge, - "x-ms-total-request-charge": -123 + "x-ms-total-request-charge": -123, }, code: code, - message: null + message: null, }); beforeEach(() => { @@ -60,7 +60,7 @@ describe("Gremlin Simple Client", () => { fakeSocket.onmessage(fakeSocket.fakeResponse); } }, - close: () => {} + close: () => {}, }; sandbox.stub(GremlinSimpleClient, "createWebSocket").returns(fakeSocket); }); @@ -89,9 +89,9 @@ describe("Gremlin Simple Client", () => { status: { code: 200, attributes: { graphExecutionStatus: 200, StorageRU: 2.29, ComputeRU: 1.07, PerPartitionComputeCharges: {} }, - message: "" + message: "", }, - result: { data: ["é"], meta: {} } + result: { data: ["é"], meta: {} }, }; const expectedDecodedUint8ArrayValues = [ 123, @@ -323,7 +323,7 @@ describe("Gremlin Simple Client", () => { 123, 125, 125, - 125 + 125, ]; // We do our best here to emulate what the server should return const gremlinResponseData = new Uint8Array(expectedDecodedUint8ArrayValues).buffer; @@ -352,7 +352,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(200, null), requestId: "id", - result: { data: "mydata" } + result: { data: "mydata" }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); const onMessageSpy = sandbox.spy(client, "onMessage"); @@ -368,7 +368,7 @@ describe("Gremlin Simple Client", () => { fakeSocket.fakeResponse = { status: fakeStatus(200, null), requestId: Object.keys(client.pendingRequests)[0], - data: new Uint8Array([1, 1, 1, 1]).buffer + data: new Uint8Array([1, 1, 1, 1]).buffer, }; client.executeGremlinQuery("test"); fakeSocket.onopen(); @@ -386,7 +386,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(200, RU), requestId: Object.keys(client.pendingRequests)[0], - result: { data: "mydata" } + result: { data: "mydata" }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(new MessageEvent("test2")); @@ -394,7 +394,7 @@ describe("Gremlin Simple Client", () => { onSuccessSpy.calledWith({ requestId: fakeResponse.requestId, data: fakeResponse.result.data, - requestCharge: RU + requestCharge: RU, }) ).toBe(true); }); @@ -410,7 +410,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(204, RU), requestId: Object.keys(client.pendingRequests)[0], - result: { data: "THIS SHOULD BE IGNORED" } + result: { data: "THIS SHOULD BE IGNORED" }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(new MessageEvent("test2")); @@ -418,7 +418,7 @@ describe("Gremlin Simple Client", () => { onSuccessSpy.calledWith({ requestId: fakeResponse.requestId, data: null, - requestCharge: RU + requestCharge: RU, }) ).toBe(true); }); @@ -435,7 +435,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(206, RU), requestId: Object.keys(client.pendingRequests)[0], - result: { data: [1, 2, 3] } + result: { data: [1, 2, 3] }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(new MessageEvent("test2")); @@ -443,7 +443,7 @@ describe("Gremlin Simple Client", () => { onProgressSpy.calledWith({ requestId: fakeResponse.requestId, data: fakeResponse.result.data, - requestCharge: RU + requestCharge: RU, }) ).toBe(true); expect(onSuccessSpy.notCalled).toBe(true); @@ -460,7 +460,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(407, null), requestId: Object.keys(client.pendingRequests)[0], - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(new MessageEvent("test2")); @@ -489,7 +489,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(401, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -501,7 +501,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(401, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -513,7 +513,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(498, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -525,7 +525,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(500, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -537,7 +537,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(597, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -549,7 +549,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(598, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -561,7 +561,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(599, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -573,7 +573,7 @@ describe("Gremlin Simple Client", () => { const fakeResponse: GremlinResponseMessage = { status: fakeStatus(123123123, null), requestId: "id", - result: { data: null } + result: { data: null }, }; sandbox.stub(client, "decodeMessage").returns(fakeResponse); client.onMessage(null); @@ -595,16 +595,16 @@ describe("Gremlin Simple Client", () => { args: { gremlin: "gremlin", bindings: {}, - language: "language" - } + language: "language", + }, }; const expectedResult: GremlinRequestMessage = { requestId: request.requestId, processor: request.processor, op: "authentication", args: { - SASL: expectedSASLResult - } + SASL: expectedSASLResult, + }, }; const actual = client.buildChallengeResponse(request); expect(actual).toEqual(expectedResult); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts index 8e5d6e4fd..1ca0365c3 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts @@ -165,7 +165,7 @@ export class GremlinSimpleClient { const result: Result = { requestId: requestId, data: rawMessage.result ? rawMessage.result.data : null, - requestCharge: rawMessage.status.attributes[GremlinSimpleClient.requestChargeHeader] + requestCharge: rawMessage.status.attributes[GremlinSimpleClient.requestChargeHeader], }; if (!this.pendingRequests[requestId]) { @@ -262,8 +262,8 @@ export class GremlinSimpleClient { args: { gremlin: query, bindings: {}, - language: "gremlin-groovy" - } + language: "gremlin-groovy", + }, }; this.connect(); return requestId; @@ -271,19 +271,19 @@ export class GremlinSimpleClient { public buildChallengeResponse(request: GremlinRequestMessage): GremlinRequestMessage { var args = { - SASL: GremlinSimpleClient.utf8ToB64("\0" + this.params.user + "\0" + this.params.password) + SASL: GremlinSimpleClient.utf8ToB64("\0" + this.params.user + "\0" + this.params.password), }; return { requestId: request.requestId, processor: request.processor, op: "authentication", - args + args, }; } public static utf8ToB64(utf8Str: string) { return btoa( - encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, function(match, p1) { + encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, function (match, p1) { return String.fromCharCode(parseInt(p1, 16)); }) ); @@ -342,7 +342,7 @@ export class GremlinSimpleClient { * RFC4122 version 4 compliant UUID */ private static uuidv4() { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8; return v.toString(16); diff --git a/src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx index 36dd33b8b..42441a9a0 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx @@ -58,7 +58,7 @@ export class LeftPaneComponent extends React.Component { className={className} as="tr" aria-label={node.caption} - onActivated={e => this.props.onRootNodeSelected(node.id)} + onActivated={(e) => this.props.onRootNodeSelected(node.id)} key={node.id} > diff --git a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx index dd3e93359..bd894129f 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx @@ -20,17 +20,17 @@ describe("Property pane", () => { name: "sourceName", id: "sourceId", edgeId: "edgeId", - edgeLabel: "sourceEdgeLabel" - } + edgeLabel: "sourceEdgeLabel", + }, ], targets: [ { name: "targetName", id: "targetId", edgeId: "edgeId", - edgeLabel: "targetEdgeLabel" - } - ] + edgeLabel: "targetEdgeLabel", + }, + ], }; const createMockProps = (): NodePropertiesComponentProps => { @@ -48,7 +48,7 @@ describe("Property pane", () => { editGraphEdges: (editedEdges: EditedEdges): Q.Promise => Q.resolve(), deleteHighlightedNode: (): void => {}, onModeChanged: (newMode: Mode): void => {}, - viewMode: Mode.READONLY_PROP + viewMode: Mode.READONLY_PROP, }; }; let wrapper: ReactWrapper; diff --git a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx index cc01e0a8a..e308d7ece 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx @@ -25,7 +25,7 @@ export enum Mode { READONLY_PROP, PROPERTY_EDITOR, EDIT_SOURCES, - EDIT_TARGETS + EDIT_TARGETS, } export interface NodePropertiesComponentProps { @@ -71,25 +71,25 @@ export class NodePropertiesComponent extends React.Component< readOnlyProperties: [], existingProperties: [], addedProperties: [], - droppedKeys: [] + droppedKeys: [], }, editedSources: { vertexId: undefined, currentNeighbors: [], droppedIds: [], - addedEdges: [] + addedEdges: [], }, editedTargets: { vertexId: undefined, currentNeighbors: [], droppedIds: [], - addedEdges: [] + addedEdges: [], }, possibleVertices: [], isDeleteConfirm: false, isPropertiesExpanded: true, isSourcesExpanded: true, - isTargetsExpanded: true + isTargetsExpanded: true, }; } @@ -162,8 +162,8 @@ export class NodePropertiesComponent extends React.Component< const readOnlyProps: ViewModels.InputProperty[] = [ { key: "label", - values: [{ value: this.props.node.label, type: "string" }] - } + values: [{ value: this.props.node.label, type: "string" }], + }, ]; const existingProps: ViewModels.InputProperty[] = []; @@ -174,7 +174,10 @@ export class NodePropertiesComponent extends React.Component< const propValues = hProps[p]; (p === partitionKeyProperty ? readOnlyProps : existingProps).push({ key: p, - values: propValues.map(val => ({ value: val.toString(), type: NodePropertiesComponent.getTypeOption(val) })) + values: propValues.map((val) => ({ + value: val.toString(), + type: NodePropertiesComponent.getTypeOption(val), + })), }); } } @@ -186,8 +189,8 @@ export class NodePropertiesComponent extends React.Component< readOnlyProperties: readOnlyProps, existingProperties: existingProps, addedProperties: [], - droppedKeys: [] - } + droppedKeys: [], + }, }); this.props.onModeChanged(newMode); } @@ -195,20 +198,20 @@ export class NodePropertiesComponent extends React.Component< private showSourcesEditor(): void { this.props.updatePossibleVertices().then((possibleVertices: PossibleVertex[]) => { this.setState({ - possibleVertices: possibleVertices + possibleVertices: possibleVertices, }); const editedSources: EditedEdges = { vertexId: this.props.node.id, currentNeighbors: this.props.node.sources.slice(), droppedIds: [], - addedEdges: [] + addedEdges: [], }; const newMode = Mode.EDIT_SOURCES; this.setState({ editedProperties: this.state.editedProperties, - editedSources: editedSources + editedSources: editedSources, }); this.props.onModeChanged(newMode); }); @@ -217,20 +220,20 @@ export class NodePropertiesComponent extends React.Component< private showTargetsEditor(): void { this.props.updatePossibleVertices().then((possibleVertices: PossibleVertex[]) => { this.setState({ - possibleVertices: possibleVertices + possibleVertices: possibleVertices, }); const editedTargets: EditedEdges = { vertexId: this.props.node.id, currentNeighbors: this.props.node.targets.slice(), droppedIds: [], - addedEdges: [] + addedEdges: [], }; const newMode = Mode.EDIT_TARGETS; this.setState({ editedProperties: this.state.editedProperties, - editedTargets: editedTargets + editedTargets: editedTargets, }); this.props.onModeChanged(newMode); }); @@ -250,7 +253,7 @@ export class NodePropertiesComponent extends React.Component< private onUpdateProperties(editedProperties: EditedProperties): void { this.setState({ - editedProperties: editedProperties + editedProperties: editedProperties, }); } @@ -264,7 +267,7 @@ export class NodePropertiesComponent extends React.Component< private setIsDeleteConfirm(state: boolean): void { this.setState({ - isDeleteConfirm: state + isDeleteConfirm: state, }); } diff --git a/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx index 0ee472f4a..f24e25cb6 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx @@ -21,7 +21,7 @@ export class QueryContainerComponent extends React.Component< public constructor(props: QueryContainerComponentProps) { super(props); this.state = { - query: this.props.initialQuery + query: this.props.initialQuery, }; } @@ -82,7 +82,7 @@ export class QueryContainerComponent extends React.Component< - -
-

* Name

- -
- -
-
-
- - - - - -
- -
- - - +
+
+
+ +
+
+ +
+ +
+ Close +
+
+ + + +
+
+ Error + + + More details + +
+
+ + + +
+
+
+ +
+
+

* Name

+ +
+
+
+
+
+ +
+
+ + +
+ +
+ +
+
diff --git a/src/Explorer/Panes/SaveQueryPane.ts b/src/Explorer/Panes/SaveQueryPane.ts index 3ddc6756d..e5ceeb138 100644 --- a/src/Explorer/Panes/SaveQueryPane.ts +++ b/src/Explorer/Panes/SaveQueryPane.ts @@ -1,165 +1,165 @@ -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import { ContextualPaneBase } from "./ContextualPaneBase"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import QueryTab from "../Tabs/QueryTab"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; - -export class SaveQueryPane extends ContextualPaneBase { - public queryName: ko.Observable; - public canSaveQueries: ko.Computed; - public setupSaveQueriesText: string = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${Constants.SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`; - - constructor(options: ViewModels.PaneOptions) { - super(options); - this.title("Save Query"); - this.queryName = ko.observable(); - this.canSaveQueries = this.container && this.container.canSaveQueries; - this.resetData(); - } - - public submit = (): void => { - this.formErrors(""); - this.formErrorsDetails(""); - if (!this.canSaveQueries()) { - this.formErrors("Cannot save query"); - this.formErrorsDetails("Failed to save query: account not set up to save queries"); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - "Failed to save query: account not setup to save queries" - ); - } - - const queryName: string = this.queryName(); - const queryTab = this.container && (this.container.tabsManager.activeTab() as QueryTab); - const query: string = queryTab && queryTab.sqlQueryEditorContent(); - if (!queryName || queryName.length === 0) { - this.formErrors("No query name specified"); - this.formErrorsDetails("No query name specified. Please specify a query name."); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - "Could not save query -- No query name specified. Please specify a query name." - ); - return; - } else if (!query || query.length === 0) { - this.formErrors("Invalid query content specified"); - this.formErrorsDetails("Invalid query content specified. Please enter query content."); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - "Could not save query -- Invalid query content specified. Please enter query content." - ); - return; - } - - const queryParam: DataModels.Query = { - id: queryName, - resourceId: this.container.queriesClient.getResourceId(), - queryName: queryName, - query: query - }; - const startKey: number = TelemetryProcessor.traceStart(Action.SaveQuery, { - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() - }); - this.isExecuting(true); - this.container.queriesClient.saveQuery(queryParam).then( - () => { - this.isExecuting(false); - queryTab.tabTitle(queryParam.queryName); - queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`); - TelemetryProcessor.traceSuccess( - Action.SaveQuery, - { - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() - }, - startKey - ); - this.close(); - }, - (error: any) => { - this.isExecuting(false); - const errorMessage = getErrorMessage(error); - this.formErrors("Failed to save query"); - this.formErrorsDetails(`Failed to save query: ${errorMessage}`); - TelemetryProcessor.traceFailure( - Action.SaveQuery, - { - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - } - ); - }; - - public setupQueries = async (src: any, event: MouseEvent): Promise => { - if (!this.container) { - return; - } - - const startKey: number = TelemetryProcessor.traceStart(Action.SetupSavedQueries, { - databaseAccountName: this.container && this.container.databaseAccount().name, - defaultExperience: this.container && this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() - }); - try { - this.isExecuting(true); - await this.container.queriesClient.setupQueriesCollection(); - this.container.refreshAllDatabases(); - TelemetryProcessor.traceSuccess( - Action.SetupSavedQueries, - { - databaseAccountName: this.container && this.container.databaseAccount().name, - defaultExperience: this.container && this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() - }, - startKey - ); - } catch (error) { - const errorMessage = getErrorMessage(error); - TelemetryProcessor.traceFailure( - Action.SetupSavedQueries, - { - databaseAccountName: this.container && this.container.databaseAccount().name, - defaultExperience: this.container && this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - this.formErrors("Failed to setup a container for saved queries"); - this.formErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`); - } finally { - this.isExecuting(false); - } - }; - - public close() { - super.close(); - this.resetData(); - } - - public resetData() { - super.resetData(); - this.queryName(""); - } -} +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import { ContextualPaneBase } from "./ContextualPaneBase"; +import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import QueryTab from "../Tabs/QueryTab"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; + +export class SaveQueryPane extends ContextualPaneBase { + public queryName: ko.Observable; + public canSaveQueries: ko.Computed; + public setupSaveQueriesText: string = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${Constants.SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`; + + constructor(options: ViewModels.PaneOptions) { + super(options); + this.title("Save Query"); + this.queryName = ko.observable(); + this.canSaveQueries = this.container && this.container.canSaveQueries; + this.resetData(); + } + + public submit = (): void => { + this.formErrors(""); + this.formErrorsDetails(""); + if (!this.canSaveQueries()) { + this.formErrors("Cannot save query"); + this.formErrorsDetails("Failed to save query: account not set up to save queries"); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + "Failed to save query: account not setup to save queries" + ); + } + + const queryName: string = this.queryName(); + const queryTab = this.container && (this.container.tabsManager.activeTab() as QueryTab); + const query: string = queryTab && queryTab.sqlQueryEditorContent(); + if (!queryName || queryName.length === 0) { + this.formErrors("No query name specified"); + this.formErrorsDetails("No query name specified. Please specify a query name."); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + "Could not save query -- No query name specified. Please specify a query name." + ); + return; + } else if (!query || query.length === 0) { + this.formErrors("Invalid query content specified"); + this.formErrorsDetails("Invalid query content specified. Please enter query content."); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + "Could not save query -- Invalid query content specified. Please enter query content." + ); + return; + } + + const queryParam: DataModels.Query = { + id: queryName, + resourceId: this.container.queriesClient.getResourceId(), + queryName: queryName, + query: query, + }; + const startKey: number = TelemetryProcessor.traceStart(Action.SaveQuery, { + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: this.title(), + }); + this.isExecuting(true); + this.container.queriesClient.saveQuery(queryParam).then( + () => { + this.isExecuting(false); + queryTab.tabTitle(queryParam.queryName); + queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`); + TelemetryProcessor.traceSuccess( + Action.SaveQuery, + { + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: this.title(), + }, + startKey + ); + this.close(); + }, + (error: any) => { + this.isExecuting(false); + const errorMessage = getErrorMessage(error); + this.formErrors("Failed to save query"); + this.formErrorsDetails(`Failed to save query: ${errorMessage}`); + TelemetryProcessor.traceFailure( + Action.SaveQuery, + { + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: this.title(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + } + ); + }; + + public setupQueries = async (src: any, event: MouseEvent): Promise => { + if (!this.container) { + return; + } + + const startKey: number = TelemetryProcessor.traceStart(Action.SetupSavedQueries, { + databaseAccountName: this.container && this.container.databaseAccount().name, + defaultExperience: this.container && this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: this.title(), + }); + try { + this.isExecuting(true); + await this.container.queriesClient.setupQueriesCollection(); + this.container.refreshAllDatabases(); + TelemetryProcessor.traceSuccess( + Action.SetupSavedQueries, + { + databaseAccountName: this.container && this.container.databaseAccount().name, + defaultExperience: this.container && this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: this.title(), + }, + startKey + ); + } catch (error) { + const errorMessage = getErrorMessage(error); + TelemetryProcessor.traceFailure( + Action.SetupSavedQueries, + { + databaseAccountName: this.container && this.container.databaseAccount().name, + defaultExperience: this.container && this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: this.title(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + this.formErrors("Failed to setup a container for saved queries"); + this.formErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`); + } finally { + this.isExecuting(false); + } + }; + + public close() { + super.close(); + this.resetData(); + } + + public resetData() { + super.resetData(); + this.queryName(""); + } +} diff --git a/src/Explorer/Panes/SettingsPane.html b/src/Explorer/Panes/SettingsPane.html index 6d022f73b..e2a94a725 100644 --- a/src/Explorer/Panes/SettingsPane.html +++ b/src/Explorer/Panes/SettingsPane.html @@ -33,7 +33,7 @@ data-bind="visible: formErrors() && formErrors() !== ''" >
- Error + Error { - describe("shouldShowQueryPageOptions()", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be true for SQL API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase()); - expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(true); - }); - - it("should be false for Cassandra API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.Cassandra.toLowerCase()); - expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); - }); - - it("should be false for Tables API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.Table.toLowerCase()); - expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); - }); - - it("should be false for Graph API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase()); - expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); - }); - - it("should be false for Mongo API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB.toLowerCase()); - expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); - }); - }); -}); +import * as Constants from "../../Common/Constants"; +import * as ViewModels from "../../Contracts/ViewModels"; +import Explorer from "../Explorer"; + +describe("Settings Pane", () => { + describe("shouldShowQueryPageOptions()", () => { + let explorer: Explorer; + + beforeEach(() => { + explorer = new Explorer(); + }); + + it("should be true for SQL API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase()); + expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(true); + }); + + it("should be false for Cassandra API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.Cassandra.toLowerCase()); + expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); + }); + + it("should be false for Tables API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.Table.toLowerCase()); + expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); + }); + + it("should be false for Graph API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase()); + expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); + }); + + it("should be false for Mongo API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB.toLowerCase()); + expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false); + }); + }); +}); diff --git a/src/Explorer/Panes/SetupNotebooksPane.html b/src/Explorer/Panes/SetupNotebooksPane.html index a7a955bad..20fbc53b2 100644 --- a/src/Explorer/Panes/SetupNotebooksPane.html +++ b/src/Explorer/Panes/SetupNotebooksPane.html @@ -1,45 +1,45 @@ -
-
-
- -
-
- -
- -
- Close -
-
- - -
-
-
- -
-
-
-
- - -
- loading indicator image -
- -
-
+
+
+
+ +
+
+ +
+ +
+ Close +
+
+ + +
+
+
+ +
+
+
+
+ + +
+ loading indicator image +
+ +
+
diff --git a/src/Explorer/Panes/SetupNotebooksPane.ts b/src/Explorer/Panes/SetupNotebooksPane.ts index 8f00572ea..1ee8b7d1d 100644 --- a/src/Explorer/Panes/SetupNotebooksPane.ts +++ b/src/Explorer/Panes/SetupNotebooksPane.ts @@ -1,113 +1,113 @@ -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import { Areas, KeyCodes } from "../../Common/Constants"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import { ContextualPaneBase } from "./ContextualPaneBase"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import * as ko from "knockout"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; - -export class SetupNotebooksPane extends ContextualPaneBase { - private description: ko.Observable; - - constructor(options: ViewModels.PaneOptions) { - super(options); - - this.description = ko.observable(); - this.resetData(); - } - - public openWithTitleAndDescription(title: string, description: string) { - this.title(title); - this.description(description); - - this.open(); - } - - public open() { - super.open(); - const completeSetupBtn = document.getElementById("completeSetupBtn"); - completeSetupBtn && completeSetupBtn.focus(); - } - - public submit() { - // override default behavior because this is not a form - } - - public onCompleteSetupClick = async (src: any, event: MouseEvent) => { - await this.setupNotebookWorkspace(); - }; - - public onCompleteSetupKeyPress = async (src: any, event: KeyboardEvent) => { - if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) { - await this.setupNotebookWorkspace(); - event.stopPropagation(); - return false; - } - return true; - }; - - public async setupNotebookWorkspace(): Promise { - if (!this.container) { - return; - } - - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, { - databaseAccountName: this.container && this.container.databaseAccount().name, - defaultExperience: this.container && this.container.defaultExperience(), - dataExplorerArea: Areas.ContextualPane, - paneTitle: this.title() - }); - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Creating a new default notebook workspace" - ); - try { - this.isExecuting(true); - await this.container.notebookWorkspaceManager.createNotebookWorkspaceAsync( - this.container.databaseAccount() && this.container.databaseAccount().id, - "default" - ); - this.container.isAccountReady.valueHasMutated(); // re-trigger init notebooks - this.close(); - TelemetryProcessor.traceSuccess( - Action.CreateNotebookWorkspace, - { - databaseAccountName: this.container && this.container.databaseAccount().name, - defaultExperience: this.container && this.container.defaultExperience(), - dataExplorerArea: Areas.ContextualPane, - paneTitle: this.title() - }, - startKey - ); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - "Successfully created a default notebook workspace for the account" - ); - } catch (error) { - const errorMessage = getErrorMessage(error); - TelemetryProcessor.traceFailure( - Action.CreateNotebookWorkspace, - { - databaseAccountName: this.container && this.container.databaseAccount().name, - defaultExperience: this.container && this.container.defaultExperience(), - dataExplorerArea: Areas.ContextualPane, - paneTitle: this.title(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - this.formErrors("Failed to setup a default notebook workspace"); - this.formErrorsDetails(`Failed to setup a default notebook workspace: ${errorMessage}`); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to create a default notebook workspace: ${errorMessage}` - ); - } finally { - this.isExecuting(false); - NotificationConsoleUtils.clearInProgressMessageWithId(id); - } - } -} +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import { Areas, KeyCodes } from "../../Common/Constants"; +import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; +import { ContextualPaneBase } from "./ContextualPaneBase"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import * as ko from "knockout"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; + +export class SetupNotebooksPane extends ContextualPaneBase { + private description: ko.Observable; + + constructor(options: ViewModels.PaneOptions) { + super(options); + + this.description = ko.observable(); + this.resetData(); + } + + public openWithTitleAndDescription(title: string, description: string) { + this.title(title); + this.description(description); + + this.open(); + } + + public open() { + super.open(); + const completeSetupBtn = document.getElementById("completeSetupBtn"); + completeSetupBtn && completeSetupBtn.focus(); + } + + public submit() { + // override default behavior because this is not a form + } + + public onCompleteSetupClick = async (src: any, event: MouseEvent) => { + await this.setupNotebookWorkspace(); + }; + + public onCompleteSetupKeyPress = async (src: any, event: KeyboardEvent) => { + if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) { + await this.setupNotebookWorkspace(); + event.stopPropagation(); + return false; + } + return true; + }; + + public async setupNotebookWorkspace(): Promise { + if (!this.container) { + return; + } + + const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, { + databaseAccountName: this.container && this.container.databaseAccount().name, + defaultExperience: this.container && this.container.defaultExperience(), + dataExplorerArea: Areas.ContextualPane, + paneTitle: this.title(), + }); + const id = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + "Creating a new default notebook workspace" + ); + try { + this.isExecuting(true); + await this.container.notebookWorkspaceManager.createNotebookWorkspaceAsync( + this.container.databaseAccount() && this.container.databaseAccount().id, + "default" + ); + this.container.isAccountReady.valueHasMutated(); // re-trigger init notebooks + this.close(); + TelemetryProcessor.traceSuccess( + Action.CreateNotebookWorkspace, + { + databaseAccountName: this.container && this.container.databaseAccount().name, + defaultExperience: this.container && this.container.defaultExperience(), + dataExplorerArea: Areas.ContextualPane, + paneTitle: this.title(), + }, + startKey + ); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + "Successfully created a default notebook workspace for the account" + ); + } catch (error) { + const errorMessage = getErrorMessage(error); + TelemetryProcessor.traceFailure( + Action.CreateNotebookWorkspace, + { + databaseAccountName: this.container && this.container.databaseAccount().name, + defaultExperience: this.container && this.container.defaultExperience(), + dataExplorerArea: Areas.ContextualPane, + paneTitle: this.title(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + this.formErrors("Failed to setup a default notebook workspace"); + this.formErrorsDetails(`Failed to setup a default notebook workspace: ${errorMessage}`); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to create a default notebook workspace: ${errorMessage}` + ); + } finally { + this.isExecuting(false); + NotificationConsoleUtils.clearInProgressMessageWithId(id); + } + } +} diff --git a/src/Explorer/Panes/StringInputPane.html b/src/Explorer/Panes/StringInputPane.html index 5335d84fe..313a4a870 100644 --- a/src/Explorer/Panes/StringInputPane.html +++ b/src/Explorer/Panes/StringInputPane.html @@ -18,7 +18,7 @@ data-bind="visible: formErrors() && formErrors() !== ''" >
- Error + Error { + (reason) => { let error = reason; if (reason instanceof Error) { error = reason.message; diff --git a/src/Explorer/Panes/SwitchDirectoryPane.ts b/src/Explorer/Panes/SwitchDirectoryPane.ts index 661bb6b8b..198e98bad 100644 --- a/src/Explorer/Panes/SwitchDirectoryPane.ts +++ b/src/Explorer/Panes/SwitchDirectoryPane.ts @@ -17,7 +17,7 @@ export class SwitchDirectoryPaneComponent { constructor() { return { viewModel: PaneComponent, - template: SwitchDirectoryPaneTemplate + template: SwitchDirectoryPaneTemplate, }; } } @@ -47,7 +47,7 @@ export class SwitchDirectoryPane { this.firstFieldHasFocus(true); this.resizePane(); TelemetryProcessor.trace(Action.ContextualPane, ActionModifiers.Open, { - paneTitle: this.title() + paneTitle: this.title(), }); this.directoryComponentAdapter.forceRender(); diff --git a/src/Explorer/Panes/Tables/AddTableEntityPane.ts b/src/Explorer/Panes/Tables/AddTableEntityPane.ts index 11561b1c1..57e9f5b69 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPane.ts +++ b/src/Explorer/Panes/Tables/AddTableEntityPane.ts @@ -1,151 +1,151 @@ -import * as ko from "knockout"; -import * as _ from "underscore"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient"; -import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; -import * as Entities from "../../Tables/Entities"; -import * as TableConstants from "../../Tables/Constants"; -import * as Utilities from "../../Tables/Utilities"; -import EntityPropertyViewModel from "./EntityPropertyViewModel"; -import TableEntityPane from "./TableEntityPane"; - -export default class AddTableEntityPane extends TableEntityPane { - private static _excludedFields: string[] = [TableConstants.EntityKeyNames.Timestamp]; - - private static _readonlyFields: string[] = [ - TableConstants.EntityKeyNames.PartitionKey, - TableConstants.EntityKeyNames.RowKey, - TableConstants.EntityKeyNames.Timestamp - ]; - - public enterRequiredValueLabel = "Enter identifier value."; // localize - public enterValueLabel = "Enter value to keep property."; // localize - - constructor(options: ViewModels.PaneOptions) { - super(options); - this.submitButtonText("Add Entity"); - this.container.isPreferredApiCassandra.subscribe(isCassandra => { - if (isCassandra) { - this.submitButtonText("Add Row"); - } - }); - this.scrollId = ko.observable("addEntityScroll"); - } - - public submit() { - if (!this.canApply()) { - return; - } - let entity: Entities.ITableEntity = this.entityFromAttributes(this.displayedAttributes()); - this.container.tableDataClient - .createDocument(this.tableViewModel.queryTablesTab.collection, entity) - .then((newEntity: Entities.ITableEntity) => { - this.tableViewModel.addEntityToCache(newEntity).then(() => { - if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) { - this.tableViewModel.redrawTableThrottled(); - } - }); - this.close(); - }); - } - - public open() { - var headers = this.tableViewModel.headers; - if (DataTableUtilities.checkForDefaultHeader(headers)) { - headers = []; - if (this.container.isPreferredApiTable()) { - headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey]; - } - } - if (this.container.isPreferredApiCassandra()) { - (this.container.tableDataClient) - .getTableSchema(this.tableViewModel.queryTablesTab.collection) - .then((columns: CassandraTableKey[]) => { - this.displayedAttributes( - this.constructDisplayedAttributes( - columns.map(col => col.property), - Utilities.getDataTypesFromCassandraSchema(columns) - ) - ); - this.updateIsActionEnabled(); - super.open(); - this.focusValueElement(); - }); - } else { - this.displayedAttributes( - this.constructDisplayedAttributes( - headers, - Utilities.getDataTypesFromEntities(headers, this.tableViewModel.items()) - ) - ); - this.updateIsActionEnabled(); - super.open(); - this.focusValueElement(); - } - } - - private focusValueElement() { - const focusElement = document.getElementById("addTableEntityValue"); - focusElement && focusElement.focus(); - } - - private constructDisplayedAttributes(headers: string[], dataTypes: any): EntityPropertyViewModel[] { - var displayedAttributes: EntityPropertyViewModel[] = []; - headers && - headers.forEach((key: string) => { - if (!_.contains(AddTableEntityPane._excludedFields, key)) { - if (this.container.isPreferredApiCassandra()) { - const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys - .concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) - .map(key => key.property); - var isRequired: boolean = _.contains(cassandraKeys, key); - var editable: boolean = false; - var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel; - var entityAttributeType: string = dataTypes[key] || TableConstants.CassandraType.Text; // Default to String if there is no type specified. - // TODO figure out validation story for blob and Inet so we can allow adding/editing them - const nonEditableType: boolean = - entityAttributeType === TableConstants.CassandraType.Blob || - entityAttributeType === TableConstants.CassandraType.Inet; - var entity: EntityPropertyViewModel = new EntityPropertyViewModel( - this, - key, - entityAttributeType, - "", // default to empty string - /* namePlaceholder */ undefined, - nonEditableType ? "Type is not editable via DataExplorer." : placeholderLabel, - editable, - /* default valid name */ true, - /* default valid value */ true, - /* required value */ isRequired, - /* removable */ false, - /* valueEditable */ !nonEditableType, - /* ignoreEmptyValue */ true - ); - } else { - var isRequired: boolean = _.contains(AddTableEntityPane.requiredFieldsForTablesAPI, key); - var editable: boolean = !_.contains(AddTableEntityPane._readonlyFields, key); - var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel; - var entityAttributeType: string = dataTypes[key] || TableConstants.TableType.String; // Default to String if there is no type specified. - var entity: EntityPropertyViewModel = new EntityPropertyViewModel( - this, - key, - entityAttributeType, - "", // default to empty string - /* namePlaceholder */ undefined, - placeholderLabel, - editable, - /* default valid name */ true, - /* default valid value */ true, - /* required value */ isRequired, - /* removable */ editable, - /* valueEditable */ true, - /* ignoreEmptyValue */ true - ); - } - displayedAttributes.push(entity); - } - }); - - return displayedAttributes; - } -} +import * as ko from "knockout"; +import * as _ from "underscore"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient"; +import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; +import * as Entities from "../../Tables/Entities"; +import * as TableConstants from "../../Tables/Constants"; +import * as Utilities from "../../Tables/Utilities"; +import EntityPropertyViewModel from "./EntityPropertyViewModel"; +import TableEntityPane from "./TableEntityPane"; + +export default class AddTableEntityPane extends TableEntityPane { + private static _excludedFields: string[] = [TableConstants.EntityKeyNames.Timestamp]; + + private static _readonlyFields: string[] = [ + TableConstants.EntityKeyNames.PartitionKey, + TableConstants.EntityKeyNames.RowKey, + TableConstants.EntityKeyNames.Timestamp, + ]; + + public enterRequiredValueLabel = "Enter identifier value."; // localize + public enterValueLabel = "Enter value to keep property."; // localize + + constructor(options: ViewModels.PaneOptions) { + super(options); + this.submitButtonText("Add Entity"); + this.container.isPreferredApiCassandra.subscribe((isCassandra) => { + if (isCassandra) { + this.submitButtonText("Add Row"); + } + }); + this.scrollId = ko.observable("addEntityScroll"); + } + + public submit() { + if (!this.canApply()) { + return; + } + let entity: Entities.ITableEntity = this.entityFromAttributes(this.displayedAttributes()); + this.container.tableDataClient + .createDocument(this.tableViewModel.queryTablesTab.collection, entity) + .then((newEntity: Entities.ITableEntity) => { + this.tableViewModel.addEntityToCache(newEntity).then(() => { + if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) { + this.tableViewModel.redrawTableThrottled(); + } + }); + this.close(); + }); + } + + public open() { + var headers = this.tableViewModel.headers; + if (DataTableUtilities.checkForDefaultHeader(headers)) { + headers = []; + if (this.container.isPreferredApiTable()) { + headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey]; + } + } + if (this.container.isPreferredApiCassandra()) { + (this.container.tableDataClient) + .getTableSchema(this.tableViewModel.queryTablesTab.collection) + .then((columns: CassandraTableKey[]) => { + this.displayedAttributes( + this.constructDisplayedAttributes( + columns.map((col) => col.property), + Utilities.getDataTypesFromCassandraSchema(columns) + ) + ); + this.updateIsActionEnabled(); + super.open(); + this.focusValueElement(); + }); + } else { + this.displayedAttributes( + this.constructDisplayedAttributes( + headers, + Utilities.getDataTypesFromEntities(headers, this.tableViewModel.items()) + ) + ); + this.updateIsActionEnabled(); + super.open(); + this.focusValueElement(); + } + } + + private focusValueElement() { + const focusElement = document.getElementById("addTableEntityValue"); + focusElement && focusElement.focus(); + } + + private constructDisplayedAttributes(headers: string[], dataTypes: any): EntityPropertyViewModel[] { + var displayedAttributes: EntityPropertyViewModel[] = []; + headers && + headers.forEach((key: string) => { + if (!_.contains(AddTableEntityPane._excludedFields, key)) { + if (this.container.isPreferredApiCassandra()) { + const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys + .concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) + .map((key) => key.property); + var isRequired: boolean = _.contains(cassandraKeys, key); + var editable: boolean = false; + var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel; + var entityAttributeType: string = dataTypes[key] || TableConstants.CassandraType.Text; // Default to String if there is no type specified. + // TODO figure out validation story for blob and Inet so we can allow adding/editing them + const nonEditableType: boolean = + entityAttributeType === TableConstants.CassandraType.Blob || + entityAttributeType === TableConstants.CassandraType.Inet; + var entity: EntityPropertyViewModel = new EntityPropertyViewModel( + this, + key, + entityAttributeType, + "", // default to empty string + /* namePlaceholder */ undefined, + nonEditableType ? "Type is not editable via DataExplorer." : placeholderLabel, + editable, + /* default valid name */ true, + /* default valid value */ true, + /* required value */ isRequired, + /* removable */ false, + /* valueEditable */ !nonEditableType, + /* ignoreEmptyValue */ true + ); + } else { + var isRequired: boolean = _.contains(AddTableEntityPane.requiredFieldsForTablesAPI, key); + var editable: boolean = !_.contains(AddTableEntityPane._readonlyFields, key); + var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel; + var entityAttributeType: string = dataTypes[key] || TableConstants.TableType.String; // Default to String if there is no type specified. + var entity: EntityPropertyViewModel = new EntityPropertyViewModel( + this, + key, + entityAttributeType, + "", // default to empty string + /* namePlaceholder */ undefined, + placeholderLabel, + editable, + /* default valid name */ true, + /* default valid value */ true, + /* required value */ isRequired, + /* removable */ editable, + /* valueEditable */ true, + /* ignoreEmptyValue */ true + ); + } + displayedAttributes.push(entity); + } + }); + + return displayedAttributes; + } +} diff --git a/src/Explorer/Panes/Tables/EditTableEntityPane.ts b/src/Explorer/Panes/Tables/EditTableEntityPane.ts index 27ed7529c..a58b0864a 100644 --- a/src/Explorer/Panes/Tables/EditTableEntityPane.ts +++ b/src/Explorer/Panes/Tables/EditTableEntityPane.ts @@ -1,229 +1,229 @@ -import * as ko from "knockout"; -import _ from "underscore"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient"; -import * as Entities from "../../Tables/Entities"; -import TableEntityPane from "./TableEntityPane"; -import * as Utilities from "../../Tables/Utilities"; -import * as TableConstants from "../../Tables/Constants"; -import EntityPropertyViewModel from "./EntityPropertyViewModel"; -import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; -import Explorer from "../../Explorer"; - -export default class EditTableEntityPane extends TableEntityPane { - container: Explorer; - visible: ko.Observable; - - public originEntity: Entities.ITableEntity; - public originalNumberOfProperties: number; - private originalDocument: any; - - constructor(options: ViewModels.PaneOptions) { - super(options); - this.submitButtonText("Update Entity"); - this.container.isPreferredApiCassandra.subscribe(isCassandra => { - if (isCassandra) { - this.submitButtonText("Update Row"); - } - }); - this.scrollId = ko.observable("editEntityScroll"); - } - - public submit() { - if (!this.canApply()) { - return; - } - let entity: Entities.ITableEntity = this.updateEntity(this.displayedAttributes()); - this.container.tableDataClient - .updateDocument(this.tableViewModel.queryTablesTab.collection, this.originalDocument, entity) - .then((newEntity: Entities.ITableEntity) => { - var numberOfProperties = 0; - for (var property in newEntity) { - if ( - property !== TableEntityProcessor.keyProperties.attachments && - property !== TableEntityProcessor.keyProperties.etag && - property !== TableEntityProcessor.keyProperties.resourceId && - property !== TableEntityProcessor.keyProperties.self && - (!this.container.isPreferredApiCassandra() || property !== TableConstants.EntityKeyNames.RowKey) - ) { - numberOfProperties++; - } - } - - var propertiesDelta = numberOfProperties - this.originalNumberOfProperties; - - return this.tableViewModel - .updateCachedEntity(newEntity) - .then(() => { - if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) { - this.tableViewModel.redrawTableThrottled(); - } - }) - .then(() => { - // Selecting updated entity - this.tableViewModel.selected.removeAll(); - this.tableViewModel.selected.push(newEntity); - }); - }); - this.close(); - } - - public open() { - this.displayedAttributes(this.constructDisplayedAttributes(this.originEntity)); - if (this.container.isPreferredApiTable()) { - this.originalDocument = TableEntityProcessor.convertEntitiesToDocuments( - [this.originEntity], - this.tableViewModel.queryTablesTab.collection - )[0]; // TODO change for Cassandra - this.originalDocument.id = ko.observable(this.originalDocument.id); - } else { - this.originalDocument = this.originEntity; - } - this.updateIsActionEnabled(); - super.open(); - } - - private constructDisplayedAttributes(entity: Entities.ITableEntity): EntityPropertyViewModel[] { - var displayedAttributes: EntityPropertyViewModel[] = []; - const keys = Object.keys(entity); - keys && - keys.forEach((key: string) => { - if ( - key !== TableEntityProcessor.keyProperties.attachments && - key !== TableEntityProcessor.keyProperties.etag && - key !== TableEntityProcessor.keyProperties.resourceId && - key !== TableEntityProcessor.keyProperties.self && - (!this.container.isPreferredApiCassandra() || key !== TableConstants.EntityKeyNames.RowKey) - ) { - if (this.container.isPreferredApiCassandra()) { - const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys - .concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) - .map(key => key.property); - var entityAttribute: Entities.ITableEntityAttribute = entity[key]; - var entityAttributeType: string = entityAttribute.$; - var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType); - var removable: boolean = false; - // TODO figure out validation story for blob and Inet so we can allow adding/editing them - const nonEditableType: boolean = - entityAttributeType === TableConstants.CassandraType.Blob || - entityAttributeType === TableConstants.CassandraType.Inet; - - displayedAttributes.push( - new EntityPropertyViewModel( - this, - key, - entityAttributeType, - displayValue, - /* namePlaceholder */ undefined, - /* valuePlaceholder */ undefined, - false, - /* default valid name */ true, - /* default valid value */ true, - /* isRequired */ false, - removable, - /*value editable*/ !_.contains(cassandraKeys, key) && !nonEditableType - ) - ); - } else { - var entityAttribute: Entities.ITableEntityAttribute = entity[key]; - var entityAttributeType: string = entityAttribute.$; - var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType); - var editable: boolean = this.isAttributeEditable(key, entityAttributeType); - // As per VSO:189935, Binary properties are read-only, we still want to be able to remove them. - var removable: boolean = editable || entityAttributeType === TableConstants.TableType.Binary; - - displayedAttributes.push( - new EntityPropertyViewModel( - this, - key, - entityAttributeType, - displayValue, - /* namePlaceholder */ undefined, - /* valuePlaceholder */ undefined, - editable, - /* default valid name */ true, - /* default valid value */ true, - /* isRequired */ false, - removable - ) - ); - } - } - }); - if (this.container.isPreferredApiCassandra()) { - (this.container.tableDataClient) - .getTableSchema(this.tableViewModel.queryTablesTab.collection) - .then((properties: CassandraTableKey[]) => { - properties && - properties.forEach(property => { - if (!_.contains(keys, property.property)) { - this.insertAttribute(property.property, property.type); - } - }); - }); - } - return displayedAttributes; - } - - private updateEntity(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity { - var updatedEntity: any = {}; - displayedAttributes && - displayedAttributes.forEach((attribute: EntityPropertyViewModel) => { - if ( - attribute.name() && - (!this.tableViewModel.queryTablesTab.container.isPreferredApiCassandra() || attribute.value() !== "") - ) { - var value = attribute.getPropertyValue(); - var type = attribute.type(); - if (type === TableConstants.TableType.Int64) { - value = Utilities.padLongWithZeros(value); - } - updatedEntity[attribute.name()] = { - _: value, - $: type - }; - } - }); - return updatedEntity; - } - - private isAttributeEditable(attributeName: string, entityAttributeType: string) { - return !( - attributeName === TableConstants.EntityKeyNames.PartitionKey || - attributeName === TableConstants.EntityKeyNames.RowKey || - attributeName === TableConstants.EntityKeyNames.Timestamp || - // As per VSO:189935, Making Binary properties read-only in Edit Entity dialog until we have a full story for it. - entityAttributeType === TableConstants.TableType.Binary - ); - } - - private getPropertyDisplayValue(entity: Entities.ITableEntity, name: string, type: string): any { - var attribute: Entities.ITableEntityAttribute = entity[name]; - var displayValue: any = attribute._; - var isBinary: boolean = type === TableConstants.TableType.Binary; - - // Showing the value in base64 for binary properties since that is what the Azure Storage Client Library expects. - // This means that, even if the Azure Storage API returns a byte[] of binary content, it needs that same array - // *base64 - encoded * as the value for the updated property or the whole update operation will fail. - if (isBinary && displayValue && $.isArray(displayValue.data)) { - var bytes: number[] = displayValue.data; - displayValue = this.getBase64DisplayValue(bytes); - } - - return displayValue; - } - - private getBase64DisplayValue(bytes: number[]): string { - var displayValue: string = null; - - try { - var chars: string[] = bytes.map((byte: number) => String.fromCharCode(byte)); - var toEncode: string = chars.join(""); - displayValue = window.btoa(toEncode); - } catch (error) { - // Error - } - - return displayValue; - } -} +import * as ko from "knockout"; +import _ from "underscore"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient"; +import * as Entities from "../../Tables/Entities"; +import TableEntityPane from "./TableEntityPane"; +import * as Utilities from "../../Tables/Utilities"; +import * as TableConstants from "../../Tables/Constants"; +import EntityPropertyViewModel from "./EntityPropertyViewModel"; +import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; +import Explorer from "../../Explorer"; + +export default class EditTableEntityPane extends TableEntityPane { + container: Explorer; + visible: ko.Observable; + + public originEntity: Entities.ITableEntity; + public originalNumberOfProperties: number; + private originalDocument: any; + + constructor(options: ViewModels.PaneOptions) { + super(options); + this.submitButtonText("Update Entity"); + this.container.isPreferredApiCassandra.subscribe((isCassandra) => { + if (isCassandra) { + this.submitButtonText("Update Row"); + } + }); + this.scrollId = ko.observable("editEntityScroll"); + } + + public submit() { + if (!this.canApply()) { + return; + } + let entity: Entities.ITableEntity = this.updateEntity(this.displayedAttributes()); + this.container.tableDataClient + .updateDocument(this.tableViewModel.queryTablesTab.collection, this.originalDocument, entity) + .then((newEntity: Entities.ITableEntity) => { + var numberOfProperties = 0; + for (var property in newEntity) { + if ( + property !== TableEntityProcessor.keyProperties.attachments && + property !== TableEntityProcessor.keyProperties.etag && + property !== TableEntityProcessor.keyProperties.resourceId && + property !== TableEntityProcessor.keyProperties.self && + (!this.container.isPreferredApiCassandra() || property !== TableConstants.EntityKeyNames.RowKey) + ) { + numberOfProperties++; + } + } + + var propertiesDelta = numberOfProperties - this.originalNumberOfProperties; + + return this.tableViewModel + .updateCachedEntity(newEntity) + .then(() => { + if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) { + this.tableViewModel.redrawTableThrottled(); + } + }) + .then(() => { + // Selecting updated entity + this.tableViewModel.selected.removeAll(); + this.tableViewModel.selected.push(newEntity); + }); + }); + this.close(); + } + + public open() { + this.displayedAttributes(this.constructDisplayedAttributes(this.originEntity)); + if (this.container.isPreferredApiTable()) { + this.originalDocument = TableEntityProcessor.convertEntitiesToDocuments( + [this.originEntity], + this.tableViewModel.queryTablesTab.collection + )[0]; // TODO change for Cassandra + this.originalDocument.id = ko.observable(this.originalDocument.id); + } else { + this.originalDocument = this.originEntity; + } + this.updateIsActionEnabled(); + super.open(); + } + + private constructDisplayedAttributes(entity: Entities.ITableEntity): EntityPropertyViewModel[] { + var displayedAttributes: EntityPropertyViewModel[] = []; + const keys = Object.keys(entity); + keys && + keys.forEach((key: string) => { + if ( + key !== TableEntityProcessor.keyProperties.attachments && + key !== TableEntityProcessor.keyProperties.etag && + key !== TableEntityProcessor.keyProperties.resourceId && + key !== TableEntityProcessor.keyProperties.self && + (!this.container.isPreferredApiCassandra() || key !== TableConstants.EntityKeyNames.RowKey) + ) { + if (this.container.isPreferredApiCassandra()) { + const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys + .concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) + .map((key) => key.property); + var entityAttribute: Entities.ITableEntityAttribute = entity[key]; + var entityAttributeType: string = entityAttribute.$; + var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType); + var removable: boolean = false; + // TODO figure out validation story for blob and Inet so we can allow adding/editing them + const nonEditableType: boolean = + entityAttributeType === TableConstants.CassandraType.Blob || + entityAttributeType === TableConstants.CassandraType.Inet; + + displayedAttributes.push( + new EntityPropertyViewModel( + this, + key, + entityAttributeType, + displayValue, + /* namePlaceholder */ undefined, + /* valuePlaceholder */ undefined, + false, + /* default valid name */ true, + /* default valid value */ true, + /* isRequired */ false, + removable, + /*value editable*/ !_.contains(cassandraKeys, key) && !nonEditableType + ) + ); + } else { + var entityAttribute: Entities.ITableEntityAttribute = entity[key]; + var entityAttributeType: string = entityAttribute.$; + var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType); + var editable: boolean = this.isAttributeEditable(key, entityAttributeType); + // As per VSO:189935, Binary properties are read-only, we still want to be able to remove them. + var removable: boolean = editable || entityAttributeType === TableConstants.TableType.Binary; + + displayedAttributes.push( + new EntityPropertyViewModel( + this, + key, + entityAttributeType, + displayValue, + /* namePlaceholder */ undefined, + /* valuePlaceholder */ undefined, + editable, + /* default valid name */ true, + /* default valid value */ true, + /* isRequired */ false, + removable + ) + ); + } + } + }); + if (this.container.isPreferredApiCassandra()) { + (this.container.tableDataClient) + .getTableSchema(this.tableViewModel.queryTablesTab.collection) + .then((properties: CassandraTableKey[]) => { + properties && + properties.forEach((property) => { + if (!_.contains(keys, property.property)) { + this.insertAttribute(property.property, property.type); + } + }); + }); + } + return displayedAttributes; + } + + private updateEntity(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity { + var updatedEntity: any = {}; + displayedAttributes && + displayedAttributes.forEach((attribute: EntityPropertyViewModel) => { + if ( + attribute.name() && + (!this.tableViewModel.queryTablesTab.container.isPreferredApiCassandra() || attribute.value() !== "") + ) { + var value = attribute.getPropertyValue(); + var type = attribute.type(); + if (type === TableConstants.TableType.Int64) { + value = Utilities.padLongWithZeros(value); + } + updatedEntity[attribute.name()] = { + _: value, + $: type, + }; + } + }); + return updatedEntity; + } + + private isAttributeEditable(attributeName: string, entityAttributeType: string) { + return !( + attributeName === TableConstants.EntityKeyNames.PartitionKey || + attributeName === TableConstants.EntityKeyNames.RowKey || + attributeName === TableConstants.EntityKeyNames.Timestamp || + // As per VSO:189935, Making Binary properties read-only in Edit Entity dialog until we have a full story for it. + entityAttributeType === TableConstants.TableType.Binary + ); + } + + private getPropertyDisplayValue(entity: Entities.ITableEntity, name: string, type: string): any { + var attribute: Entities.ITableEntityAttribute = entity[name]; + var displayValue: any = attribute._; + var isBinary: boolean = type === TableConstants.TableType.Binary; + + // Showing the value in base64 for binary properties since that is what the Azure Storage Client Library expects. + // This means that, even if the Azure Storage API returns a byte[] of binary content, it needs that same array + // *base64 - encoded * as the value for the updated property or the whole update operation will fail. + if (isBinary && displayValue && $.isArray(displayValue.data)) { + var bytes: number[] = displayValue.data; + displayValue = this.getBase64DisplayValue(bytes); + } + + return displayValue; + } + + private getBase64DisplayValue(bytes: number[]): string { + var displayValue: string = null; + + try { + var chars: string[] = bytes.map((byte: number) => String.fromCharCode(byte)); + var toEncode: string = chars.join(""); + displayValue = window.btoa(toEncode); + } catch (error) { + // Error + } + + return displayValue; + } +} diff --git a/src/Explorer/Panes/Tables/EntityPropertyViewModel.ts b/src/Explorer/Panes/Tables/EntityPropertyViewModel.ts index e0fef4486..9fd007683 100644 --- a/src/Explorer/Panes/Tables/EntityPropertyViewModel.ts +++ b/src/Explorer/Panes/Tables/EntityPropertyViewModel.ts @@ -1,164 +1,164 @@ -import * as ko from "knockout"; - -import * as DateTimeUtilities from "../../Tables/QueryBuilder/DateTimeUtilities"; -import * as EntityPropertyNameValidator from "./Validators/EntityPropertyNameValidator"; -import EntityPropertyValueValidator from "./Validators/EntityPropertyValueValidator"; -import * as Constants from "../../Tables/Constants"; -import * as Utilities from "../../Tables/Utilities"; -import TableEntityPane from "./TableEntityPane"; - -export interface IValidationResult { - isInvalid: boolean; - help: string; -} - -export interface IActionEnabledDialog { - updateIsActionEnabled: () => void; -} - -/** - * View model for an entity proprety - */ -export default class EntityPropertyViewModel { - /* Constants */ - public static noTooltip = ""; - // Maximum number of custom properties, see Azure Service Data Model - // At https://msdn.microsoft.com/library/azure/dd179338.aspx - public static maximumNumberOfProperties = 252; - - // Labels - public closeButtonLabel: string = "Close"; // localize - - /* Observables */ - public name: ko.Observable; - public type: ko.Observable; - public value: ko.Observable; - public inputType: ko.Computed; - - public nameTooltip: ko.Observable; - public isInvalidName: ko.Observable; - - public valueTooltip: ko.Observable; - public isInvalidValue: ko.Observable; - - public namePlaceholder: ko.Observable; - public valuePlaceholder: ko.Observable; - - public hasFocus: ko.Observable; - public valueHasFocus: ko.Observable; - public isDateType: ko.Computed; - - public editable: boolean; // If a property's name or type is editable, these two are always the same regarding editability. - public valueEditable: boolean; // If a property's value is editable, could be different from name or type. - public removable: boolean; // If a property is removable, usually, PartitionKey, RowKey and TimeStamp (if applicable) are not removable. - public isRequired: boolean; // If a property's value is required, used to differentiate the place holder label. - public ignoreEmptyValue: boolean; - - /* Members */ - private tableEntityPane: TableEntityPane; - private _validator: EntityPropertyValueValidator; - - constructor( - tableEntityPane: TableEntityPane, - name: string, - type: string, - value: any, - namePlaceholder: string = "", - valuePlaceholder: string = "", - editable: boolean = false, - defaultValidName: boolean = true, - defaultValidValue: boolean = false, - isRequired: boolean = false, - removable: boolean = editable, - valueEditable: boolean = editable, - ignoreEmptyValue: boolean = false - ) { - this.name = ko.observable(name); - this.type = ko.observable(type); - this.isDateType = ko.pureComputed(() => this.type() === Constants.TableType.DateTime); - if (this.isDateType()) { - value = value ? DateTimeUtilities.getLocalDateTime(value) : value; - } - this.value = ko.observable(value); - this.inputType = ko.pureComputed(() => { - if (!this.valueHasFocus() && !this.value() && this.isDateType()) { - return Constants.InputType.Text; - } - return Utilities.getInputTypeFromDisplayedName(this.type()); - }); - - this.namePlaceholder = ko.observable(namePlaceholder); - this.valuePlaceholder = ko.observable(valuePlaceholder); - - this.editable = editable; - this.isRequired = isRequired; - this.removable = removable; - this.valueEditable = valueEditable; - - this._validator = new EntityPropertyValueValidator(isRequired); - - this.tableEntityPane = tableEntityPane; - - this.nameTooltip = ko.observable(EntityPropertyViewModel.noTooltip); - this.isInvalidName = ko.observable(!defaultValidName); - this.name.subscribe((name: string) => this.validateName(name)); - if (!defaultValidName) { - this.validateName(name); - } - - this.valueTooltip = ko.observable(EntityPropertyViewModel.noTooltip); - this.isInvalidValue = ko.observable(!defaultValidValue); - this.value.subscribe((value: string) => this.validateValue(value, this.type())); - if (!defaultValidValue) { - this.validateValue(value, type); - } - - this.type.subscribe((type: string) => this.validateValue(this.value(), type)); - - this.hasFocus = ko.observable(false); - this.valueHasFocus = ko.observable(false); - } - - /** - * Gets the Javascript value of the entity property based on its EDM type. - */ - public getPropertyValue(): any { - var value: string = this.value(); - if (this.type() === Constants.TableType.DateTime) { - value = DateTimeUtilities.getUTCDateTime(value); - } - return this._validator.parseValue(value, this.type()); - } - - private validateName(name: string): void { - var result: IValidationResult = this.isInvalidNameInput(name); - - this.isInvalidName(result.isInvalid); - this.nameTooltip(result.help); - this.namePlaceholder(result.help); - this.tableEntityPane.updateIsActionEnabled(); - } - - private validateValue(value: string, type: string): void { - var result: IValidationResult = this.isInvalidValueInput(value, type); - if (!result) { - return; - } - - this.isInvalidValue(result.isInvalid); - this.valueTooltip(result.help); - this.valuePlaceholder(result.help); - this.tableEntityPane.updateIsActionEnabled(); - } - - private isInvalidNameInput(name: string): IValidationResult { - return EntityPropertyNameValidator.validate(name); - } - - private isInvalidValueInput(value: string, type: string): IValidationResult { - if (this.ignoreEmptyValue && this.value() === "") { - return { isInvalid: false, help: "" }; - } - return this._validator.validate(value, type); - } -} +import * as ko from "knockout"; + +import * as DateTimeUtilities from "../../Tables/QueryBuilder/DateTimeUtilities"; +import * as EntityPropertyNameValidator from "./Validators/EntityPropertyNameValidator"; +import EntityPropertyValueValidator from "./Validators/EntityPropertyValueValidator"; +import * as Constants from "../../Tables/Constants"; +import * as Utilities from "../../Tables/Utilities"; +import TableEntityPane from "./TableEntityPane"; + +export interface IValidationResult { + isInvalid: boolean; + help: string; +} + +export interface IActionEnabledDialog { + updateIsActionEnabled: () => void; +} + +/** + * View model for an entity proprety + */ +export default class EntityPropertyViewModel { + /* Constants */ + public static noTooltip = ""; + // Maximum number of custom properties, see Azure Service Data Model + // At https://msdn.microsoft.com/library/azure/dd179338.aspx + public static maximumNumberOfProperties = 252; + + // Labels + public closeButtonLabel: string = "Close"; // localize + + /* Observables */ + public name: ko.Observable; + public type: ko.Observable; + public value: ko.Observable; + public inputType: ko.Computed; + + public nameTooltip: ko.Observable; + public isInvalidName: ko.Observable; + + public valueTooltip: ko.Observable; + public isInvalidValue: ko.Observable; + + public namePlaceholder: ko.Observable; + public valuePlaceholder: ko.Observable; + + public hasFocus: ko.Observable; + public valueHasFocus: ko.Observable; + public isDateType: ko.Computed; + + public editable: boolean; // If a property's name or type is editable, these two are always the same regarding editability. + public valueEditable: boolean; // If a property's value is editable, could be different from name or type. + public removable: boolean; // If a property is removable, usually, PartitionKey, RowKey and TimeStamp (if applicable) are not removable. + public isRequired: boolean; // If a property's value is required, used to differentiate the place holder label. + public ignoreEmptyValue: boolean; + + /* Members */ + private tableEntityPane: TableEntityPane; + private _validator: EntityPropertyValueValidator; + + constructor( + tableEntityPane: TableEntityPane, + name: string, + type: string, + value: any, + namePlaceholder: string = "", + valuePlaceholder: string = "", + editable: boolean = false, + defaultValidName: boolean = true, + defaultValidValue: boolean = false, + isRequired: boolean = false, + removable: boolean = editable, + valueEditable: boolean = editable, + ignoreEmptyValue: boolean = false + ) { + this.name = ko.observable(name); + this.type = ko.observable(type); + this.isDateType = ko.pureComputed(() => this.type() === Constants.TableType.DateTime); + if (this.isDateType()) { + value = value ? DateTimeUtilities.getLocalDateTime(value) : value; + } + this.value = ko.observable(value); + this.inputType = ko.pureComputed(() => { + if (!this.valueHasFocus() && !this.value() && this.isDateType()) { + return Constants.InputType.Text; + } + return Utilities.getInputTypeFromDisplayedName(this.type()); + }); + + this.namePlaceholder = ko.observable(namePlaceholder); + this.valuePlaceholder = ko.observable(valuePlaceholder); + + this.editable = editable; + this.isRequired = isRequired; + this.removable = removable; + this.valueEditable = valueEditable; + + this._validator = new EntityPropertyValueValidator(isRequired); + + this.tableEntityPane = tableEntityPane; + + this.nameTooltip = ko.observable(EntityPropertyViewModel.noTooltip); + this.isInvalidName = ko.observable(!defaultValidName); + this.name.subscribe((name: string) => this.validateName(name)); + if (!defaultValidName) { + this.validateName(name); + } + + this.valueTooltip = ko.observable(EntityPropertyViewModel.noTooltip); + this.isInvalidValue = ko.observable(!defaultValidValue); + this.value.subscribe((value: string) => this.validateValue(value, this.type())); + if (!defaultValidValue) { + this.validateValue(value, type); + } + + this.type.subscribe((type: string) => this.validateValue(this.value(), type)); + + this.hasFocus = ko.observable(false); + this.valueHasFocus = ko.observable(false); + } + + /** + * Gets the Javascript value of the entity property based on its EDM type. + */ + public getPropertyValue(): any { + var value: string = this.value(); + if (this.type() === Constants.TableType.DateTime) { + value = DateTimeUtilities.getUTCDateTime(value); + } + return this._validator.parseValue(value, this.type()); + } + + private validateName(name: string): void { + var result: IValidationResult = this.isInvalidNameInput(name); + + this.isInvalidName(result.isInvalid); + this.nameTooltip(result.help); + this.namePlaceholder(result.help); + this.tableEntityPane.updateIsActionEnabled(); + } + + private validateValue(value: string, type: string): void { + var result: IValidationResult = this.isInvalidValueInput(value, type); + if (!result) { + return; + } + + this.isInvalidValue(result.isInvalid); + this.valueTooltip(result.help); + this.valuePlaceholder(result.help); + this.tableEntityPane.updateIsActionEnabled(); + } + + private isInvalidNameInput(name: string): IValidationResult { + return EntityPropertyNameValidator.validate(name); + } + + private isInvalidValueInput(value: string, type: string): IValidationResult { + if (this.ignoreEmptyValue && this.value() === "") { + return { isInvalid: false, help: "" }; + } + return this._validator.validate(value, type); + } +} diff --git a/src/Explorer/Panes/Tables/QuerySelectPane.ts b/src/Explorer/Panes/Tables/QuerySelectPane.ts index b82d3f59f..216dec1fe 100644 --- a/src/Explorer/Panes/Tables/QuerySelectPane.ts +++ b/src/Explorer/Panes/Tables/QuerySelectPane.ts @@ -1,174 +1,174 @@ -import * as ko from "knockout"; -import _ from "underscore"; -import * as Constants from "../../Tables/Constants"; -import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { ContextualPaneBase } from "../ContextualPaneBase"; - -export interface ISelectColumn { - columnName: ko.Observable; - selected: ko.Observable; - editable: ko.Observable; -} - -export class QuerySelectPane extends ContextualPaneBase { - public titleLabel: string = "Select Columns"; - public instructionLabel: string = "Select the columns that you want to query."; - public availableColumnsTableQueryLabel: string = "Available Columns"; - public noColumnSelectedWarning: string = "At least one column should be selected."; - - public columnOptions: ko.ObservableArray; - public anyColumnSelected: ko.Computed; - public canSelectAll: ko.Computed; - public allSelected: ko.Computed; - - private selectedColumnOption: ISelectColumn = null; - - public queryViewModel: QueryViewModel; - - constructor(options: ViewModels.PaneOptions) { - super(options); - - this.columnOptions = ko.observableArray(); - this.anyColumnSelected = ko.computed(() => { - return _.some(this.columnOptions(), (value: ISelectColumn) => { - return value.selected(); - }); - }); - - this.canSelectAll = ko.computed(() => { - return _.some(this.columnOptions(), (value: ISelectColumn) => { - return !value.selected(); - }); - }); - - this.allSelected = ko.pureComputed({ - read: () => { - return !this.canSelectAll(); - }, - write: value => { - if (value) { - this.selectAll(); - } else { - this.clearAll(); - } - }, - owner: this - }); - } - - public submit() { - this.queryViewModel.selectText(this.getParameters()); - this.queryViewModel.getSelectMessage(); - this.close(); - } - - public open() { - this.setTableColumns(this.queryViewModel.columnOptions()); - this.setDisplayedColumns(this.queryViewModel.selectText(), this.columnOptions()); - super.open(); - } - - private getParameters(): string[] { - if (this.canSelectAll() === false) { - return []; - } - - var selectedColumns = this.columnOptions().filter((value: ISelectColumn) => value.selected() === true); - - var columns: string[] = selectedColumns.map((value: ISelectColumn) => { - var name: string = value.columnName(); - return name; - }); - - return columns; - } - - public setTableColumns(columnNames: string[]): void { - var columns: ISelectColumn[] = columnNames.map((value: string) => { - var columnOption: ISelectColumn = { - columnName: ko.observable(value), - selected: ko.observable(true), - editable: ko.observable(this.isEntityEditable(value)) - }; - return columnOption; - }); - - this.columnOptions(columns); - } - - public setDisplayedColumns(querySelect: string[], columns: ISelectColumn[]): void { - if (querySelect == null || _.isEmpty(querySelect)) { - return; - } - this.setSelected(querySelect, columns); - } - - private setSelected(querySelect: string[], columns: ISelectColumn[]): void { - this.clearAll(); - querySelect && - querySelect.forEach((value: string) => { - for (var i = 0; i < columns.length; i++) { - if (value === columns[i].columnName()) { - columns[i].selected(true); - } - } - }); - } - - public availableColumnsCheckboxClick(): boolean { - if (this.canSelectAll()) { - return this.selectAll(); - } else { - return this.clearAll(); - } - } - - public selectAll(): boolean { - const columnOptions = this.columnOptions && this.columnOptions(); - columnOptions && - columnOptions.forEach((value: ISelectColumn) => { - value.selected(true); - }); - return true; - } - - public clearAll(): boolean { - const columnOptions = this.columnOptions && this.columnOptions(); - columnOptions && - columnOptions.forEach((column: ISelectColumn) => { - if (this.isEntityEditable(column.columnName())) { - column.selected(false); - } else { - column.selected(true); - } - }); - return true; - } - - public handleClick = (data: ISelectColumn, event: KeyboardEvent): boolean => { - this.selectTargetItem($(event.currentTarget), data); - return true; - }; - - private selectTargetItem($target: JQuery, targetColumn: ISelectColumn): void { - this.selectedColumnOption = targetColumn; - - $(".list-item.selected").removeClass("selected"); - $target.addClass("selected"); - } - - private isEntityEditable(name: string) { - if (this.queryViewModel.queryTablesTab.container.isPreferredApiCassandra()) { - const cassandraKeys = this.queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys - .concat(this.queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) - .map(key => key.property); - return !_.contains(cassandraKeys, name); - } - return !( - name === Constants.EntityKeyNames.PartitionKey || - name === Constants.EntityKeyNames.RowKey || - name === Constants.EntityKeyNames.Timestamp - ); - } -} +import * as ko from "knockout"; +import _ from "underscore"; +import * as Constants from "../../Tables/Constants"; +import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { ContextualPaneBase } from "../ContextualPaneBase"; + +export interface ISelectColumn { + columnName: ko.Observable; + selected: ko.Observable; + editable: ko.Observable; +} + +export class QuerySelectPane extends ContextualPaneBase { + public titleLabel: string = "Select Columns"; + public instructionLabel: string = "Select the columns that you want to query."; + public availableColumnsTableQueryLabel: string = "Available Columns"; + public noColumnSelectedWarning: string = "At least one column should be selected."; + + public columnOptions: ko.ObservableArray; + public anyColumnSelected: ko.Computed; + public canSelectAll: ko.Computed; + public allSelected: ko.Computed; + + private selectedColumnOption: ISelectColumn = null; + + public queryViewModel: QueryViewModel; + + constructor(options: ViewModels.PaneOptions) { + super(options); + + this.columnOptions = ko.observableArray(); + this.anyColumnSelected = ko.computed(() => { + return _.some(this.columnOptions(), (value: ISelectColumn) => { + return value.selected(); + }); + }); + + this.canSelectAll = ko.computed(() => { + return _.some(this.columnOptions(), (value: ISelectColumn) => { + return !value.selected(); + }); + }); + + this.allSelected = ko.pureComputed({ + read: () => { + return !this.canSelectAll(); + }, + write: (value) => { + if (value) { + this.selectAll(); + } else { + this.clearAll(); + } + }, + owner: this, + }); + } + + public submit() { + this.queryViewModel.selectText(this.getParameters()); + this.queryViewModel.getSelectMessage(); + this.close(); + } + + public open() { + this.setTableColumns(this.queryViewModel.columnOptions()); + this.setDisplayedColumns(this.queryViewModel.selectText(), this.columnOptions()); + super.open(); + } + + private getParameters(): string[] { + if (this.canSelectAll() === false) { + return []; + } + + var selectedColumns = this.columnOptions().filter((value: ISelectColumn) => value.selected() === true); + + var columns: string[] = selectedColumns.map((value: ISelectColumn) => { + var name: string = value.columnName(); + return name; + }); + + return columns; + } + + public setTableColumns(columnNames: string[]): void { + var columns: ISelectColumn[] = columnNames.map((value: string) => { + var columnOption: ISelectColumn = { + columnName: ko.observable(value), + selected: ko.observable(true), + editable: ko.observable(this.isEntityEditable(value)), + }; + return columnOption; + }); + + this.columnOptions(columns); + } + + public setDisplayedColumns(querySelect: string[], columns: ISelectColumn[]): void { + if (querySelect == null || _.isEmpty(querySelect)) { + return; + } + this.setSelected(querySelect, columns); + } + + private setSelected(querySelect: string[], columns: ISelectColumn[]): void { + this.clearAll(); + querySelect && + querySelect.forEach((value: string) => { + for (var i = 0; i < columns.length; i++) { + if (value === columns[i].columnName()) { + columns[i].selected(true); + } + } + }); + } + + public availableColumnsCheckboxClick(): boolean { + if (this.canSelectAll()) { + return this.selectAll(); + } else { + return this.clearAll(); + } + } + + public selectAll(): boolean { + const columnOptions = this.columnOptions && this.columnOptions(); + columnOptions && + columnOptions.forEach((value: ISelectColumn) => { + value.selected(true); + }); + return true; + } + + public clearAll(): boolean { + const columnOptions = this.columnOptions && this.columnOptions(); + columnOptions && + columnOptions.forEach((column: ISelectColumn) => { + if (this.isEntityEditable(column.columnName())) { + column.selected(false); + } else { + column.selected(true); + } + }); + return true; + } + + public handleClick = (data: ISelectColumn, event: KeyboardEvent): boolean => { + this.selectTargetItem($(event.currentTarget), data); + return true; + }; + + private selectTargetItem($target: JQuery, targetColumn: ISelectColumn): void { + this.selectedColumnOption = targetColumn; + + $(".list-item.selected").removeClass("selected"); + $target.addClass("selected"); + } + + private isEntityEditable(name: string) { + if (this.queryViewModel.queryTablesTab.container.isPreferredApiCassandra()) { + const cassandraKeys = this.queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys + .concat(this.queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) + .map((key) => key.property); + return !_.contains(cassandraKeys, name); + } + return !( + name === Constants.EntityKeyNames.PartitionKey || + name === Constants.EntityKeyNames.RowKey || + name === Constants.EntityKeyNames.Timestamp + ); + } +} diff --git a/src/Explorer/Panes/Tables/TableAddEntityPane.html b/src/Explorer/Panes/Tables/TableAddEntityPane.html index 575f1ef44..cf8a6cf98 100644 --- a/src/Explorer/Panes/Tables/TableAddEntityPane.html +++ b/src/Explorer/Panes/Tables/TableAddEntityPane.html @@ -1,191 +1,190 @@ -
-
-
- -
-
- -
- -
- Close -
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
- -
- -
- - -
- -
- -
- - Edit - - - Cancel - -
-
-
-
-
-
- - Insert attribute - - -
-
-
-
-
-
-
- -
-
-
-
- - - - -
-
+
+
+
+ +
+
+ +
+ +
+ Close +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + Edit + + + Cancel + +
+
+
+
+
+
+ + Insert attribute + + +
+
+
+
+
+
+
+ +
+
+
+
+ + + + +
+
diff --git a/src/Explorer/Panes/Tables/TableColumnOptionsPane.html b/src/Explorer/Panes/Tables/TableColumnOptionsPane.html index 0ce40bac6..a0e289976 100644 --- a/src/Explorer/Panes/Tables/TableColumnOptionsPane.html +++ b/src/Explorer/Panes/Tables/TableColumnOptionsPane.html @@ -1,78 +1,78 @@ -
-
-
- -
-
- -
- Column Options -
- Close -
-
- -
-
Choose the columns and the order in which you want to display them in the table.
-
-
- - - - Move down - - - Move up - -
-
-
-
    -
  • - - -
  • -
-
-
-
- -
- -
-
-
-
-
-
-
- -
-
+
+
+
+ +
+
+ +
+ Column Options +
+ Close +
+
+ +
+
Choose the columns and the order in which you want to display them in the table.
+
+
+ + + + Move down + + + Move up + +
+
+
+
    +
  • + + +
  • +
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+
diff --git a/src/Explorer/Panes/Tables/TableColumnOptionsPane.ts b/src/Explorer/Panes/Tables/TableColumnOptionsPane.ts index 16541436c..0da321ab2 100644 --- a/src/Explorer/Panes/Tables/TableColumnOptionsPane.ts +++ b/src/Explorer/Panes/Tables/TableColumnOptionsPane.ts @@ -1,195 +1,195 @@ -import * as ko from "knockout"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import * as DataTableOperations from "../../Tables/DataTable/DataTableOperations"; -import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel"; -import { ContextualPaneBase } from "../ContextualPaneBase"; -import _ from "underscore"; - -/** - * Represents an item shown in the available columns. - * columnName: the name of the column. - * selected: indicate whether user wants to display the column in the table. - * order: the order in the initial table. E.g., - * Order array of initial table: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8} - * Order array of current table: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8} - * if order = 6, then this column will be the one with column name prop6 - * index: index in the observable array, this used for selection and moving up/down. - */ -interface IColumnOption { - columnName: ko.Observable; - selected: ko.Observable; - order: number; - index: number; -} - -export interface IColumnSetting { - columnNames: string[]; - visible?: boolean[]; - order?: number[]; -} - -export class TableColumnOptionsPane extends ContextualPaneBase { - public titleLabel: string = "Column Options"; - public instructionLabel: string = "Choose the columns and the order in which you want to display them in the table."; - public availableColumnsLabel: string = "Available Columns"; - public moveUpLabel: string = "Move Up"; - public moveDownLabel: string = "Move Down"; - public noColumnSelectedWarning: string = "At least one column should be selected."; - - public columnOptions: ko.ObservableArray; - public allSelected: ko.Computed; - public anyColumnSelected: ko.Computed; - public canSelectAll: ko.Computed; - public canMoveUp: ko.Observable; - public canMoveDown: ko.Observable; - - public tableViewModel: TableEntityListViewModel; - public parameters: IColumnSetting; - - private selectedColumnOption: IColumnOption = null; - - constructor(options: ViewModels.PaneOptions) { - super(options); - - this.columnOptions = ko.observableArray(); - this.anyColumnSelected = ko.computed(() => { - return _.some(this.columnOptions(), (value: IColumnOption) => { - return value.selected(); - }); - }); - - this.canSelectAll = ko.computed(() => { - return _.some(this.columnOptions(), (value: IColumnOption) => { - return !value.selected(); - }); - }); - - this.canMoveUp = ko.observable(false); - this.canMoveDown = ko.observable(false); - - this.allSelected = ko.pureComputed({ - read: () => { - return !this.canSelectAll(); - }, - write: value => { - if (value) { - this.selectAll(); - } else { - this.clearAll(); - } - }, - owner: this - }); - } - - public submit() { - var newColumnSetting = this.getParameters(); - DataTableOperations.reorderColumns(this.tableViewModel.table, newColumnSetting.order).then(() => { - DataTableOperations.filterColumns(this.tableViewModel.table, newColumnSetting.visible); - this.visible(false); - }); - } - public open() { - this.setDisplayedColumns(this.parameters.columnNames, this.parameters.order, this.parameters.visible); - super.open(); - } - - private getParameters(): IColumnSetting { - var newColumnSettings: IColumnSetting = { - columnNames: [], - order: [], - visible: [] - }; - this.columnOptions().map((value: IColumnOption) => { - newColumnSettings.columnNames.push(value.columnName()); - newColumnSettings.order.push(value.order); - newColumnSettings.visible.push(value.selected()); - }); - return newColumnSettings; - } - - public setDisplayedColumns(columnNames: string[], order: number[], visible: boolean[]): void { - var options: IColumnOption[] = order.map((value: number, index: number) => { - var columnOption: IColumnOption = { - columnName: ko.observable(columnNames[index]), - order: value, - selected: ko.observable(visible[index]), - index: index - }; - return columnOption; - }); - this.columnOptions(options); - } - - public selectAll(): void { - const columnOptions = this.columnOptions && this.columnOptions(); - columnOptions && - columnOptions.forEach((value: IColumnOption) => { - value.selected(true); - }); - } - - public clearAll(): void { - const columnOptions = this.columnOptions && this.columnOptions(); - columnOptions && - columnOptions.forEach((value: IColumnOption) => { - value.selected(false); - }); - - if (columnOptions && columnOptions.length > 0) { - columnOptions[0].selected(true); - } - } - - public moveUp(): void { - if (this.selectedColumnOption) { - var currentSelectedIndex: number = this.selectedColumnOption.index; - var swapTargetIndex: number = currentSelectedIndex - 1; - //Debug.assert(currentSelectedIndex > 0); - - this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex); - this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]); - } - } - - public moveDown(): void { - if (this.selectedColumnOption) { - var currentSelectedIndex: number = this.selectedColumnOption.index; - var swapTargetIndex: number = currentSelectedIndex + 1; - //Debug.assert(currentSelectedIndex < (this.columnOptions().length - 1)); - - this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex); - this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]); - } - } - - public handleClick = (data: IColumnOption, event: KeyboardEvent): boolean => { - this.selectTargetItem($(event.currentTarget), data); - return true; - }; - - private selectTargetItem($target: JQuery, targetColumn: IColumnOption): void { - this.selectedColumnOption = targetColumn; - - this.canMoveUp(targetColumn.index !== 0); - this.canMoveDown(targetColumn.index !== this.columnOptions().length - 1); - - $(".list-item.selected").removeClass("selected"); - $target.addClass("selected"); - } - - private swapColumnOption(options: IColumnOption[], indexA: number, indexB: number): void { - var tempColumnName: string = options[indexA].columnName(); - var tempSelected: boolean = options[indexA].selected(); - var tempOrder: number = options[indexA].order; - - options[indexA].columnName(options[indexB].columnName()); - options[indexB].columnName(tempColumnName); - - options[indexA].selected(options[indexB].selected()); - options[indexB].selected(tempSelected); - - options[indexA].order = options[indexB].order; - options[indexB].order = tempOrder; - } -} +import * as ko from "knockout"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import * as DataTableOperations from "../../Tables/DataTable/DataTableOperations"; +import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel"; +import { ContextualPaneBase } from "../ContextualPaneBase"; +import _ from "underscore"; + +/** + * Represents an item shown in the available columns. + * columnName: the name of the column. + * selected: indicate whether user wants to display the column in the table. + * order: the order in the initial table. E.g., + * Order array of initial table: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8} + * Order array of current table: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8} + * if order = 6, then this column will be the one with column name prop6 + * index: index in the observable array, this used for selection and moving up/down. + */ +interface IColumnOption { + columnName: ko.Observable; + selected: ko.Observable; + order: number; + index: number; +} + +export interface IColumnSetting { + columnNames: string[]; + visible?: boolean[]; + order?: number[]; +} + +export class TableColumnOptionsPane extends ContextualPaneBase { + public titleLabel: string = "Column Options"; + public instructionLabel: string = "Choose the columns and the order in which you want to display them in the table."; + public availableColumnsLabel: string = "Available Columns"; + public moveUpLabel: string = "Move Up"; + public moveDownLabel: string = "Move Down"; + public noColumnSelectedWarning: string = "At least one column should be selected."; + + public columnOptions: ko.ObservableArray; + public allSelected: ko.Computed; + public anyColumnSelected: ko.Computed; + public canSelectAll: ko.Computed; + public canMoveUp: ko.Observable; + public canMoveDown: ko.Observable; + + public tableViewModel: TableEntityListViewModel; + public parameters: IColumnSetting; + + private selectedColumnOption: IColumnOption = null; + + constructor(options: ViewModels.PaneOptions) { + super(options); + + this.columnOptions = ko.observableArray(); + this.anyColumnSelected = ko.computed(() => { + return _.some(this.columnOptions(), (value: IColumnOption) => { + return value.selected(); + }); + }); + + this.canSelectAll = ko.computed(() => { + return _.some(this.columnOptions(), (value: IColumnOption) => { + return !value.selected(); + }); + }); + + this.canMoveUp = ko.observable(false); + this.canMoveDown = ko.observable(false); + + this.allSelected = ko.pureComputed({ + read: () => { + return !this.canSelectAll(); + }, + write: (value) => { + if (value) { + this.selectAll(); + } else { + this.clearAll(); + } + }, + owner: this, + }); + } + + public submit() { + var newColumnSetting = this.getParameters(); + DataTableOperations.reorderColumns(this.tableViewModel.table, newColumnSetting.order).then(() => { + DataTableOperations.filterColumns(this.tableViewModel.table, newColumnSetting.visible); + this.visible(false); + }); + } + public open() { + this.setDisplayedColumns(this.parameters.columnNames, this.parameters.order, this.parameters.visible); + super.open(); + } + + private getParameters(): IColumnSetting { + var newColumnSettings: IColumnSetting = { + columnNames: [], + order: [], + visible: [], + }; + this.columnOptions().map((value: IColumnOption) => { + newColumnSettings.columnNames.push(value.columnName()); + newColumnSettings.order.push(value.order); + newColumnSettings.visible.push(value.selected()); + }); + return newColumnSettings; + } + + public setDisplayedColumns(columnNames: string[], order: number[], visible: boolean[]): void { + var options: IColumnOption[] = order.map((value: number, index: number) => { + var columnOption: IColumnOption = { + columnName: ko.observable(columnNames[index]), + order: value, + selected: ko.observable(visible[index]), + index: index, + }; + return columnOption; + }); + this.columnOptions(options); + } + + public selectAll(): void { + const columnOptions = this.columnOptions && this.columnOptions(); + columnOptions && + columnOptions.forEach((value: IColumnOption) => { + value.selected(true); + }); + } + + public clearAll(): void { + const columnOptions = this.columnOptions && this.columnOptions(); + columnOptions && + columnOptions.forEach((value: IColumnOption) => { + value.selected(false); + }); + + if (columnOptions && columnOptions.length > 0) { + columnOptions[0].selected(true); + } + } + + public moveUp(): void { + if (this.selectedColumnOption) { + var currentSelectedIndex: number = this.selectedColumnOption.index; + var swapTargetIndex: number = currentSelectedIndex - 1; + //Debug.assert(currentSelectedIndex > 0); + + this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex); + this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]); + } + } + + public moveDown(): void { + if (this.selectedColumnOption) { + var currentSelectedIndex: number = this.selectedColumnOption.index; + var swapTargetIndex: number = currentSelectedIndex + 1; + //Debug.assert(currentSelectedIndex < (this.columnOptions().length - 1)); + + this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex); + this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]); + } + } + + public handleClick = (data: IColumnOption, event: KeyboardEvent): boolean => { + this.selectTargetItem($(event.currentTarget), data); + return true; + }; + + private selectTargetItem($target: JQuery, targetColumn: IColumnOption): void { + this.selectedColumnOption = targetColumn; + + this.canMoveUp(targetColumn.index !== 0); + this.canMoveDown(targetColumn.index !== this.columnOptions().length - 1); + + $(".list-item.selected").removeClass("selected"); + $target.addClass("selected"); + } + + private swapColumnOption(options: IColumnOption[], indexA: number, indexB: number): void { + var tempColumnName: string = options[indexA].columnName(); + var tempSelected: boolean = options[indexA].selected(); + var tempOrder: number = options[indexA].order; + + options[indexA].columnName(options[indexB].columnName()); + options[indexB].columnName(tempColumnName); + + options[indexA].selected(options[indexB].selected()); + options[indexB].selected(tempSelected); + + options[indexA].order = options[indexB].order; + options[indexB].order = tempOrder; + } +} diff --git a/src/Explorer/Panes/Tables/TableEditEntityPane.html b/src/Explorer/Panes/Tables/TableEditEntityPane.html index 435d5ea3f..44cbc6c9a 100644 --- a/src/Explorer/Panes/Tables/TableEditEntityPane.html +++ b/src/Explorer/Panes/Tables/TableEditEntityPane.html @@ -1,188 +1,187 @@ -
-
-
- -
-
- -
- -
- Close -
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
- -
- -
- - -
- -
- -
- - Edit attribute - - - Remove attribute - -
-
-
-
-
-
- - Add attribute - - -
-
-
-
-
-
-
- -
-
-
-
- - - - -
-
+
+
+
+ +
+
+ +
+ +
+ Close +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + Edit attribute + + + Remove attribute + +
+
+
+
+
+
+ + Add attribute + + +
+
+
+
+
+
+
+ +
+
+
+
+ + + + +
+
diff --git a/src/Explorer/Panes/Tables/TableEntityPane.ts b/src/Explorer/Panes/Tables/TableEntityPane.ts index 56d3961da..f9f358aa7 100644 --- a/src/Explorer/Panes/Tables/TableEntityPane.ts +++ b/src/Explorer/Panes/Tables/TableEntityPane.ts @@ -1,281 +1,281 @@ -import * as ko from "knockout"; -import _ from "underscore"; -import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; -import * as Entities from "../../Tables/Entities"; -import EntityPropertyViewModel from "./EntityPropertyViewModel"; -import * as TableConstants from "../../Tables/Constants"; -import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel"; -import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; -import * as Utilities from "../../Tables/Utilities"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { KeyCodes } from "../../../Common/Constants"; -import { ContextualPaneBase } from "../ContextualPaneBase"; - -// Class with variables and functions that are common to both adding and editing entities -export default abstract class TableEntityPane extends ContextualPaneBase { - protected static requiredFieldsForTablesAPI: string[] = [ - TableConstants.EntityKeyNames.PartitionKey, - TableConstants.EntityKeyNames.RowKey - ]; - - /* Labels */ - public attributeNameLabel = "Property Name"; // localize - public dataTypeLabel = "Type"; // localize - public attributeValueLabel = "Value"; // localize - - /* Controls */ - public removeButtonLabel = "Remove"; // localize - public editButtonLabel = "Edit"; // localize - public addButtonLabel = "Add Property"; // localize - - public edmTypes: ko.ObservableArray = ko.observableArray([ - TableConstants.TableType.String, - TableConstants.TableType.Boolean, - TableConstants.TableType.Binary, - TableConstants.TableType.DateTime, - TableConstants.TableType.Double, - TableConstants.TableType.Guid, - TableConstants.TableType.Int32, - TableConstants.TableType.Int64 - ]); - - public canAdd: ko.Computed; - public canApply: ko.Observable; - public displayedAttributes = ko.observableArray(); - public editingProperty = ko.observable(); - public isEditing = ko.observable(false); - public submitButtonText = ko.observable(); - - public tableViewModel: TableEntityListViewModel; - - protected scrollId: ko.Observable; - - constructor(options: ViewModels.PaneOptions) { - super(options); - this.container.isPreferredApiCassandra.subscribe(isCassandra => { - if (isCassandra) { - this.edmTypes([ - TableConstants.CassandraType.Text, - TableConstants.CassandraType.Ascii, - TableConstants.CassandraType.Bigint, - TableConstants.CassandraType.Blob, - TableConstants.CassandraType.Boolean, - TableConstants.CassandraType.Decimal, - TableConstants.CassandraType.Double, - TableConstants.CassandraType.Float, - TableConstants.CassandraType.Int, - TableConstants.CassandraType.Uuid, - TableConstants.CassandraType.Varchar, - TableConstants.CassandraType.Varint, - TableConstants.CassandraType.Inet, - TableConstants.CassandraType.Smallint, - TableConstants.CassandraType.Tinyint - ]); - } - }); - - this.canAdd = ko.computed(() => { - // Cassandra can't add since the schema can't be changed once created - if (this.container.isPreferredApiCassandra()) { - return false; - } - // Adding '2' to the maximum to take into account PartitionKey and RowKey - return this.displayedAttributes().length < EntityPropertyViewModel.maximumNumberOfProperties + 2; - }); - this.canApply = ko.observable(true); - this.editingProperty(this.displayedAttributes()[0]); - } - - public removeAttribute = (index: number, data: any): void => { - this.displayedAttributes.splice(index, 1); - this.updateIsActionEnabled(); - document.getElementById("addProperty").focus(); - }; - - public editAttribute = (index: number, data: EntityPropertyViewModel): void => { - this.editingProperty(data); - this.isEditing(true); - document.getElementById("textAreaEditProperty").focus(); - }; - - public finishEditingAttribute = (): void => { - this.isEditing(false); - this.editingProperty(null); - }; - - public onKeyUp = (data: any, event: KeyboardEvent): boolean => { - var handled: boolean = Utilities.onEsc(event, ($sourceElement: JQuery) => { - this.finishEditingAttribute(); - }); - - return !handled; - }; - - public onAddPropertyKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.insertAttribute(); - event.stopPropagation(); - return false; - } - - return true; - }; - - public onEditPropertyKeyDown = ( - index: number, - data: EntityPropertyViewModel, - event: KeyboardEvent, - source: any - ): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.editAttribute(index, data); - event.stopPropagation(); - return false; - } - - return true; - }; - - public onDeletePropertyKeyDown = ( - index: number, - data: EntityPropertyViewModel, - event: KeyboardEvent, - source: any - ): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.removeAttribute(index, data); - event.stopPropagation(); - return false; - } - - return true; - }; - - public onBackButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.finishEditingAttribute(); - event.stopPropagation(); - return false; - } - - return true; - }; - - public insertAttribute = (name?: string, type?: string): void => { - let entityProperty: EntityPropertyViewModel; - if (!!name && !!type && this.container.isPreferredApiCassandra()) { - // TODO figure out validation story for blob and Inet so we can allow adding/editing them - const nonEditableType: boolean = - type === TableConstants.CassandraType.Blob || type === TableConstants.CassandraType.Inet; - entityProperty = new EntityPropertyViewModel( - this, - name, - type, - "", // default to empty string - /* namePlaceholder */ undefined, - /* valuePlaceholder */ undefined, - /* editable */ false, - /* default valid name */ false, - /* default valid value */ true, - /* isRequired */ false, - /* removable */ false, - /*value editable*/ !nonEditableType - ); - } else { - entityProperty = new EntityPropertyViewModel( - this, - "", - this.edmTypes()[0], // default to the first Edm type: 'string' - "", // default to empty string - /* namePlaceholder */ undefined, - /* valuePlaceholder */ undefined, - /* editable */ true, - /* default valid name */ false, - /* default valid value */ true - ); - } - - this.displayedAttributes.push(entityProperty); - this.updateIsActionEnabled(); - this.scrollToBottom(); - - entityProperty.hasFocus(true); - }; - - public updateIsActionEnabled(needRequiredFields: boolean = true): void { - var properties: EntityPropertyViewModel[] = this.displayedAttributes() || []; - var disable: boolean = _.some(properties, (property: EntityPropertyViewModel) => { - return property.isInvalidName() || property.isInvalidValue(); - }); - - this.canApply(!disable); - } - - protected entityFromAttributes(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity { - var entity: any = {}; - - displayedAttributes && - displayedAttributes.forEach((attribute: EntityPropertyViewModel) => { - if (attribute.name() && (attribute.value() !== "" || attribute.isRequired)) { - var value = attribute.getPropertyValue(); - var type = attribute.type(); - if (type === TableConstants.TableType.Int64) { - value = Utilities.padLongWithZeros(value); - } - entity[attribute.name()] = { - _: value, - $: type - }; - } - }); - - return entity; - } - - // Removing Binary from Add Entity dialog until we have a full story for it. - protected setOptionDisable(option: Node, value: string): void { - ko.applyBindingsToNode(option, { disable: value === TableConstants.TableType.Binary }, value); - } - - /** - * Parse the updated entity to see if there are any new attributes that old headers don't have. - * In this case, add these attributes names as new headers. - * Remarks: adding new headers will automatically trigger table redraw. - */ - protected tryInsertNewHeaders(viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean { - var newHeaders: string[] = []; - const keys = Object.keys(newEntity); - keys && - keys.forEach((key: string) => { - if ( - !_.contains(viewModel.headers, key) && - key !== TableEntityProcessor.keyProperties.attachments && - key !== TableEntityProcessor.keyProperties.etag && - key !== TableEntityProcessor.keyProperties.resourceId && - key !== TableEntityProcessor.keyProperties.self && - (!viewModel.queryTablesTab.container.isPreferredApiCassandra() || - key !== TableConstants.EntityKeyNames.RowKey) - ) { - newHeaders.push(key); - } - }); - - var newHeadersInserted: boolean = false; - if (newHeaders.length) { - if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) { - newHeaders = viewModel.headers.concat(newHeaders); - } - viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false); - newHeadersInserted = true; - } - return newHeadersInserted; - } - - protected scrollToBottom(): void { - var scrollBox = document.getElementById(this.scrollId()); - var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1; - if (isScrolledToBottom) { - scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight; - } - } -} +import * as ko from "knockout"; +import _ from "underscore"; +import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; +import * as Entities from "../../Tables/Entities"; +import EntityPropertyViewModel from "./EntityPropertyViewModel"; +import * as TableConstants from "../../Tables/Constants"; +import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel"; +import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; +import * as Utilities from "../../Tables/Utilities"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { KeyCodes } from "../../../Common/Constants"; +import { ContextualPaneBase } from "../ContextualPaneBase"; + +// Class with variables and functions that are common to both adding and editing entities +export default abstract class TableEntityPane extends ContextualPaneBase { + protected static requiredFieldsForTablesAPI: string[] = [ + TableConstants.EntityKeyNames.PartitionKey, + TableConstants.EntityKeyNames.RowKey, + ]; + + /* Labels */ + public attributeNameLabel = "Property Name"; // localize + public dataTypeLabel = "Type"; // localize + public attributeValueLabel = "Value"; // localize + + /* Controls */ + public removeButtonLabel = "Remove"; // localize + public editButtonLabel = "Edit"; // localize + public addButtonLabel = "Add Property"; // localize + + public edmTypes: ko.ObservableArray = ko.observableArray([ + TableConstants.TableType.String, + TableConstants.TableType.Boolean, + TableConstants.TableType.Binary, + TableConstants.TableType.DateTime, + TableConstants.TableType.Double, + TableConstants.TableType.Guid, + TableConstants.TableType.Int32, + TableConstants.TableType.Int64, + ]); + + public canAdd: ko.Computed; + public canApply: ko.Observable; + public displayedAttributes = ko.observableArray(); + public editingProperty = ko.observable(); + public isEditing = ko.observable(false); + public submitButtonText = ko.observable(); + + public tableViewModel: TableEntityListViewModel; + + protected scrollId: ko.Observable; + + constructor(options: ViewModels.PaneOptions) { + super(options); + this.container.isPreferredApiCassandra.subscribe((isCassandra) => { + if (isCassandra) { + this.edmTypes([ + TableConstants.CassandraType.Text, + TableConstants.CassandraType.Ascii, + TableConstants.CassandraType.Bigint, + TableConstants.CassandraType.Blob, + TableConstants.CassandraType.Boolean, + TableConstants.CassandraType.Decimal, + TableConstants.CassandraType.Double, + TableConstants.CassandraType.Float, + TableConstants.CassandraType.Int, + TableConstants.CassandraType.Uuid, + TableConstants.CassandraType.Varchar, + TableConstants.CassandraType.Varint, + TableConstants.CassandraType.Inet, + TableConstants.CassandraType.Smallint, + TableConstants.CassandraType.Tinyint, + ]); + } + }); + + this.canAdd = ko.computed(() => { + // Cassandra can't add since the schema can't be changed once created + if (this.container.isPreferredApiCassandra()) { + return false; + } + // Adding '2' to the maximum to take into account PartitionKey and RowKey + return this.displayedAttributes().length < EntityPropertyViewModel.maximumNumberOfProperties + 2; + }); + this.canApply = ko.observable(true); + this.editingProperty(this.displayedAttributes()[0]); + } + + public removeAttribute = (index: number, data: any): void => { + this.displayedAttributes.splice(index, 1); + this.updateIsActionEnabled(); + document.getElementById("addProperty").focus(); + }; + + public editAttribute = (index: number, data: EntityPropertyViewModel): void => { + this.editingProperty(data); + this.isEditing(true); + document.getElementById("textAreaEditProperty").focus(); + }; + + public finishEditingAttribute = (): void => { + this.isEditing(false); + this.editingProperty(null); + }; + + public onKeyUp = (data: any, event: KeyboardEvent): boolean => { + var handled: boolean = Utilities.onEsc(event, ($sourceElement: JQuery) => { + this.finishEditingAttribute(); + }); + + return !handled; + }; + + public onAddPropertyKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.insertAttribute(); + event.stopPropagation(); + return false; + } + + return true; + }; + + public onEditPropertyKeyDown = ( + index: number, + data: EntityPropertyViewModel, + event: KeyboardEvent, + source: any + ): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.editAttribute(index, data); + event.stopPropagation(); + return false; + } + + return true; + }; + + public onDeletePropertyKeyDown = ( + index: number, + data: EntityPropertyViewModel, + event: KeyboardEvent, + source: any + ): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.removeAttribute(index, data); + event.stopPropagation(); + return false; + } + + return true; + }; + + public onBackButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.finishEditingAttribute(); + event.stopPropagation(); + return false; + } + + return true; + }; + + public insertAttribute = (name?: string, type?: string): void => { + let entityProperty: EntityPropertyViewModel; + if (!!name && !!type && this.container.isPreferredApiCassandra()) { + // TODO figure out validation story for blob and Inet so we can allow adding/editing them + const nonEditableType: boolean = + type === TableConstants.CassandraType.Blob || type === TableConstants.CassandraType.Inet; + entityProperty = new EntityPropertyViewModel( + this, + name, + type, + "", // default to empty string + /* namePlaceholder */ undefined, + /* valuePlaceholder */ undefined, + /* editable */ false, + /* default valid name */ false, + /* default valid value */ true, + /* isRequired */ false, + /* removable */ false, + /*value editable*/ !nonEditableType + ); + } else { + entityProperty = new EntityPropertyViewModel( + this, + "", + this.edmTypes()[0], // default to the first Edm type: 'string' + "", // default to empty string + /* namePlaceholder */ undefined, + /* valuePlaceholder */ undefined, + /* editable */ true, + /* default valid name */ false, + /* default valid value */ true + ); + } + + this.displayedAttributes.push(entityProperty); + this.updateIsActionEnabled(); + this.scrollToBottom(); + + entityProperty.hasFocus(true); + }; + + public updateIsActionEnabled(needRequiredFields: boolean = true): void { + var properties: EntityPropertyViewModel[] = this.displayedAttributes() || []; + var disable: boolean = _.some(properties, (property: EntityPropertyViewModel) => { + return property.isInvalidName() || property.isInvalidValue(); + }); + + this.canApply(!disable); + } + + protected entityFromAttributes(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity { + var entity: any = {}; + + displayedAttributes && + displayedAttributes.forEach((attribute: EntityPropertyViewModel) => { + if (attribute.name() && (attribute.value() !== "" || attribute.isRequired)) { + var value = attribute.getPropertyValue(); + var type = attribute.type(); + if (type === TableConstants.TableType.Int64) { + value = Utilities.padLongWithZeros(value); + } + entity[attribute.name()] = { + _: value, + $: type, + }; + } + }); + + return entity; + } + + // Removing Binary from Add Entity dialog until we have a full story for it. + protected setOptionDisable(option: Node, value: string): void { + ko.applyBindingsToNode(option, { disable: value === TableConstants.TableType.Binary }, value); + } + + /** + * Parse the updated entity to see if there are any new attributes that old headers don't have. + * In this case, add these attributes names as new headers. + * Remarks: adding new headers will automatically trigger table redraw. + */ + protected tryInsertNewHeaders(viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean { + var newHeaders: string[] = []; + const keys = Object.keys(newEntity); + keys && + keys.forEach((key: string) => { + if ( + !_.contains(viewModel.headers, key) && + key !== TableEntityProcessor.keyProperties.attachments && + key !== TableEntityProcessor.keyProperties.etag && + key !== TableEntityProcessor.keyProperties.resourceId && + key !== TableEntityProcessor.keyProperties.self && + (!viewModel.queryTablesTab.container.isPreferredApiCassandra() || + key !== TableConstants.EntityKeyNames.RowKey) + ) { + newHeaders.push(key); + } + }); + + var newHeadersInserted: boolean = false; + if (newHeaders.length) { + if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) { + newHeaders = viewModel.headers.concat(newHeaders); + } + viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false); + newHeadersInserted = true; + } + return newHeadersInserted; + } + + protected scrollToBottom(): void { + var scrollBox = document.getElementById(this.scrollId()); + var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1; + if (isScrolledToBottom) { + scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight; + } + } +} diff --git a/src/Explorer/Panes/Tables/TableQuerySelectPane.html b/src/Explorer/Panes/Tables/TableQuerySelectPane.html index a6cb4110d..65e8cfb87 100644 --- a/src/Explorer/Panes/Tables/TableQuerySelectPane.html +++ b/src/Explorer/Panes/Tables/TableQuerySelectPane.html @@ -1,79 +1,79 @@ -
-
-
- -
-
- -
- Select Column -
- Close -
-
- -
- -
Select the columns that you want to query.
-
-
- - -
-
-
-
    - -
  • - - -
  • - - -
  • - - -
  • - -
-
-
-
- -
- -
-
-
-
-
-
-
- -
-
+
+
+
+ +
+
+ +
+ Select Column +
+ Close +
+
+ +
+ +
Select the columns that you want to query.
+
+
+ + +
+
+
+
    + +
  • + + +
  • + + +
  • + + +
  • + +
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+
diff --git a/src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts b/src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts index c2e61fcb3..44abf0fa0 100644 --- a/src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts +++ b/src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts @@ -1,306 +1,306 @@ -/* Constants */ -var MaximumNameLength = 255; -var noHelp = ""; -var detailedHelp = "Enter a name up to 255 characters in size. Most valid C# identifiers are allowed."; // localize - -export interface IValidationResult { - isInvalid: boolean; - help: string; -} - -export function validate(name: string): IValidationResult { - var help: string = noHelp; - // Note: Disabling encoding check to err the side of lax validation. - // A valid property name should also be XML-serializable. - // Hence, only allowing names that don't require special encoding for network transmission. - // var encoded: string = tryEncode(name); - // var success: boolean = (name === encoded); - var success: boolean = true; - - if (success) { - success = name.length <= MaximumNameLength; - if (success) { - success = IsValidIdentifier(name); - } - } - - if (!success) { - help = detailedHelp; - } - - return { isInvalid: !success, help: help }; -} - -/* -function tryEncode(name: string): string { - var encoded: string = null; - - try { - encoded = encodeURIComponent(name); - } catch (error) { - console.error("tryEncode", "Error encoding:", name, error); - - encoded = null; - } - - return encoded; -} -*/ - -// Port of http://referencesource.microsoft.com/#System/compmod/microsoft/csharp/csharpcodeprovider.cs,7b5c20ff8d28dfa7 -function IsValidIdentifier(value: string): boolean { - // identifiers must be 1 char or longer - if (!value) { - return false; - } - - if (value.length > 512) { - return false; - } - - // Note: Disabling keyword check to err the side of lax validation. - // identifiers cannot be a keyword, unless they are escaped with an '@' - /* - if (value[0] !== "@") { - if (IsKeyword(value)) { - return false; - } - } else { - value = value.substring(1); - } - */ - - return IsValidLanguageIndependentIdentifier(value); -} - -/* -var keywords: string[][] = [ - // 2 characters - [ - "as", - "do", - "if", - "in", - "is", - ], - // 3 characters - [ - "for", - "int", - "new", - "out", - "ref", - "try", - ], - // 4 characters - [ - "base", - "bool", - "byte", - "case", - "char", - "else", - "enum", - "goto", - "lock", - "long", - "null", - "this", - "true", - "uint", - "void", - ], - // 5 characters - [ - "break", - "catch", - "class", - "const", - "event", - "false", - "fixed", - "float", - "sbyte", - "short", - "throw", - "ulong", - "using", - "while", - ], - // 6 characters - [ - "double", - "extern", - "object", - "params", - "public", - "return", - "sealed", - "sizeof", - "static", - "string", - "struct", - "switch", - "typeof", - "unsafe", - "ushort", - ], - // 7 characters - [ - "checked", - "decimal", - "default", - "finally", - "foreach", - "private", - "virtual", - ], - // 8 characters - [ - "abstract", - "continue", - "delegate", - "explicit", - "implicit", - "internal", - "operator", - "override", - "readonly", - "volatile", - ], - // 9 characters - [ - "__arglist", - "__makeref", - "__reftype", - "interface", - "namespace", - "protected", - "unchecked", - ], - // 10 characters - [ - "__refvalue", - "stackalloc", - ] -]; - -function IsKeyword(value: string): boolean { - var isKeyword: boolean = false; - var listCount: number = keywords.length; - - for (var i = 0; ((i < listCount) && !isKeyword); ++i) { - var list: string[] = keywords[i]; - var listKeywordCount: number = list.length; - - for (var j = 0; ((j < listKeywordCount) && !isKeyword); ++j) { - var keyword: string = list[j]; - - isKeyword = (value === keyword); - } - } - - return isKeyword; -} -*/ - -function IsValidLanguageIndependentIdentifier(value: string): boolean { - return IsValidTypeNameOrIdentifier(value, /* isTypeName */ false); -} - -var UnicodeCategory = { - // Uppercase Letter - Lu: /[A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԱ-ՖႠ-ჅḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώℂℇℋ-ℍℐ-ℒℕℙ-ℝℤΩℨK-ℭℰ-ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱯⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋA-Z]|\ud801[\udc00-\udc27]|\ud835[\udc00-\udc19\udc34-\udc4d\udc68-\udc81\udc9c\udc9e-\udc9f\udca2\udca5-\udca6\udca9-\udcac\udcae-\udcb5\udcd0-\udce9\udd04-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd38-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd6c-\udd85\udda0-\uddb9\uddd4-\udded\ude08-\ude21\ude3c-\ude55\ude70-\ude89\udea8-\udec0\udee2-\udefa\udf1c-\udf34\udf56-\udf6e\udf90-\udfa8\udfca]/, - // Lowercase Letter - Ll: /[a-zªµºß-öø-ÿāăąćĉċčďđēĕėęěĝğġģĥħĩīĭįıijĵķ-ĸĺļľŀłńņň-ʼnŋōŏőœŕŗřśŝşšţťŧũūŭůűųŵŷźżž-ƀƃƅƈƌ-ƍƒƕƙ-ƛƞơƣƥƨƪ-ƫƭưƴƶƹ-ƺƽ-ƿdžljnjǎǐǒǔǖǘǚǜ-ǝǟǡǣǥǧǩǫǭǯ-ǰdzǵǹǻǽǿȁȃȅȇȉȋȍȏȑȓȕȗșțȝȟȡȣȥȧȩȫȭȯȱȳ-ȹȼȿ-ɀɂɇɉɋɍɏ-ʓʕ-ʯͱͳͷͻ-ͽΐά-ώϐ-ϑϕ-ϗϙϛϝϟϡϣϥϧϩϫϭϯ-ϳϵϸϻ-ϼа-џѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿҁҋҍҏґғҕҗҙқҝҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӂӄӆӈӊӌӎ-ӏӑӓӕӗәӛӝӟӡӣӥӧөӫӭӯӱӳӵӷӹӻӽӿԁԃԅԇԉԋԍԏԑԓԕԗԙԛԝԟԡԣա-ևᴀ-ᴫᵢ-ᵷᵹ-ᶚḁḃḅḇḉḋḍḏḑḓḕḗḙḛḝḟḡḣḥḧḩḫḭḯḱḳḵḷḹḻḽḿṁṃṅṇṉṋṍṏṑṓṕṗṙṛṝṟṡṣṥṧṩṫṭṯṱṳṵṷṹṻṽṿẁẃẅẇẉẋẍẏẑẓẕ-ẝẟạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹỻỽỿ-ἇἐ-ἕἠ-ἧἰ-ἷὀ-ὅὐ-ὗὠ-ὧὰ-ώᾀ-ᾇᾐ-ᾗᾠ-ᾧᾰ-ᾴᾶ-ᾷιῂ-ῄῆ-ῇῐ-ΐῖ-ῗῠ-ῧῲ-ῴῶ-ῷⁱⁿℊℎ-ℏℓℯℴℹℼ-ℽⅆ-ⅉⅎↄⰰ-ⱞⱡⱥ-ⱦⱨⱪⱬⱱⱳ-ⱴⱶ-ⱼⲁⲃⲅⲇⲉⲋⲍⲏⲑⲓⲕⲗⲙⲛⲝⲟⲡⲣⲥⲧⲩⲫⲭⲯⲱⲳⲵⲷⲹⲻⲽⲿⳁⳃⳅⳇⳉⳋⳍⳏⳑⳓⳕⳗⳙⳛⳝⳟⳡⳣ-ⳤⴀ-ⴥꙁꙃꙅꙇꙉꙋꙍꙏꙑꙓꙕꙗꙙꙛꙝꙟꙣꙥꙧꙩꙫꙭꚁꚃꚅꚇꚉꚋꚍꚏꚑꚓꚕꚗꜣꜥꜧꜩꜫꜭꜯ-ꜱꜳꜵꜷꜹꜻꜽꜿꝁꝃꝅꝇꝉꝋꝍꝏꝑꝓꝕꝗꝙꝛꝝꝟꝡꝣꝥꝧꝩꝫꝭꝯꝱ-ꝸꝺꝼꝿꞁꞃꞅꞇꞌff-stﬓ-ﬗa-z]|\ud801[\udc28-\udc4f]|\ud835[\udc1a-\udc33\udc4e-\udc54\udc56-\udc67\udc82-\udc9b\udcb6-\udcb9\udcbb\udcbd-\udcc3\udcc5-\udccf\udcea-\udd03\udd1e-\udd37\udd52-\udd6b\udd86-\udd9f\uddba-\uddd3\uddee-\ude07\ude22-\ude3b\ude56-\ude6f\ude8a-\udea5\udec2-\udeda\udedc-\udee1\udefc-\udf14\udf16-\udf1b\udf36-\udf4e\udf50-\udf55\udf70-\udf88\udf8a-\udf8f\udfaa-\udfc2\udfc4-\udfc9\udfcb]/, - // Titlecase Letter - Lt: /[DžLjNjDzᾈ-ᾏᾘ-ᾟᾨ-ᾯᾼῌῼ]/, - // Modifier/Number Letter - Lm: /[ʰ-ˁˆ-ˑˠ-ˤˬˮʹͺՙـۥ-ۦߴ-ߵߺॱๆໆჼៗᡃᱸ-ᱽᴬ-ᵡᵸᶛ-ᶿₐ-ₔⱽⵯⸯ々〱-〵〻ゝ-ゞー-ヾꀕꘌꙿꜗ-ꜟꝰꞈー゙-゚]/, - // Other Letter - Lo: /[ƻǀ-ǃʔא-תװ-ײء-ؿف-يٮ-ٯٱ-ۓەۮ-ۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪऄ-हऽॐक़-ॡॲॻ-ॿঅ-ঌএ-ঐও-নপ-রলশ-হঽৎড়-ঢ়য়-ৡৰ-ৱਅ-ਊਏ-ਐਓ-ਨਪ-ਰਲ-ਲ਼ਵ-ਸ਼ਸ-ਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલ-ળવ-હઽૐૠ-ૡଅ-ଌଏ-ଐଓ-ନପ-ରଲ-ଳଵ-ହଽଡ଼-ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கங-சஜஞ-டண-தந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘ-ౙౠ-ౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠ-ೡഅ-ഌഎ-ഐഒ-നപ-ഹഽൠ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะา-ำเ-ๅກ-ຂຄງ-ຈຊຍດ-ທນ-ຟມ-ຣລວສ-ຫອ-ະາ-ຳຽເ-ໄໜ-ໝༀཀ-ཇཉ-ཬྈ-ྋက-ဪဿၐ-ၕၚ-ၝၡၥ-ၦၮ-ၰၵ-ႁႎა-ჺᄀ-ᅙᅟ-ᆢᆨ-ᇹሀ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙶᚁ-ᚚᚠ-ᛪᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៜᠠ-ᡂᡄ-ᡷᢀ-ᢨᢪᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦩᧁ-ᧇᨀ-ᨖᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮ-ᮯᰀ-ᰣᱍ-ᱏᱚ-ᱷℵ-ℸⴰ-ⵥⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞ〆〼ぁ-ゖゟァ-ヺヿㄅ-ㄭㄱ-ㆎㆠ-ㆷㇰ-ㇿ㐀-䶵一-鿃ꀀ-ꀔꀖ-ꒌꔀ-ꘋꘐ-ꘟꘪ-ꘫꙮꟻ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꤊ-ꤥꤰ-ꥆꨀ-ꨨꩀ-ꩂꩄ-ꩋ가-힣豈-鶴侮-頻並-龎יִײַ-ﬨשׁ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼヲ-ッア-ンᅠ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]|[\ud840-\ud868][\udc00-\udfff]|\ud800[\udc00-\udc0b\udc0d-\udc26\udc28-\udc3a\udc3c-\udc3d\udc3f-\udc4d\udc50-\udc5d\udc80-\udcfa\ude80-\ude9c\udea0-\uded0\udf00-\udf1e\udf30-\udf40\udf42-\udf49\udf80-\udf9d\udfa0-\udfc3\udfc8-\udfcf]|\ud801[\udc50-\udc9d]|\ud802[\udc00-\udc05\udc08\udc0a-\udc35\udc37-\udc38\udc3c\udc3f\udd00-\udd15\udd20-\udd39\ude00\ude10-\ude13\ude15-\ude17\ude19-\ude33]|\ud808[\udc00-\udf6e]|\ud869[\udc00-\uded6]|\ud87e[\udc00-\ude1d]/, - // Non Spacing Mark - Mn: /[\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0901-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0954\u0962-\u0963\u0981\u09bc\u09c1-\u09c4\u09cd\u09e2-\u09e3\u0a01-\u0a02\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a70-\u0a71\u0a75\u0a81-\u0a82\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd\u0ae2-\u0ae3\u0b01\u0b3c\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b62-\u0b63\u0b82\u0bc0\u0bcd\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0cbc\u0cbf\u0cc6\u0ccc-\u0ccd\u0ce2-\u0ce3\u0d41-\u0d44\u0d4d\u0d62-\u0d63\u0dca\u0dd2-\u0dd4\u0dd6\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108d\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193b\u1a17-\u1a18\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1c2c-\u1c33\u1c36-\u1c37\u1dc0-\u1de6\u1dfe-\u1dff\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2de0-\u2dff\u302a-\u302f\u3099-\u309a\ua66f\ua67c-\ua67d\ua802\ua806\ua80b\ua825-\ua826\ua8c4\ua926-\ua92d\ua947-\ua951\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43\uaa4c\ufb1e\ufe00-\ufe0f\ufe20-\ufe26]|\ud800\uddfd|\ud802[\ude01-\ude03\ude05-\ude06\ude0c-\ude0f\ude38-\ude3a\ude3f]|\ud834[\udd67-\udd69\udd7b-\udd82\udd85-\udd8b\uddaa-\uddad\ude42-\ude44]|\udb40[\udd00-\uddef]/, - // Spacing Combining Mark - Mc: /[\u0903\u093e-\u0940\u0949-\u094c\u0982-\u0983\u09be-\u09c0\u09c7-\u09c8\u09cb-\u09cc\u09d7\u0a03\u0a3e-\u0a40\u0a83\u0abe-\u0ac0\u0ac9\u0acb-\u0acc\u0b02-\u0b03\u0b3e\u0b40\u0b47-\u0b48\u0b4b-\u0b4c\u0b57\u0bbe-\u0bbf\u0bc1-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcc\u0bd7\u0c01-\u0c03\u0c41-\u0c44\u0c82-\u0c83\u0cbe\u0cc0-\u0cc4\u0cc7-\u0cc8\u0cca-\u0ccb\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d40\u0d46-\u0d48\u0d4a-\u0d4c\u0d57\u0d82-\u0d83\u0dcf-\u0dd1\u0dd8-\u0ddf\u0df2-\u0df3\u0f3e-\u0f3f\u0f7f\u102b-\u102c\u1031\u1038\u103b-\u103c\u1056-\u1057\u1062-\u1064\u1067-\u106d\u1083-\u1084\u1087-\u108c\u108f\u17b6\u17be-\u17c5\u17c7-\u17c8\u1923-\u1926\u1929-\u192b\u1930-\u1931\u1933-\u1938\u19b0-\u19c0\u19c8-\u19c9\u1a19-\u1a1b\u1b04\u1b35\u1b3b\u1b3d-\u1b41\u1b43-\u1b44\u1b82\u1ba1\u1ba6-\u1ba7\u1baa\u1c24-\u1c2b\u1c34-\u1c35\ua823-\ua824\ua827\ua880-\ua881\ua8b4-\ua8c3\ua952-\ua953\uaa2f-\uaa30\uaa33-\uaa34\uaa4d]|\ud834[\udd65-\udd66\udd6d-\udd72]/, - // Connector Punctuation - Pc: /[_‿-⁀⁔︳-︴﹍-﹏_]/, - // Decimal Digit Number - Nd: /[0-9٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯๐-๙໐-໙༠-༩၀-၉႐-႙០-៩᠐-᠙᥆-᥏᧐-᧙᭐-᭙᮰-᮹᱀-᱉᱐-᱙꘠-꘩꣐-꣙꤀-꤉꩐-꩙0-9]|\ud801[\udca0-\udca9]|\ud835[\udfce-\udfff]/ -}; - -function IsValidTypeNameOrIdentifier(value: string, isTypeName: boolean): boolean { - var nextMustBeStartChar: boolean = true; - - if (!value.length) { - return false; - } - - // each char must be Lu, Ll, Lt, Lm, Lo, Nd, Mn, Mc, Pc - for (var i = 0; i < value.length; i++) { - var ch: string = value[i]; - - if ( - UnicodeCategory.Lu.test(ch) || - UnicodeCategory.Ll.test(ch) || - UnicodeCategory.Lt.test(ch) || - UnicodeCategory.Lm.test(ch) || - UnicodeCategory.Lo.test(ch) - ) { - nextMustBeStartChar = false; - } else if ( - UnicodeCategory.Mn.test(ch) || - UnicodeCategory.Mc.test(ch) || - UnicodeCategory.Pc.test(ch) || - UnicodeCategory.Nd.test(ch) - ) { - // Underscore is a valid starting character, even though it is a ConnectorPunctuation. - if (nextMustBeStartChar && ch !== "_") { - return false; - } - - nextMustBeStartChar = false; - } else { - // We only check the special Type chars for type names. - if (!isTypeName) { - return false; - } else { - var ref: { nextMustBeStartChar: boolean } = { nextMustBeStartChar: nextMustBeStartChar }; - var isSpecialTypeChar = IsSpecialTypeChar(ch, ref); - - nextMustBeStartChar = ref.nextMustBeStartChar; - - if (!isSpecialTypeChar) { - return false; - } - } - } - } - - return true; -} - -// This can be a special character like a separator that shows up in a type name -// This is an odd set of characters. Some come from characters that are allowed by C++, like < and >. -// Others are characters that are specified in the type and assembly name grammer. -function IsSpecialTypeChar(ch: string, ref: { nextMustBeStartChar: boolean }): boolean { - switch (ch) { - case ":": - case ".": - case "$": - case "+": - case "<": - case ">": - case "-": - case "[": - case "]": - case ",": - case "&": - case "*": - ref.nextMustBeStartChar = true; - return true; - case "`": - return true; - } - return false; -} +/* Constants */ +var MaximumNameLength = 255; +var noHelp = ""; +var detailedHelp = "Enter a name up to 255 characters in size. Most valid C# identifiers are allowed."; // localize + +export interface IValidationResult { + isInvalid: boolean; + help: string; +} + +export function validate(name: string): IValidationResult { + var help: string = noHelp; + // Note: Disabling encoding check to err the side of lax validation. + // A valid property name should also be XML-serializable. + // Hence, only allowing names that don't require special encoding for network transmission. + // var encoded: string = tryEncode(name); + // var success: boolean = (name === encoded); + var success: boolean = true; + + if (success) { + success = name.length <= MaximumNameLength; + if (success) { + success = IsValidIdentifier(name); + } + } + + if (!success) { + help = detailedHelp; + } + + return { isInvalid: !success, help: help }; +} + +/* +function tryEncode(name: string): string { + var encoded: string = null; + + try { + encoded = encodeURIComponent(name); + } catch (error) { + console.error("tryEncode", "Error encoding:", name, error); + + encoded = null; + } + + return encoded; +} +*/ + +// Port of http://referencesource.microsoft.com/#System/compmod/microsoft/csharp/csharpcodeprovider.cs,7b5c20ff8d28dfa7 +function IsValidIdentifier(value: string): boolean { + // identifiers must be 1 char or longer + if (!value) { + return false; + } + + if (value.length > 512) { + return false; + } + + // Note: Disabling keyword check to err the side of lax validation. + // identifiers cannot be a keyword, unless they are escaped with an '@' + /* + if (value[0] !== "@") { + if (IsKeyword(value)) { + return false; + } + } else { + value = value.substring(1); + } + */ + + return IsValidLanguageIndependentIdentifier(value); +} + +/* +var keywords: string[][] = [ + // 2 characters + [ + "as", + "do", + "if", + "in", + "is", + ], + // 3 characters + [ + "for", + "int", + "new", + "out", + "ref", + "try", + ], + // 4 characters + [ + "base", + "bool", + "byte", + "case", + "char", + "else", + "enum", + "goto", + "lock", + "long", + "null", + "this", + "true", + "uint", + "void", + ], + // 5 characters + [ + "break", + "catch", + "class", + "const", + "event", + "false", + "fixed", + "float", + "sbyte", + "short", + "throw", + "ulong", + "using", + "while", + ], + // 6 characters + [ + "double", + "extern", + "object", + "params", + "public", + "return", + "sealed", + "sizeof", + "static", + "string", + "struct", + "switch", + "typeof", + "unsafe", + "ushort", + ], + // 7 characters + [ + "checked", + "decimal", + "default", + "finally", + "foreach", + "private", + "virtual", + ], + // 8 characters + [ + "abstract", + "continue", + "delegate", + "explicit", + "implicit", + "internal", + "operator", + "override", + "readonly", + "volatile", + ], + // 9 characters + [ + "__arglist", + "__makeref", + "__reftype", + "interface", + "namespace", + "protected", + "unchecked", + ], + // 10 characters + [ + "__refvalue", + "stackalloc", + ] +]; + +function IsKeyword(value: string): boolean { + var isKeyword: boolean = false; + var listCount: number = keywords.length; + + for (var i = 0; ((i < listCount) && !isKeyword); ++i) { + var list: string[] = keywords[i]; + var listKeywordCount: number = list.length; + + for (var j = 0; ((j < listKeywordCount) && !isKeyword); ++j) { + var keyword: string = list[j]; + + isKeyword = (value === keyword); + } + } + + return isKeyword; +} +*/ + +function IsValidLanguageIndependentIdentifier(value: string): boolean { + return IsValidTypeNameOrIdentifier(value, /* isTypeName */ false); +} + +var UnicodeCategory = { + // Uppercase Letter + Lu: /[A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԱ-ՖႠ-ჅḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώℂℇℋ-ℍℐ-ℒℕℙ-ℝℤΩℨK-ℭℰ-ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱯⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋA-Z]|\ud801[\udc00-\udc27]|\ud835[\udc00-\udc19\udc34-\udc4d\udc68-\udc81\udc9c\udc9e-\udc9f\udca2\udca5-\udca6\udca9-\udcac\udcae-\udcb5\udcd0-\udce9\udd04-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd38-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd6c-\udd85\udda0-\uddb9\uddd4-\udded\ude08-\ude21\ude3c-\ude55\ude70-\ude89\udea8-\udec0\udee2-\udefa\udf1c-\udf34\udf56-\udf6e\udf90-\udfa8\udfca]/, + // Lowercase Letter + Ll: /[a-zªµºß-öø-ÿāăąćĉċčďđēĕėęěĝğġģĥħĩīĭįıijĵķ-ĸĺļľŀłńņň-ʼnŋōŏőœŕŗřśŝşšţťŧũūŭůűųŵŷźżž-ƀƃƅƈƌ-ƍƒƕƙ-ƛƞơƣƥƨƪ-ƫƭưƴƶƹ-ƺƽ-ƿdžljnjǎǐǒǔǖǘǚǜ-ǝǟǡǣǥǧǩǫǭǯ-ǰdzǵǹǻǽǿȁȃȅȇȉȋȍȏȑȓȕȗșțȝȟȡȣȥȧȩȫȭȯȱȳ-ȹȼȿ-ɀɂɇɉɋɍɏ-ʓʕ-ʯͱͳͷͻ-ͽΐά-ώϐ-ϑϕ-ϗϙϛϝϟϡϣϥϧϩϫϭϯ-ϳϵϸϻ-ϼа-џѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿҁҋҍҏґғҕҗҙқҝҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӂӄӆӈӊӌӎ-ӏӑӓӕӗәӛӝӟӡӣӥӧөӫӭӯӱӳӵӷӹӻӽӿԁԃԅԇԉԋԍԏԑԓԕԗԙԛԝԟԡԣա-ևᴀ-ᴫᵢ-ᵷᵹ-ᶚḁḃḅḇḉḋḍḏḑḓḕḗḙḛḝḟḡḣḥḧḩḫḭḯḱḳḵḷḹḻḽḿṁṃṅṇṉṋṍṏṑṓṕṗṙṛṝṟṡṣṥṧṩṫṭṯṱṳṵṷṹṻṽṿẁẃẅẇẉẋẍẏẑẓẕ-ẝẟạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹỻỽỿ-ἇἐ-ἕἠ-ἧἰ-ἷὀ-ὅὐ-ὗὠ-ὧὰ-ώᾀ-ᾇᾐ-ᾗᾠ-ᾧᾰ-ᾴᾶ-ᾷιῂ-ῄῆ-ῇῐ-ΐῖ-ῗῠ-ῧῲ-ῴῶ-ῷⁱⁿℊℎ-ℏℓℯℴℹℼ-ℽⅆ-ⅉⅎↄⰰ-ⱞⱡⱥ-ⱦⱨⱪⱬⱱⱳ-ⱴⱶ-ⱼⲁⲃⲅⲇⲉⲋⲍⲏⲑⲓⲕⲗⲙⲛⲝⲟⲡⲣⲥⲧⲩⲫⲭⲯⲱⲳⲵⲷⲹⲻⲽⲿⳁⳃⳅⳇⳉⳋⳍⳏⳑⳓⳕⳗⳙⳛⳝⳟⳡⳣ-ⳤⴀ-ⴥꙁꙃꙅꙇꙉꙋꙍꙏꙑꙓꙕꙗꙙꙛꙝꙟꙣꙥꙧꙩꙫꙭꚁꚃꚅꚇꚉꚋꚍꚏꚑꚓꚕꚗꜣꜥꜧꜩꜫꜭꜯ-ꜱꜳꜵꜷꜹꜻꜽꜿꝁꝃꝅꝇꝉꝋꝍꝏꝑꝓꝕꝗꝙꝛꝝꝟꝡꝣꝥꝧꝩꝫꝭꝯꝱ-ꝸꝺꝼꝿꞁꞃꞅꞇꞌff-stﬓ-ﬗa-z]|\ud801[\udc28-\udc4f]|\ud835[\udc1a-\udc33\udc4e-\udc54\udc56-\udc67\udc82-\udc9b\udcb6-\udcb9\udcbb\udcbd-\udcc3\udcc5-\udccf\udcea-\udd03\udd1e-\udd37\udd52-\udd6b\udd86-\udd9f\uddba-\uddd3\uddee-\ude07\ude22-\ude3b\ude56-\ude6f\ude8a-\udea5\udec2-\udeda\udedc-\udee1\udefc-\udf14\udf16-\udf1b\udf36-\udf4e\udf50-\udf55\udf70-\udf88\udf8a-\udf8f\udfaa-\udfc2\udfc4-\udfc9\udfcb]/, + // Titlecase Letter + Lt: /[DžLjNjDzᾈ-ᾏᾘ-ᾟᾨ-ᾯᾼῌῼ]/, + // Modifier/Number Letter + Lm: /[ʰ-ˁˆ-ˑˠ-ˤˬˮʹͺՙـۥ-ۦߴ-ߵߺॱๆໆჼៗᡃᱸ-ᱽᴬ-ᵡᵸᶛ-ᶿₐ-ₔⱽⵯⸯ々〱-〵〻ゝ-ゞー-ヾꀕꘌꙿꜗ-ꜟꝰꞈー゙-゚]/, + // Other Letter + Lo: /[ƻǀ-ǃʔא-תװ-ײء-ؿف-يٮ-ٯٱ-ۓەۮ-ۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪऄ-हऽॐक़-ॡॲॻ-ॿঅ-ঌএ-ঐও-নপ-রলশ-হঽৎড়-ঢ়য়-ৡৰ-ৱਅ-ਊਏ-ਐਓ-ਨਪ-ਰਲ-ਲ਼ਵ-ਸ਼ਸ-ਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલ-ળવ-હઽૐૠ-ૡଅ-ଌଏ-ଐଓ-ନପ-ରଲ-ଳଵ-ହଽଡ଼-ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கங-சஜஞ-டண-தந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘ-ౙౠ-ౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠ-ೡഅ-ഌഎ-ഐഒ-നപ-ഹഽൠ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะา-ำเ-ๅກ-ຂຄງ-ຈຊຍດ-ທນ-ຟມ-ຣລວສ-ຫອ-ະາ-ຳຽເ-ໄໜ-ໝༀཀ-ཇཉ-ཬྈ-ྋက-ဪဿၐ-ၕၚ-ၝၡၥ-ၦၮ-ၰၵ-ႁႎა-ჺᄀ-ᅙᅟ-ᆢᆨ-ᇹሀ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙶᚁ-ᚚᚠ-ᛪᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៜᠠ-ᡂᡄ-ᡷᢀ-ᢨᢪᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦩᧁ-ᧇᨀ-ᨖᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮ-ᮯᰀ-ᰣᱍ-ᱏᱚ-ᱷℵ-ℸⴰ-ⵥⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞ〆〼ぁ-ゖゟァ-ヺヿㄅ-ㄭㄱ-ㆎㆠ-ㆷㇰ-ㇿ㐀-䶵一-鿃ꀀ-ꀔꀖ-ꒌꔀ-ꘋꘐ-ꘟꘪ-ꘫꙮꟻ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꤊ-ꤥꤰ-ꥆꨀ-ꨨꩀ-ꩂꩄ-ꩋ가-힣豈-鶴侮-頻並-龎יִײַ-ﬨשׁ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼヲ-ッア-ンᅠ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]|[\ud840-\ud868][\udc00-\udfff]|\ud800[\udc00-\udc0b\udc0d-\udc26\udc28-\udc3a\udc3c-\udc3d\udc3f-\udc4d\udc50-\udc5d\udc80-\udcfa\ude80-\ude9c\udea0-\uded0\udf00-\udf1e\udf30-\udf40\udf42-\udf49\udf80-\udf9d\udfa0-\udfc3\udfc8-\udfcf]|\ud801[\udc50-\udc9d]|\ud802[\udc00-\udc05\udc08\udc0a-\udc35\udc37-\udc38\udc3c\udc3f\udd00-\udd15\udd20-\udd39\ude00\ude10-\ude13\ude15-\ude17\ude19-\ude33]|\ud808[\udc00-\udf6e]|\ud869[\udc00-\uded6]|\ud87e[\udc00-\ude1d]/, + // Non Spacing Mark + Mn: /[\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0901-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0954\u0962-\u0963\u0981\u09bc\u09c1-\u09c4\u09cd\u09e2-\u09e3\u0a01-\u0a02\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a70-\u0a71\u0a75\u0a81-\u0a82\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd\u0ae2-\u0ae3\u0b01\u0b3c\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b62-\u0b63\u0b82\u0bc0\u0bcd\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0cbc\u0cbf\u0cc6\u0ccc-\u0ccd\u0ce2-\u0ce3\u0d41-\u0d44\u0d4d\u0d62-\u0d63\u0dca\u0dd2-\u0dd4\u0dd6\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108d\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193b\u1a17-\u1a18\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1c2c-\u1c33\u1c36-\u1c37\u1dc0-\u1de6\u1dfe-\u1dff\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2de0-\u2dff\u302a-\u302f\u3099-\u309a\ua66f\ua67c-\ua67d\ua802\ua806\ua80b\ua825-\ua826\ua8c4\ua926-\ua92d\ua947-\ua951\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43\uaa4c\ufb1e\ufe00-\ufe0f\ufe20-\ufe26]|\ud800\uddfd|\ud802[\ude01-\ude03\ude05-\ude06\ude0c-\ude0f\ude38-\ude3a\ude3f]|\ud834[\udd67-\udd69\udd7b-\udd82\udd85-\udd8b\uddaa-\uddad\ude42-\ude44]|\udb40[\udd00-\uddef]/, + // Spacing Combining Mark + Mc: /[\u0903\u093e-\u0940\u0949-\u094c\u0982-\u0983\u09be-\u09c0\u09c7-\u09c8\u09cb-\u09cc\u09d7\u0a03\u0a3e-\u0a40\u0a83\u0abe-\u0ac0\u0ac9\u0acb-\u0acc\u0b02-\u0b03\u0b3e\u0b40\u0b47-\u0b48\u0b4b-\u0b4c\u0b57\u0bbe-\u0bbf\u0bc1-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcc\u0bd7\u0c01-\u0c03\u0c41-\u0c44\u0c82-\u0c83\u0cbe\u0cc0-\u0cc4\u0cc7-\u0cc8\u0cca-\u0ccb\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d40\u0d46-\u0d48\u0d4a-\u0d4c\u0d57\u0d82-\u0d83\u0dcf-\u0dd1\u0dd8-\u0ddf\u0df2-\u0df3\u0f3e-\u0f3f\u0f7f\u102b-\u102c\u1031\u1038\u103b-\u103c\u1056-\u1057\u1062-\u1064\u1067-\u106d\u1083-\u1084\u1087-\u108c\u108f\u17b6\u17be-\u17c5\u17c7-\u17c8\u1923-\u1926\u1929-\u192b\u1930-\u1931\u1933-\u1938\u19b0-\u19c0\u19c8-\u19c9\u1a19-\u1a1b\u1b04\u1b35\u1b3b\u1b3d-\u1b41\u1b43-\u1b44\u1b82\u1ba1\u1ba6-\u1ba7\u1baa\u1c24-\u1c2b\u1c34-\u1c35\ua823-\ua824\ua827\ua880-\ua881\ua8b4-\ua8c3\ua952-\ua953\uaa2f-\uaa30\uaa33-\uaa34\uaa4d]|\ud834[\udd65-\udd66\udd6d-\udd72]/, + // Connector Punctuation + Pc: /[_‿-⁀⁔︳-︴﹍-﹏_]/, + // Decimal Digit Number + Nd: /[0-9٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯๐-๙໐-໙༠-༩၀-၉႐-႙០-៩᠐-᠙᥆-᥏᧐-᧙᭐-᭙᮰-᮹᱀-᱉᱐-᱙꘠-꘩꣐-꣙꤀-꤉꩐-꩙0-9]|\ud801[\udca0-\udca9]|\ud835[\udfce-\udfff]/, +}; + +function IsValidTypeNameOrIdentifier(value: string, isTypeName: boolean): boolean { + var nextMustBeStartChar: boolean = true; + + if (!value.length) { + return false; + } + + // each char must be Lu, Ll, Lt, Lm, Lo, Nd, Mn, Mc, Pc + for (var i = 0; i < value.length; i++) { + var ch: string = value[i]; + + if ( + UnicodeCategory.Lu.test(ch) || + UnicodeCategory.Ll.test(ch) || + UnicodeCategory.Lt.test(ch) || + UnicodeCategory.Lm.test(ch) || + UnicodeCategory.Lo.test(ch) + ) { + nextMustBeStartChar = false; + } else if ( + UnicodeCategory.Mn.test(ch) || + UnicodeCategory.Mc.test(ch) || + UnicodeCategory.Pc.test(ch) || + UnicodeCategory.Nd.test(ch) + ) { + // Underscore is a valid starting character, even though it is a ConnectorPunctuation. + if (nextMustBeStartChar && ch !== "_") { + return false; + } + + nextMustBeStartChar = false; + } else { + // We only check the special Type chars for type names. + if (!isTypeName) { + return false; + } else { + var ref: { nextMustBeStartChar: boolean } = { nextMustBeStartChar: nextMustBeStartChar }; + var isSpecialTypeChar = IsSpecialTypeChar(ch, ref); + + nextMustBeStartChar = ref.nextMustBeStartChar; + + if (!isSpecialTypeChar) { + return false; + } + } + } + } + + return true; +} + +// This can be a special character like a separator that shows up in a type name +// This is an odd set of characters. Some come from characters that are allowed by C++, like < and >. +// Others are characters that are specified in the type and assembly name grammer. +function IsSpecialTypeChar(ch: string, ref: { nextMustBeStartChar: boolean }): boolean { + switch (ch) { + case ":": + case ".": + case "$": + case "+": + case "<": + case ">": + case "-": + case "[": + case "]": + case ",": + case "&": + case "*": + ref.nextMustBeStartChar = true; + return true; + case "`": + return true; + } + return false; +} diff --git a/src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts b/src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts index 8872b16fb..d1eca51a4 100644 --- a/src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts +++ b/src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts @@ -1,23 +1,23 @@ -export var Int32 = { - Min: -2147483648, - Max: 2147483647 -}; - -export var Int64 = { - Min: -9223372036854775808, - Max: 9223372036854775807 -}; - -var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d"; -var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?"; -var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d"; - -export var ValidationRegExp = { - Guid: /^[{(]?[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$/i, - Float: /^[+-]?\d+(\.\d+)?(e[+-]?\d+)?$/i, - // OData seems to require an "L" suffix for Int64 values, yet Azure Storage errors out with it. See http://www.odata.org/documentation/odata-version-2-0/overview/ - Integer: /^[+-]?\d+$/i, // Used for both Int32 and Int64 values - Boolean: /^"?(true|false)"?$/i, - DateTime: new RegExp(`^${yearMonthDay}${timeOfDay}${timeZone}$`), - PrimaryKey: /^[^/\\#?\u0000-\u001F\u007F-\u009F]*$/ -}; +export var Int32 = { + Min: -2147483648, + Max: 2147483647, +}; + +export var Int64 = { + Min: -9223372036854775808, + Max: 9223372036854775807, +}; + +var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d"; +var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?"; +var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d"; + +export var ValidationRegExp = { + Guid: /^[{(]?[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$/i, + Float: /^[+-]?\d+(\.\d+)?(e[+-]?\d+)?$/i, + // OData seems to require an "L" suffix for Int64 values, yet Azure Storage errors out with it. See http://www.odata.org/documentation/odata-version-2-0/overview/ + Integer: /^[+-]?\d+$/i, // Used for both Int32 and Int64 values + Boolean: /^"?(true|false)"?$/i, + DateTime: new RegExp(`^${yearMonthDay}${timeOfDay}${timeZone}$`), + PrimaryKey: /^[^/\\#?\u0000-\u001F\u007F-\u009F]*$/, +}; diff --git a/src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts b/src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts index 70c4d5223..f2a2190c2 100644 --- a/src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts +++ b/src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts @@ -1,341 +1,341 @@ -import * as Utilities from "../../../Tables/Utilities"; -import * as StorageExplorerConstants from "../../../Tables/Constants"; -import * as EntityPropertyValidationCommon from "./EntityPropertyValidationCommon"; - -interface IValidationResult { - isInvalid: boolean; - help: string; -} - -interface IValueValidator { - validate: (value: string) => IValidationResult; - parseValue: (value: string) => any; -} - -/* Constants */ -var noHelp: string = ""; -var MaximumStringLength = 64 * 1024; // 64 KB -var MaximumRequiredStringLength = 1 * 1024; // 1 KB - -class ValueValidator implements IValueValidator { - public validate(value: string): IValidationResult { - // throw new Errors.NotImplementedFunctionError("ValueValidator.validate"); - return null; - } - - public parseValue(value: string): any { - return value; // default pass-thru implementation - } -} - -class KeyValidator implements ValueValidator { - private static detailedHelp = "Enter a string ('/', '\\', '#', '?' and control characters not allowed)."; // Localize - - public validate(value: string): IValidationResult { - if ( - value == null || - value.trim().length === 0 || - EntityPropertyValidationCommon.ValidationRegExp.PrimaryKey.test(value) - ) { - return { isInvalid: false, help: noHelp }; - } else { - return { isInvalid: true, help: KeyValidator.detailedHelp }; - } - } - - public parseValue(value: string): string { - return value; - } -} - -class BooleanValueValidator extends ValueValidator { - private detailedHelp = "Enter true or false."; // localize - - public validate(value: string): IValidationResult { - var success: boolean = false; - var help: string = noHelp; - - if (value) { - success = EntityPropertyValidationCommon.ValidationRegExp.Boolean.test(value); - } - - if (!success) { - help = this.detailedHelp; - } - - return { isInvalid: !success, help: help }; - } - - public parseValue(value: string): boolean { - // OData seems to require lowercase boolean values, see http://www.odata.org/documentation/odata-version-2-0/overview/ - return value.toString().toLowerCase() === "true"; - } -} - -class DateTimeValueValidator extends ValueValidator { - private detailedHelp = "Enter a date and time."; // localize - - public validate(value: string): IValidationResult { - var success: boolean = false; - var help: string = noHelp; - - if (value) { - // Try to parse the value to see if it is a valid date string - var parsed: number = Date.parse(value); - - success = !isNaN(parsed); - } - - if (!success) { - help = this.detailedHelp; - } - - return { isInvalid: !success, help: help }; - } - - public parseValue(value: string): Date { - var millisecondTime = Date.parse(value); - var parsed: Date = new Date(millisecondTime); - - return parsed; - } -} - -class DoubleValueValidator extends ValueValidator { - private detailedHelp = "Enter a 64-bit floating point value."; // localize - - public validate(value: string): IValidationResult { - var success: boolean = false; - var help: string = noHelp; - - if (value) { - success = EntityPropertyValidationCommon.ValidationRegExp.Float.test(value); - } - - if (!success) { - help = this.detailedHelp; - } - - return { isInvalid: !success, help: help }; - } - - public parseValue(value: string): number { - return parseFloat(value); - } -} - -class GuidValueValidator extends ValueValidator { - private detailedHelp = "Enter a 16-byte (128-bit) GUID value."; // localize - - public validate(value: string): IValidationResult { - var success: boolean = false; - var help: string = noHelp; - - if (value) { - success = EntityPropertyValidationCommon.ValidationRegExp.Guid.test(value); - } - - if (!success) { - help = this.detailedHelp; - } - - return { isInvalid: !success, help: help }; - } -} - -class IntegerValueValidator extends ValueValidator { - private detailedInt32Help = "Enter a signed 32-bit integer."; // localize - private detailedInt64Help = "Enter a signed 64-bit integer, in the range (-2^53 - 1, 2^53 - 1)."; // localize - - private isInt64: boolean; - - constructor(isInt64: boolean = true) { - super(); - - this.isInt64 = isInt64; - } - - public validate(value: string): IValidationResult { - var success: boolean = false; - var help: string = noHelp; - - if (value) { - success = EntityPropertyValidationCommon.ValidationRegExp.Integer.test(value) && Utilities.isSafeInteger(value); - if (success) { - var intValue = parseInt(value, 10); - - success = !isNaN(intValue); - if (success && !this.isInt64) { - success = - EntityPropertyValidationCommon.Int32.Min <= intValue && - intValue <= EntityPropertyValidationCommon.Int32.Max; - } - } - } - - if (!success) { - help = this.isInt64 ? this.detailedInt64Help : this.detailedInt32Help; - } - - return { isInvalid: !success, help: help }; - } - - public parseValue(value: string): number { - return parseInt(value, 10); - } -} - -// Allow all values for string type, unless the property is required, in which case an empty string is invalid. -class StringValidator extends ValueValidator { - private detailedHelp = "Enter a value up to 64 KB in size."; // localize - private isRequiredHelp = "Enter a value up to 1 KB in size."; // localize - private emptyStringHelp = "Empty string."; // localize - private isRequired: boolean; - - constructor(isRequired: boolean) { - super(); - - this.isRequired = isRequired; - } - - public validate(value: string): IValidationResult { - var help: string = this.isRequired ? this.isRequiredHelp : this.detailedHelp; - if (value === null) { - return { isInvalid: false, help: help }; - } - // Ensure we validate the string projection of value. - value = String(value); - - var success = true; - - if (success) { - success = value.length <= (this.isRequired ? MaximumRequiredStringLength : MaximumStringLength); - } - - if (success && this.isRequired) { - help = value ? noHelp : this.emptyStringHelp; - } - - return { isInvalid: !success, help: help }; - } - - public parseValue(value: string): string { - return String(value); // Ensure value is converted to string. - } -} - -class NotSupportedValidator extends ValueValidator { - private type: string; - - constructor(type: string) { - super(); - - this.type = type; - } - - public validate(ignoredValue: string): IValidationResult { - //throw new Errors.NotSupportedError(this.getMessage()); - return null; - } - - public parseValue(ignoredValue: string): any { - //throw new Errors.NotSupportedError(this.getMessage()); - return null; - } - - private getMessage(): string { - return "Properties of type " + this.type + " are not supported."; - } -} - -class PropertyValidatorFactory { - public getValidator(type: string, isRequired: boolean) { - var validator: IValueValidator = null; - - // TODO classify rest of Cassandra types/create validators for them - switch (type) { - case StorageExplorerConstants.TableType.Boolean: - case StorageExplorerConstants.CassandraType.Boolean: - validator = new BooleanValueValidator(); - break; - case StorageExplorerConstants.TableType.DateTime: - validator = new DateTimeValueValidator(); - break; - case StorageExplorerConstants.TableType.Double: - case StorageExplorerConstants.CassandraType.Decimal: - case StorageExplorerConstants.CassandraType.Double: - case StorageExplorerConstants.CassandraType.Float: - validator = new DoubleValueValidator(); - break; - case StorageExplorerConstants.TableType.Guid: - case StorageExplorerConstants.CassandraType.Uuid: - validator = new GuidValueValidator(); - break; - case StorageExplorerConstants.TableType.Int32: - case StorageExplorerConstants.CassandraType.Int: - // TODO create separate validators for smallint and tinyint - case StorageExplorerConstants.CassandraType.Smallint: - case StorageExplorerConstants.CassandraType.Tinyint: - validator = new IntegerValueValidator(/* isInt64 */ false); - break; - case StorageExplorerConstants.TableType.Int64: - case StorageExplorerConstants.CassandraType.Bigint: - case StorageExplorerConstants.CassandraType.Varint: - validator = new IntegerValueValidator(/* isInt64 */ true); - break; - case StorageExplorerConstants.TableType.String: - case StorageExplorerConstants.CassandraType.Text: - case StorageExplorerConstants.CassandraType.Ascii: - case StorageExplorerConstants.CassandraType.Varchar: - validator = new StringValidator(isRequired); - break; - case "Key": - validator = new KeyValidator(); - break; - default: - validator = new NotSupportedValidator(type); - break; - } - - return validator; - } -} - -interface ITypeValidatorMap { - [type: string]: IValueValidator; -} - -export default class EntityPropertyValueValidator { - private validators: ITypeValidatorMap; - private validatorFactory: PropertyValidatorFactory; - private isRequired: boolean; - - constructor(isRequired: boolean) { - this.validators = {}; - this.validatorFactory = new PropertyValidatorFactory(); - this.isRequired = isRequired; - } - - public validate(value: string, type: string): IValidationResult { - var validator: IValueValidator = this.getValidator(type); - - return validator ? validator.validate(value) : null; // Should not happen. - } - - public parseValue(value: string, type: string): any { - var validator: IValueValidator = this.getValidator(type); - - return validator ? validator.parseValue(value) : null; // Should not happen. - } - - private getValidator(type: string): IValueValidator { - var validator: IValueValidator = this.validators[type]; - - if (!validator) { - validator = this.validatorFactory.getValidator(type, this.isRequired); - this.validators[type] = validator; - } - - return validator; - } -} +import * as Utilities from "../../../Tables/Utilities"; +import * as StorageExplorerConstants from "../../../Tables/Constants"; +import * as EntityPropertyValidationCommon from "./EntityPropertyValidationCommon"; + +interface IValidationResult { + isInvalid: boolean; + help: string; +} + +interface IValueValidator { + validate: (value: string) => IValidationResult; + parseValue: (value: string) => any; +} + +/* Constants */ +var noHelp: string = ""; +var MaximumStringLength = 64 * 1024; // 64 KB +var MaximumRequiredStringLength = 1 * 1024; // 1 KB + +class ValueValidator implements IValueValidator { + public validate(value: string): IValidationResult { + // throw new Errors.NotImplementedFunctionError("ValueValidator.validate"); + return null; + } + + public parseValue(value: string): any { + return value; // default pass-thru implementation + } +} + +class KeyValidator implements ValueValidator { + private static detailedHelp = "Enter a string ('/', '\\', '#', '?' and control characters not allowed)."; // Localize + + public validate(value: string): IValidationResult { + if ( + value == null || + value.trim().length === 0 || + EntityPropertyValidationCommon.ValidationRegExp.PrimaryKey.test(value) + ) { + return { isInvalid: false, help: noHelp }; + } else { + return { isInvalid: true, help: KeyValidator.detailedHelp }; + } + } + + public parseValue(value: string): string { + return value; + } +} + +class BooleanValueValidator extends ValueValidator { + private detailedHelp = "Enter true or false."; // localize + + public validate(value: string): IValidationResult { + var success: boolean = false; + var help: string = noHelp; + + if (value) { + success = EntityPropertyValidationCommon.ValidationRegExp.Boolean.test(value); + } + + if (!success) { + help = this.detailedHelp; + } + + return { isInvalid: !success, help: help }; + } + + public parseValue(value: string): boolean { + // OData seems to require lowercase boolean values, see http://www.odata.org/documentation/odata-version-2-0/overview/ + return value.toString().toLowerCase() === "true"; + } +} + +class DateTimeValueValidator extends ValueValidator { + private detailedHelp = "Enter a date and time."; // localize + + public validate(value: string): IValidationResult { + var success: boolean = false; + var help: string = noHelp; + + if (value) { + // Try to parse the value to see if it is a valid date string + var parsed: number = Date.parse(value); + + success = !isNaN(parsed); + } + + if (!success) { + help = this.detailedHelp; + } + + return { isInvalid: !success, help: help }; + } + + public parseValue(value: string): Date { + var millisecondTime = Date.parse(value); + var parsed: Date = new Date(millisecondTime); + + return parsed; + } +} + +class DoubleValueValidator extends ValueValidator { + private detailedHelp = "Enter a 64-bit floating point value."; // localize + + public validate(value: string): IValidationResult { + var success: boolean = false; + var help: string = noHelp; + + if (value) { + success = EntityPropertyValidationCommon.ValidationRegExp.Float.test(value); + } + + if (!success) { + help = this.detailedHelp; + } + + return { isInvalid: !success, help: help }; + } + + public parseValue(value: string): number { + return parseFloat(value); + } +} + +class GuidValueValidator extends ValueValidator { + private detailedHelp = "Enter a 16-byte (128-bit) GUID value."; // localize + + public validate(value: string): IValidationResult { + var success: boolean = false; + var help: string = noHelp; + + if (value) { + success = EntityPropertyValidationCommon.ValidationRegExp.Guid.test(value); + } + + if (!success) { + help = this.detailedHelp; + } + + return { isInvalid: !success, help: help }; + } +} + +class IntegerValueValidator extends ValueValidator { + private detailedInt32Help = "Enter a signed 32-bit integer."; // localize + private detailedInt64Help = "Enter a signed 64-bit integer, in the range (-2^53 - 1, 2^53 - 1)."; // localize + + private isInt64: boolean; + + constructor(isInt64: boolean = true) { + super(); + + this.isInt64 = isInt64; + } + + public validate(value: string): IValidationResult { + var success: boolean = false; + var help: string = noHelp; + + if (value) { + success = EntityPropertyValidationCommon.ValidationRegExp.Integer.test(value) && Utilities.isSafeInteger(value); + if (success) { + var intValue = parseInt(value, 10); + + success = !isNaN(intValue); + if (success && !this.isInt64) { + success = + EntityPropertyValidationCommon.Int32.Min <= intValue && + intValue <= EntityPropertyValidationCommon.Int32.Max; + } + } + } + + if (!success) { + help = this.isInt64 ? this.detailedInt64Help : this.detailedInt32Help; + } + + return { isInvalid: !success, help: help }; + } + + public parseValue(value: string): number { + return parseInt(value, 10); + } +} + +// Allow all values for string type, unless the property is required, in which case an empty string is invalid. +class StringValidator extends ValueValidator { + private detailedHelp = "Enter a value up to 64 KB in size."; // localize + private isRequiredHelp = "Enter a value up to 1 KB in size."; // localize + private emptyStringHelp = "Empty string."; // localize + private isRequired: boolean; + + constructor(isRequired: boolean) { + super(); + + this.isRequired = isRequired; + } + + public validate(value: string): IValidationResult { + var help: string = this.isRequired ? this.isRequiredHelp : this.detailedHelp; + if (value === null) { + return { isInvalid: false, help: help }; + } + // Ensure we validate the string projection of value. + value = String(value); + + var success = true; + + if (success) { + success = value.length <= (this.isRequired ? MaximumRequiredStringLength : MaximumStringLength); + } + + if (success && this.isRequired) { + help = value ? noHelp : this.emptyStringHelp; + } + + return { isInvalid: !success, help: help }; + } + + public parseValue(value: string): string { + return String(value); // Ensure value is converted to string. + } +} + +class NotSupportedValidator extends ValueValidator { + private type: string; + + constructor(type: string) { + super(); + + this.type = type; + } + + public validate(ignoredValue: string): IValidationResult { + //throw new Errors.NotSupportedError(this.getMessage()); + return null; + } + + public parseValue(ignoredValue: string): any { + //throw new Errors.NotSupportedError(this.getMessage()); + return null; + } + + private getMessage(): string { + return "Properties of type " + this.type + " are not supported."; + } +} + +class PropertyValidatorFactory { + public getValidator(type: string, isRequired: boolean) { + var validator: IValueValidator = null; + + // TODO classify rest of Cassandra types/create validators for them + switch (type) { + case StorageExplorerConstants.TableType.Boolean: + case StorageExplorerConstants.CassandraType.Boolean: + validator = new BooleanValueValidator(); + break; + case StorageExplorerConstants.TableType.DateTime: + validator = new DateTimeValueValidator(); + break; + case StorageExplorerConstants.TableType.Double: + case StorageExplorerConstants.CassandraType.Decimal: + case StorageExplorerConstants.CassandraType.Double: + case StorageExplorerConstants.CassandraType.Float: + validator = new DoubleValueValidator(); + break; + case StorageExplorerConstants.TableType.Guid: + case StorageExplorerConstants.CassandraType.Uuid: + validator = new GuidValueValidator(); + break; + case StorageExplorerConstants.TableType.Int32: + case StorageExplorerConstants.CassandraType.Int: + // TODO create separate validators for smallint and tinyint + case StorageExplorerConstants.CassandraType.Smallint: + case StorageExplorerConstants.CassandraType.Tinyint: + validator = new IntegerValueValidator(/* isInt64 */ false); + break; + case StorageExplorerConstants.TableType.Int64: + case StorageExplorerConstants.CassandraType.Bigint: + case StorageExplorerConstants.CassandraType.Varint: + validator = new IntegerValueValidator(/* isInt64 */ true); + break; + case StorageExplorerConstants.TableType.String: + case StorageExplorerConstants.CassandraType.Text: + case StorageExplorerConstants.CassandraType.Ascii: + case StorageExplorerConstants.CassandraType.Varchar: + validator = new StringValidator(isRequired); + break; + case "Key": + validator = new KeyValidator(); + break; + default: + validator = new NotSupportedValidator(type); + break; + } + + return validator; + } +} + +interface ITypeValidatorMap { + [type: string]: IValueValidator; +} + +export default class EntityPropertyValueValidator { + private validators: ITypeValidatorMap; + private validatorFactory: PropertyValidatorFactory; + private isRequired: boolean; + + constructor(isRequired: boolean) { + this.validators = {}; + this.validatorFactory = new PropertyValidatorFactory(); + this.isRequired = isRequired; + } + + public validate(value: string, type: string): IValidationResult { + var validator: IValueValidator = this.getValidator(type); + + return validator ? validator.validate(value) : null; // Should not happen. + } + + public parseValue(value: string, type: string): any { + var validator: IValueValidator = this.getValidator(type); + + return validator ? validator.parseValue(value) : null; // Should not happen. + } + + private getValidator(type: string): IValueValidator { + var validator: IValueValidator = this.validators[type]; + + if (!validator) { + validator = this.validatorFactory.getValidator(type, this.isRequired); + this.validators[type] = validator; + } + + return validator; + } +} diff --git a/src/Explorer/Panes/UploadFilePane.html b/src/Explorer/Panes/UploadFilePane.html index ff815d130..0f0d4702d 100644 --- a/src/Explorer/Panes/UploadFilePane.html +++ b/src/Explorer/Panes/UploadFilePane.html @@ -20,7 +20,7 @@ data-bind="visible: formErrors() && formErrors() !== ''" >
- Error + Error ") - .closest("form") - .get(0) - .reset(); + inputElement.wrap("
").closest("form").get(0).reset(); inputElement.unwrap(); } } diff --git a/src/Explorer/Panes/UploadItemsPane.html b/src/Explorer/Panes/UploadItemsPane.html index 2073f1b6a..c9408c0cd 100644 --- a/src/Explorer/Panes/UploadItemsPane.html +++ b/src/Explorer/Panes/UploadItemsPane.html @@ -1,130 +1,130 @@ -
-
-
- -
- - -
- -
- Close -
-
- - - -
- - - -
-
-
- Select JSON Files - - More information - Select one or more JSON files to upload. Each file can contain a single JSON document or an array of - JSON documents. The combined size of all files in an individual upload operation must be less than 2 - MB. You can perform multiple upload operations for larger data sets. - -
- - - - Select JSON files to upload - -
-
- File upload status - - - - - - - - - - - - - - - -
FILE NAMESTATUS
-
-
-
-
-
- - -
- - -
- -
- -
-
+
+
+
+ +
+
+ +
+ +
+ Close +
+
+ + + +
+
+ Error + + + More details + +
+
+ + + +
+
+
+ Select JSON Files + + More information + Select one or more JSON files to upload. Each file can contain a single JSON document or an array of + JSON documents. The combined size of all files in an individual upload operation must be less than 2 + MB. You can perform multiple upload operations for larger data sets. + +
+ + + + Select JSON files to upload + +
+
+ File upload status + + + + + + + + + + + + + + + +
FILE NAMESTATUS
+
+
+
+
+
+ +
+
+ + +
+ +
+ +
+
diff --git a/src/Explorer/Panes/UploadItemsPane.ts b/src/Explorer/Panes/UploadItemsPane.ts index a1d4255eb..0b5d9d5a6 100644 --- a/src/Explorer/Panes/UploadItemsPane.ts +++ b/src/Explorer/Panes/UploadItemsPane.ts @@ -1,147 +1,143 @@ -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { ContextualPaneBase } from "./ContextualPaneBase"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; - -const UPLOAD_FILE_SIZE_LIMIT = 2097152; - -export class UploadItemsPane extends ContextualPaneBase { - public selectedFilesTitle: ko.Observable; - public files: ko.Observable; - public uploadFileDataVisible: ko.Computed; - public uploadFileData: ko.ObservableArray; - - constructor(options: ViewModels.PaneOptions) { - super(options); - this._initTitle(); - this.resetData(); - - this.selectedFilesTitle = ko.observable(""); - this.uploadFileData = ko.observableArray(); - this.uploadFileDataVisible = ko.computed( - () => !!this.uploadFileData() && this.uploadFileData().length > 0 - ); - this.files = ko.observable(); - this.files.subscribe((newFiles: FileList) => this._updateSelectedFilesTitle(newFiles)); - } - - public submit() { - this.formErrors(""); - if (!this.files() || this.files().length === 0) { - this.formErrors("No files specified"); - this.formErrorsDetails("No files were specified. Please input at least one file."); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - "Could not upload items -- No files were specified. Please input at least one file." - ); - return; - } else if (this._totalFileSizeForFileList(this.files()) > UPLOAD_FILE_SIZE_LIMIT) { - this.formErrors("Upload file size limit exceeded"); - this.formErrorsDetails("Total file upload size exceeds the 2 MB file size limit."); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - "Could not upload items -- Total file upload size exceeds the 2 MB file size limit." - ); - return; - } - - const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection(); - this.isExecuting(true); - selectedCollection && - selectedCollection - .uploadFiles(this.files()) - .then( - (uploadDetails: UploadDetails) => { - this.uploadFileData(uploadDetails.data); - this.files(undefined); - this._resetFileInput(); - }, - (error: any) => { - const errorMessage = getErrorMessage(error); - this.formErrors(errorMessage); - this.formErrorsDetails(errorMessage); - } - ) - .finally(() => { - this.isExecuting(false); - }); - } - - public updateSelectedFiles(element: any, event: any): void { - this.files(event.target.files); - } - - public close() { - super.close(); - this.resetData(); - this.files(undefined); - this.uploadFileData([]); - this._resetFileInput(); - } - - public onImportLinkClick(source: any, event: MouseEvent): boolean { - document.getElementById("importDocsInput").click(); - return false; - } - - public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { - this.onImportLinkClick(source, null); - return false; - } - return true; - }; - - public fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => { - return `${numSucceeded} items created, ${numFailed} errors`; - }; - - private _totalFileSizeForFileList(fileList: FileList): number { - let totalFileSize: number = 0; - if (!fileList) { - return totalFileSize; - } - for (let i = 0; i < fileList.length; i++) { - totalFileSize = totalFileSize + fileList.item(i).size; - } - - return totalFileSize; - } - - private _updateSelectedFilesTitle(fileList: FileList) { - this.selectedFilesTitle(""); - - if (!fileList || fileList.length === 0) { - return; - } - - for (let i = 0; i < fileList.length; i++) { - const originalTitle = this.selectedFilesTitle(); - this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`); - } - } - - private _initTitle(): void { - if (this.container.isPreferredApiCassandra() || this.container.isPreferredApiTable()) { - this.title("Upload Tables"); - } else if (this.container.isPreferredApiGraph()) { - this.title("Upload Graph"); - } else { - this.title("Upload Items"); - } - } - - private _resetFileInput(): void { - const inputElement = $("#importDocsInput"); - inputElement - .wrap("
") - .closest("form") - .get(0) - .reset(); - inputElement.unwrap(); - } -} +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { ContextualPaneBase } from "./ContextualPaneBase"; +import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; + +const UPLOAD_FILE_SIZE_LIMIT = 2097152; + +export class UploadItemsPane extends ContextualPaneBase { + public selectedFilesTitle: ko.Observable; + public files: ko.Observable; + public uploadFileDataVisible: ko.Computed; + public uploadFileData: ko.ObservableArray; + + constructor(options: ViewModels.PaneOptions) { + super(options); + this._initTitle(); + this.resetData(); + + this.selectedFilesTitle = ko.observable(""); + this.uploadFileData = ko.observableArray(); + this.uploadFileDataVisible = ko.computed( + () => !!this.uploadFileData() && this.uploadFileData().length > 0 + ); + this.files = ko.observable(); + this.files.subscribe((newFiles: FileList) => this._updateSelectedFilesTitle(newFiles)); + } + + public submit() { + this.formErrors(""); + if (!this.files() || this.files().length === 0) { + this.formErrors("No files specified"); + this.formErrorsDetails("No files were specified. Please input at least one file."); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + "Could not upload items -- No files were specified. Please input at least one file." + ); + return; + } else if (this._totalFileSizeForFileList(this.files()) > UPLOAD_FILE_SIZE_LIMIT) { + this.formErrors("Upload file size limit exceeded"); + this.formErrorsDetails("Total file upload size exceeds the 2 MB file size limit."); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + "Could not upload items -- Total file upload size exceeds the 2 MB file size limit." + ); + return; + } + + const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection(); + this.isExecuting(true); + selectedCollection && + selectedCollection + .uploadFiles(this.files()) + .then( + (uploadDetails: UploadDetails) => { + this.uploadFileData(uploadDetails.data); + this.files(undefined); + this._resetFileInput(); + }, + (error: any) => { + const errorMessage = getErrorMessage(error); + this.formErrors(errorMessage); + this.formErrorsDetails(errorMessage); + } + ) + .finally(() => { + this.isExecuting(false); + }); + } + + public updateSelectedFiles(element: any, event: any): void { + this.files(event.target.files); + } + + public close() { + super.close(); + this.resetData(); + this.files(undefined); + this.uploadFileData([]); + this._resetFileInput(); + } + + public onImportLinkClick(source: any, event: MouseEvent): boolean { + document.getElementById("importDocsInput").click(); + return false; + } + + public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { + this.onImportLinkClick(source, null); + return false; + } + return true; + }; + + public fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => { + return `${numSucceeded} items created, ${numFailed} errors`; + }; + + private _totalFileSizeForFileList(fileList: FileList): number { + let totalFileSize: number = 0; + if (!fileList) { + return totalFileSize; + } + for (let i = 0; i < fileList.length; i++) { + totalFileSize = totalFileSize + fileList.item(i).size; + } + + return totalFileSize; + } + + private _updateSelectedFilesTitle(fileList: FileList) { + this.selectedFilesTitle(""); + + if (!fileList || fileList.length === 0) { + return; + } + + for (let i = 0; i < fileList.length; i++) { + const originalTitle = this.selectedFilesTitle(); + this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`); + } + } + + private _initTitle(): void { + if (this.container.isPreferredApiCassandra() || this.container.isPreferredApiTable()) { + this.title("Upload Tables"); + } else if (this.container.isPreferredApiGraph()) { + this.title("Upload Graph"); + } else { + this.title("Upload Items"); + } + } + + private _resetFileInput(): void { + const inputElement = $("#importDocsInput"); + inputElement.wrap("").closest("form").get(0).reset(); + inputElement.unwrap(); + } +} diff --git a/src/Explorer/Panes/UploadItemsPaneAdapter.tsx b/src/Explorer/Panes/UploadItemsPaneAdapter.tsx index 37e514c61..643f8fe4e 100644 --- a/src/Explorer/Panes/UploadItemsPaneAdapter.tsx +++ b/src/Explorer/Panes/UploadItemsPaneAdapter.tsx @@ -42,13 +42,13 @@ export class UploadItemsPaneAdapter implements ReactAdapter { title: "Upload Items", submitButtonText: "Upload", onClose: () => this.close(), - onSubmit: () => this.submit() + onSubmit: () => this.submit(), }; const uploadItemsPaneProps: UploadItemsPaneProps = { selectedFilesTitle: this.selectedFilesTitle, updateSelectedFiles: this.updateSelectedFiles, - uploadFileData: this.uploadFileData + uploadFileData: this.uploadFileData, }; return ( @@ -106,7 +106,7 @@ export class UploadItemsPaneAdapter implements ReactAdapter { this.selectedFiles = undefined; this.selectedFilesTitle = ""; }, - error => { + (error) => { const errorMessage = getErrorMessage(error); this.formError = errorMessage; this.formErrorDetail = errorMessage; diff --git a/src/Explorer/SplashScreen/SplashScreenComponentAdapter.test.ts b/src/Explorer/SplashScreen/SplashScreenComponentAdapter.test.ts index 998d9ef86..62832a4d8 100644 --- a/src/Explorer/SplashScreen/SplashScreenComponentAdapter.test.ts +++ b/src/Explorer/SplashScreen/SplashScreenComponentAdapter.test.ts @@ -30,7 +30,7 @@ describe("SplashScreenComponentAdapter", () => { const mainButtons = splashScreenAdapter.createMainItems(); // Press all buttons and make sure create gets called - mainButtons.forEach(button => { + mainButtons.forEach((button) => { try { button.onClick(); } catch (e) { @@ -55,7 +55,7 @@ describe("SplashScreenComponentAdapter", () => { const mainButtons = splashScreenAdapter.createMainItems(); // Press all buttons and make sure create doesn't get called - mainButtons.forEach(button => { + mainButtons.forEach((button) => { try { button.onClick(); } catch (e) { diff --git a/src/Explorer/SplashScreen/SplashScreenComponentApdapter.tsx b/src/Explorer/SplashScreen/SplashScreenComponentApdapter.tsx index eb989decd..03efd6588 100644 --- a/src/Explorer/SplashScreen/SplashScreenComponentApdapter.tsx +++ b/src/Explorer/SplashScreen/SplashScreenComponentApdapter.tsx @@ -31,7 +31,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter { constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); - this.container.tabsManager.openedTabs.subscribe(tabs => { + this.container.tabsManager.openedTabs.subscribe((tabs) => { if (tabs.length === 0) { this.forceRender(); } @@ -78,8 +78,8 @@ export class SplashScreenComponentAdapter implements ReactAdapter { iconSrc: NewContainerIcon, title: this.container.addCollectionText(), description: "Create a new container for storage and throughput", - onClick: () => this.container.onNewCollectionClicked() - } + onClick: () => this.container.onNewCollectionClicked(), + }, ]; if (dataSampleUtil.isSampleContainerCreationSupported()) { @@ -88,7 +88,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter { iconSrc: SampleIcon, title: "Start with Sample", description: "Get started with a sample provided by Cosmos DB", - onClick: () => dataSampleUtil.createSampleContainerAsync() + onClick: () => dataSampleUtil.createSampleContainerAsync(), }); } @@ -97,7 +97,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter { iconSrc: NewNotebookIcon, title: "New Notebook", description: "Create a notebook to start querying, visualizing, and modeling your data", - onClick: () => this.container.onNewNotebookClicked() + onClick: () => this.container.onNewNotebookClicked(), }); } @@ -120,7 +120,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter { selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null); }, title: "New SQL Query", - description: null + description: null, }); } else if (this.container.isPreferredApiMongoDB()) { items.push({ @@ -130,7 +130,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter { selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null); }, title: "New Query", - description: null + description: null, }); } @@ -138,7 +138,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter { iconSrc: OpenQueryIcon, title: "Open Query", description: null, - onClick: () => this.container.browseQueriesPane.open() + onClick: () => this.container.browseQueriesPane.open(), }); if (!this.container.isPreferredApiCassandra()) { @@ -149,7 +149,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter { onClick: () => { const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null); - } + }, }); } @@ -170,14 +170,14 @@ export class SplashScreenComponentAdapter implements ReactAdapter { onClick: () => { const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection(); selectedCollection && selectedCollection.onSettingsClick(); - } + }, }); } else { items.push({ iconSrc: AddDatabaseIcon, title: this.container.addDatabaseText(), description: null, - onClick: () => this.container.addDatabasePane.open() + onClick: () => this.container.addDatabasePane.open(), }); } @@ -194,12 +194,12 @@ export class SplashScreenComponentAdapter implements ReactAdapter { } private createRecentItems(): SplashScreenItem[] { - return this.container.mostRecentActivity.getItems(userContext.databaseAccount?.id).map(item => ({ + return this.container.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((item) => ({ iconSrc: MostRecentActivity.MostRecentActivity.getItemIcon(item), title: item.title, description: item.description, info: SplashScreenComponentAdapter.getInfo(item), - onClick: () => this.container.mostRecentActivity.onItemClicked(item) + onClick: () => this.container.mostRecentActivity.onItemClicked(item), })); } @@ -209,20 +209,20 @@ export class SplashScreenComponentAdapter implements ReactAdapter { iconSrc: null, title: "Data Modeling", description: "Learn more about modeling", - onClick: () => window.open(SplashScreenComponentAdapter.dataModelingUrl) + onClick: () => window.open(SplashScreenComponentAdapter.dataModelingUrl), }, { iconSrc: null, title: "Cost & Throughput Calculation", description: "Learn more about cost calculation", - onClick: () => window.open(SplashScreenComponentAdapter.throughputEstimatorUrl) + onClick: () => window.open(SplashScreenComponentAdapter.throughputEstimatorUrl), }, { iconSrc: null, title: "Configure automatic failover", description: "Learn more about Cosmos DB high-availability", - onClick: () => window.open(SplashScreenComponentAdapter.failoverUrl) - } + onClick: () => window.open(SplashScreenComponentAdapter.failoverUrl), + }, ]; } } diff --git a/src/Explorer/Tables/Constants.ts b/src/Explorer/Tables/Constants.ts index 919059d12..5d5d60fee 100644 --- a/src/Explorer/Tables/Constants.ts +++ b/src/Explorer/Tables/Constants.ts @@ -1,171 +1,171 @@ -export var TableType = { - String: "String", - Boolean: "Boolean", - Binary: "Binary", - DateTime: "DateTime", - Double: "Double", - Guid: "Guid", - Int32: "Int32", - Int64: "Int64" -}; - -export var CassandraType = { - Ascii: "Ascii", - Bigint: "Bigint", - Blob: "Blob", - Boolean: "Boolean", - Decimal: "Decimal", - Double: "Double", - Float: "Float", - Int: "Int", - Text: "Text", - Uuid: "Uuid", - Varchar: "Varchar", - Varint: "Varint", - Inet: "Inet", - Smallint: "Smallint", - Tinyint: "Tinyint" -}; - -export var ClauseRule = { - And: "And", - Or: "Or" -}; - -export var Operator = { - EqualTo: "==", - GreaterThan: ">", - GreaterThanOrEqualTo: ">=", - LessThan: "<", - LessThanOrEqualTo: "<=", - NotEqualTo: "<>", - Equal: "=" -}; - -export var ODataOperator = { - EqualTo: "eq", - GreaterThan: "gt", - GreaterThanOrEqualTo: "ge", - LessThan: "lt", - LessThanOrEqualTo: "le", - NotEqualTo: "ne" -}; - -export var timeOptions = { - lastHour: "Last hour", - last24Hours: "Last 24 hours", - last7Days: "Last 7 days", - last31Days: "Last 31 days", - last365Days: "Last 365 days", - currentMonth: "Current month", - currentYear: "Current year", - custom: "Custom..." -}; - -export var htmlSelectors = { - dataTableSelector: "#storageTable", - dataTableAllRowsSelector: "#storageTable tbody tr", - dataTableHeadRowSelector: ".dataTable thead tr", - dataTableBodyRowSelector: ".dataTable tbody tr", - dataTableScrollBodySelector: ".dataTables_scrollBody", - dataTableScrollContainerSelector: ".dataTables_scroll", - dataTableHeaderTypeSelector: "table thead th", - dataTablePaginationButtonSelector: ".paginate_button", - dataTableHeaderTableSelector: "#storageTable_wrapper .dataTables_scrollHeadInner table", - dataTableBodyTableSelector: "#storageTable_wrapper .dataTables_scrollBody table", - searchInputField: ".search-input", - uploadDropdownSelector: "#upload-dropdown", - navigationDropdownSelector: "#navigation-dropdown", - addressBarInputSelector: "#address-bar", - breadCrumbsSelector: "#breadcrumb-list", - breadCrumbItemsSelector: ".breadcrumb li a", - paginateSelector: "#storageTable_paginate", - dataTablesInfoSelector: "#storageTable_info", - selectAllDropdownSelector: "#select-all-dropdown" -}; - -export var defaultHeader = " "; - -export var EntityKeyNames = { - PartitionKey: "PartitionKey", - RowKey: "RowKey", - Timestamp: "Timestamp", - Metadata: ".metadata", - Etag: "etag" -}; - -export var htmlAttributeNames = { - dataTableNameAttr: "name_attr", - dataTableContentTypeAttr: "contentType_attr", - dataTableSnapshotAttr: "snapshot_attr", - dataTableRowKeyAttr: "rowKey_attr", - dataTableMessageIdAttr: "messageId_attr", - dataTableHeaderIndex: "data-column-index" -}; - -export var cssColors = { - commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */ -}; - -export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"]; -export var transparentColor = "transparent"; - -export var keyCodes = { - RightClick: 3, - Enter: 13, - Esc: 27, - Tab: 9, - LeftArrow: 37, - UpArrow: 38, - RightArrow: 39, - DownArrow: 40, - Delete: 46, - A: 65, - B: 66, - C: 67, - D: 68, - E: 69, - F: 70, - G: 71, - H: 72, - I: 73, - J: 74, - K: 75, - L: 76, - M: 77, - N: 78, - O: 79, - P: 80, - Q: 81, - R: 82, - S: 83, - T: 84, - U: 85, - V: 86, - W: 87, - X: 88, - Y: 89, - Z: 90, - Period: 190, - DecimalPoint: 110, - F1: 112, - F2: 113, - F3: 114, - F4: 115, - F5: 116, - F6: 117, - F7: 118, - F8: 119, - F9: 120, - F10: 121, - F11: 122, - F12: 123, - Dash: 189 -}; - -export var InputType = { - Text: "text", - // Chrome doesn't support datetime, instead, datetime-local is supported. - DateTime: "datetime-local", - Number: "number" -}; +export var TableType = { + String: "String", + Boolean: "Boolean", + Binary: "Binary", + DateTime: "DateTime", + Double: "Double", + Guid: "Guid", + Int32: "Int32", + Int64: "Int64", +}; + +export var CassandraType = { + Ascii: "Ascii", + Bigint: "Bigint", + Blob: "Blob", + Boolean: "Boolean", + Decimal: "Decimal", + Double: "Double", + Float: "Float", + Int: "Int", + Text: "Text", + Uuid: "Uuid", + Varchar: "Varchar", + Varint: "Varint", + Inet: "Inet", + Smallint: "Smallint", + Tinyint: "Tinyint", +}; + +export var ClauseRule = { + And: "And", + Or: "Or", +}; + +export var Operator = { + EqualTo: "==", + GreaterThan: ">", + GreaterThanOrEqualTo: ">=", + LessThan: "<", + LessThanOrEqualTo: "<=", + NotEqualTo: "<>", + Equal: "=", +}; + +export var ODataOperator = { + EqualTo: "eq", + GreaterThan: "gt", + GreaterThanOrEqualTo: "ge", + LessThan: "lt", + LessThanOrEqualTo: "le", + NotEqualTo: "ne", +}; + +export var timeOptions = { + lastHour: "Last hour", + last24Hours: "Last 24 hours", + last7Days: "Last 7 days", + last31Days: "Last 31 days", + last365Days: "Last 365 days", + currentMonth: "Current month", + currentYear: "Current year", + custom: "Custom...", +}; + +export var htmlSelectors = { + dataTableSelector: "#storageTable", + dataTableAllRowsSelector: "#storageTable tbody tr", + dataTableHeadRowSelector: ".dataTable thead tr", + dataTableBodyRowSelector: ".dataTable tbody tr", + dataTableScrollBodySelector: ".dataTables_scrollBody", + dataTableScrollContainerSelector: ".dataTables_scroll", + dataTableHeaderTypeSelector: "table thead th", + dataTablePaginationButtonSelector: ".paginate_button", + dataTableHeaderTableSelector: "#storageTable_wrapper .dataTables_scrollHeadInner table", + dataTableBodyTableSelector: "#storageTable_wrapper .dataTables_scrollBody table", + searchInputField: ".search-input", + uploadDropdownSelector: "#upload-dropdown", + navigationDropdownSelector: "#navigation-dropdown", + addressBarInputSelector: "#address-bar", + breadCrumbsSelector: "#breadcrumb-list", + breadCrumbItemsSelector: ".breadcrumb li a", + paginateSelector: "#storageTable_paginate", + dataTablesInfoSelector: "#storageTable_info", + selectAllDropdownSelector: "#select-all-dropdown", +}; + +export var defaultHeader = " "; + +export var EntityKeyNames = { + PartitionKey: "PartitionKey", + RowKey: "RowKey", + Timestamp: "Timestamp", + Metadata: ".metadata", + Etag: "etag", +}; + +export var htmlAttributeNames = { + dataTableNameAttr: "name_attr", + dataTableContentTypeAttr: "contentType_attr", + dataTableSnapshotAttr: "snapshot_attr", + dataTableRowKeyAttr: "rowKey_attr", + dataTableMessageIdAttr: "messageId_attr", + dataTableHeaderIndex: "data-column-index", +}; + +export var cssColors = { + commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */, +}; + +export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"]; +export var transparentColor = "transparent"; + +export var keyCodes = { + RightClick: 3, + Enter: 13, + Esc: 27, + Tab: 9, + LeftArrow: 37, + UpArrow: 38, + RightArrow: 39, + DownArrow: 40, + Delete: 46, + A: 65, + B: 66, + C: 67, + D: 68, + E: 69, + F: 70, + G: 71, + H: 72, + I: 73, + J: 74, + K: 75, + L: 76, + M: 77, + N: 78, + O: 79, + P: 80, + Q: 81, + R: 82, + S: 83, + T: 84, + U: 85, + V: 86, + W: 87, + X: 88, + Y: 89, + Z: 90, + Period: 190, + DecimalPoint: 110, + F1: 112, + F2: 113, + F3: 114, + F4: 115, + F5: 116, + F6: 117, + F7: 118, + F8: 119, + F9: 120, + F10: 121, + F11: 122, + F12: 123, + Dash: 189, +}; + +export var InputType = { + Text: "text", + // Chrome doesn't support datetime, instead, datetime-local is supported. + DateTime: "datetime-local", + Number: "number", +}; diff --git a/src/Explorer/Tables/DataTable/CacheBase.ts b/src/Explorer/Tables/DataTable/CacheBase.ts index e5df50dd4..e99622173 100644 --- a/src/Explorer/Tables/DataTable/CacheBase.ts +++ b/src/Explorer/Tables/DataTable/CacheBase.ts @@ -1,26 +1,26 @@ -abstract class CacheBase { - public data: T[] | null; - public sortOrder: any; - public serverCallInProgress: boolean; - - constructor() { - this.data = null; - this.sortOrder = null; - this.serverCallInProgress = false; - } - - public get length(): number { - return this.data ? this.data.length : 0; - } - - public clear() { - this.preClear(); - this.data = null; - this.sortOrder = null; - this.serverCallInProgress = false; - } - - protected abstract preClear(): void; -} - -export default CacheBase; +abstract class CacheBase { + public data: T[] | null; + public sortOrder: any; + public serverCallInProgress: boolean; + + constructor() { + this.data = null; + this.sortOrder = null; + this.serverCallInProgress = false; + } + + public get length(): number { + return this.data ? this.data.length : 0; + } + + public clear() { + this.preClear(); + this.data = null; + this.sortOrder = null; + this.serverCallInProgress = false; + } + + protected abstract preClear(): void; +} + +export default CacheBase; diff --git a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts index b70251767..ab7b2ee21 100644 --- a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts +++ b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts @@ -1,411 +1,396 @@ -import * as ko from "knockout"; -import * as _ from "underscore"; - -import * as Constants from "../Constants"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import * as DataTableBuilder from "./DataTableBuilder"; -import DataTableOperationManager from "./DataTableOperationManager"; -import * as DataTableOperations from "./DataTableOperations"; -import QueryTablesTab from "../../Tabs/QueryTablesTab"; -import TableEntityListViewModel from "./TableEntityListViewModel"; -import * as Utilities from "../Utilities"; -import * as Entities from "../Entities"; - -/** - * Custom binding manager of datatable - */ -var tableEntityListViewModelMap: { - [key: string]: { - tableViewModel: TableEntityListViewModel; - operationManager: DataTableOperationManager; - $dataTable: JQuery; - }; -} = {}; - -function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { - var tableEntityListViewModel = bindingContext.$data; - tableEntityListViewModel.notifyColumnChanges = onTableColumnChange; - var $dataTable = $(element); - var queryTablesTab = bindingContext.$parent; - var operationManager = new DataTableOperationManager( - $dataTable, - tableEntityListViewModel, - queryTablesTab.tableCommands - ); - - tableEntityListViewModelMap[queryTablesTab.tabId] = { - tableViewModel: tableEntityListViewModel, - operationManager: operationManager, - $dataTable: $dataTable - }; - - createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start. - $(window).resize(updateTableScrollableRegionMetrics); - operationManager.focusTable(); // Also selects the first row if needed. -} - -function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) { - var columnsFilter: boolean[] = null; - var tableEntityListViewModel = tableEntityListViewModelMap[queryTablesTab.tabId].tableViewModel; - if (queryTablesTab.queryViewModel()) { - queryTablesTab - .queryViewModel() - .queryBuilderViewModel() - .updateColumnOptions(); - } - createDataTable( - tableEntityListViewModel.tablePageStartIndex, - tableEntityListViewModel, - queryTablesTab, - true, - columnsFilter - ); -} - -function createDataTable( - startIndex: number, - tableEntityListViewModel: TableEntityListViewModel, - queryTablesTab: QueryTablesTab, - destroy: boolean = false, - columnsFilter: boolean[] = null -): void { - var $dataTable = tableEntityListViewModelMap[queryTablesTab.tabId].$dataTable; - if (destroy) { - // Find currently displayed columns. - var currentColumns: string[] = tableEntityListViewModel.headers; - - // Calculate how many more columns need to added to the current table. - var columnsToAdd: number = _.difference(tableEntityListViewModel.headers, currentColumns).length; - - // This is needed as current solution of adding column is more like a workaround - // The official support for dynamically add column is not yet there - // Please track github issue https://github.com/DataTables/DataTables/issues/273 for its offical support - for (var i = 0; i < columnsToAdd; i++) { - $(".dataTables_scrollHead table thead tr th") - .eq(0) - .after(""); - } - tableEntityListViewModel.table.destroy(); - $dataTable.empty(); - } - - var jsonColTable = []; - - for (var i = 0; i < tableEntityListViewModel.headers.length; i++) { - jsonColTable.push({ - sTitle: tableEntityListViewModel.headers[i], - data: tableEntityListViewModel.headers[i], - aTargets: [i], - mRender: bindColumn, - visible: !!columnsFilter ? columnsFilter[i] : true - }); - } - - tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, { - // WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText) - // so that they don't get interpreted as HTML in our page. - colReorder: true, - aoColumnDefs: jsonColTable, - stateSave: false, - dom: "RZlfrtip", - oColReorder: { - iFixedColumns: 1 - }, - displayStart: startIndex, - bPaginate: true, - pagingType: "full_numbers", - bProcessing: true, - oLanguage: { - sInfo: "Results _START_ - _END_ of _TOTAL_", - oPaginate: { - sFirst: "<<", - sNext: ">", - sPrevious: "<", - sLast: ">>" - }, - sProcessing: '', - oAria: { - sSortAscending: "", - sSortDescending: "" - } - }, - destroy: destroy, - bInfo: true, - bLength: false, - bLengthChange: false, - scrollX: true, - scrollCollapse: true, - iDisplayLength: 100, - serverSide: true, - ajax: queryTablesTab.tabId, // Using this settings to make sure for getServerData we update the table based on the appropriate tab - fnServerData: getServerData, - fnRowCallback: bindClientId, - fnInitComplete: initializeTable, - fnDrawCallback: updateSelectionStatus - }); - - (tableEntityListViewModel.table.table(0).container() as Element) - .querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector) - .forEach(table => { - table.setAttribute( - "summary", - `Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}` - ); - }); - - (tableEntityListViewModel.table.table(0).container() as Element) - .querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector) - .forEach(table => { - table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`); - }); -} - -function bindColumn(data: any, type: string, full: any) { - var displayedValue: any = null; - if (data) { - displayedValue = data._; - - // SECURITY: Make sure we don't allow cross-site scripting by interpreting the values as HTML - displayedValue = Utilities.htmlEncode(displayedValue); - - // Css' empty psuedo class can only tell the difference of whether a cell has values. - // A cell has no values no matter it's empty or it has no such a property. - // To distinguish between an empty cell and a non-existing property cell, - // we add a whitespace to the empty cell so that css will treat it as a cell with values. - if (displayedValue === "" && data.$ === Constants.TableType.String) { - displayedValue = " "; - } - } - return displayedValue; -} - -function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: any) { - tableEntityListViewModelMap[oSettings.ajax].tableViewModel.renderNextPageAndupdateCache( - sSource, - aoData, - fnCallback, - oSettings - ); -} - -/** - * Bind table data information to row element so that we can track back to the table data - * from UI elements. - */ -function bindClientId(nRow: Node, aData: Entities.ITableEntity) { - $(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._); - return nRow; -} - -function selectionChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { - $(".dataTable tr.selected") - .attr("tabindex", "-1") - .removeClass("selected"); - - const selected = - bindingContext && bindingContext.$data && bindingContext.$data.selected && bindingContext.$data.selected(); - selected && - selected.forEach((b: Entities.ITableEntity) => { - var sel = DataTableOperations.getRowSelector([ - { - key: Constants.htmlAttributeNames.dataTableRowKeyAttr, - value: b.RowKey && b.RowKey._ && b.RowKey._.toString() - } - ]); - - $(sel) - .attr("tabindex", "0") - .focus() - .addClass("selected"); - }); - //selected = bindingContext.$data.selected(); -} - -function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { - // do nothing for now -} - -function initializeTable(): void { - updateTableScrollableRegionMetrics(); - initializeEventHandlers(); -} - -function updateTableScrollableRegionMetrics(): void { - updateTableScrollableRegionHeight(); - updateTableScrollableRegionWidth(); -} - -/* - * Update the table's scrollable region height. So the pagination control is always shown at the bottom of the page. - */ -function updateTableScrollableRegionHeight(): void { - $(".tab-pane").each(function(index, tabElement) { - if (!$(tabElement).hasClass("tableContainer")) { - return; - } - - // Add some padding to the table so it doesn't get too close to the container border. - var dataTablePaddingBottom = 10; - var bodyHeight = $(window).height(); - var dataTablesScrollBodyPosY = $(tabElement) - .find(Constants.htmlSelectors.dataTableScrollBodySelector) - .offset().top; - var dataTablesInfoElem = $(tabElement).find(".dataTables_info"); - var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate"); - const explorer = window.dataExplorer; - const notificationConsoleHeight = explorer.isNotificationConsoleExpanded() - ? 252 /** 32px(header) + 220px(content height) **/ - : 32 /** Header height **/; - - var scrollHeight = - bodyHeight - - dataTablesScrollBodyPosY - - dataTablesPaginateElem.outerHeight(true) - - dataTablePaddingBottom - - notificationConsoleHeight; - - //info and paginate control are stacked - if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) { - scrollHeight -= dataTablesInfoElem.outerHeight(true); - } - - // TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue) - // in the current version of JQuery we are using. Ideally, we would upgrade JQuery and use this line instead: - // $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight); - var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0]; - var style = getComputedStyle(element); - var actualHeight = parseInt(style.height); - var change = element.offsetHeight - scrollHeight; - $(tabElement) - .find(Constants.htmlSelectors.dataTableScrollBodySelector) - .height(actualHeight - change); - }); -} - -/* - * Update the table's scrollable region width to make efficient use of the remaining space. - */ -function updateTableScrollableRegionWidth(): void { - $(".tab-pane").each(function(index, tabElement) { - if (!$(tabElement).hasClass("tableContainer")) { - return; - } - - var bodyWidth = $(window).width(); - var dataTablesScrollBodyPosLeft = $(tabElement) - .find(Constants.htmlSelectors.dataTableScrollBodySelector) - .offset().left; - var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft; - - // jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature. - // We work around that by setting the height for it's container instead. - $(tabElement) - .find(Constants.htmlSelectors.dataTableScrollContainerSelector) - .width(scrollWidth); - }); -} - -function initializeEventHandlers(): void { - var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector); - var $firstHeader: JQuery = $headers.first(); - var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex); - - $headers - .on("keydown", (event: JQueryEventObject) => { - Utilities.onEnter(event, ($sourceElement: JQuery) => { - $sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive); - }); - - // Bind shift+tab from first header back to search input field - Utilities.onTab( - event, - ($sourceElement: JQuery) => { - var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex); - - if (sourceIndex === firstIndex) { - event.preventDefault(); - } - }, - /* metaKey */ null, - /* shiftKey */ true, - /* altKey */ null - ); - - // Also reset color if [shift-] tabbing away from button while holding down 'enter' - Utilities.onTab(event, ($sourceElement: JQuery) => { - $sourceElement.css("background-color", ""); - }); - }) - .on("keyup", (event: JQueryEventObject) => { - Utilities.onEnter(event, ($sourceElement: JQuery) => { - $sourceElement.css("background-color", ""); - }); - }); -} - -function updateSelectionStatus(oSettings: any): void { - var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector); - if ($dataTableRows) { - for (var i = 0; i < $dataTableRows.length; i++) { - var $row: JQuery = $dataTableRows.eq(i); - var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr); - var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel; - if (table.isItemSelected(table.getTableEntityKeys(rowKey))) { - $row.attr("tabindex", "0"); - } - } - } - - updateDataTableFocus(oSettings.ajax); - - DataTableOperations.setPaginationButtonEventHandlers(); -} - -// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity. -// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects." -function updateDataTableFocus(queryTablesTabId: string): void { - var $activeElement: JQuery = $(document.activeElement); - var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element. - var storageExplorerFrameHasFocus: boolean = document.hasFocus(); - var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager; - if (operationManager) { - if (isFocusLost && storageExplorerFrameHasFocus) { - // We get here when no control is active, meaning that the table update was triggered - // from a dialog, the context menu or by clicking on a toolbar control or header. - // Note that giving focus to the table also selects the first row if needed. - // The document.hasFocus() ensures that the table will only get focus when the - // focus was lost (i.e. "body has the focus") within the Storage Explorer frame - // i.e. not when the focus is lost because it is in another frame - // e.g. a daytona dialog or in the Activity Log. - operationManager.focusTable(); - } - if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) { - // If table header is selected, focus is shifted to the selected element as part of accessibility - $activeElement && $activeElement.focus(); - } else { - // If some control is active, we don't give focus back to the table, - // just select the first row if needed (empty selection). - operationManager.selectFirstIfNeeded(); - } - } -} - -(ko.bindingHandlers).tableSource = { - init: bindDataTable, - update: dataChanged -}; - -(ko.bindingHandlers).tableSelection = { - update: selectionChanged -}; - -(ko.bindingHandlers).readOnly = { - update: function(element: any, valueAccessor: any) { - var value = ko.utils.unwrapObservable(valueAccessor()); - if (value) { - element.setAttribute("readOnly", true); - } else { - element.removeAttribute("readOnly"); - } - } -}; +import * as ko from "knockout"; +import * as _ from "underscore"; + +import * as Constants from "../Constants"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import * as DataTableBuilder from "./DataTableBuilder"; +import DataTableOperationManager from "./DataTableOperationManager"; +import * as DataTableOperations from "./DataTableOperations"; +import QueryTablesTab from "../../Tabs/QueryTablesTab"; +import TableEntityListViewModel from "./TableEntityListViewModel"; +import * as Utilities from "../Utilities"; +import * as Entities from "../Entities"; + +/** + * Custom binding manager of datatable + */ +var tableEntityListViewModelMap: { + [key: string]: { + tableViewModel: TableEntityListViewModel; + operationManager: DataTableOperationManager; + $dataTable: JQuery; + }; +} = {}; + +function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { + var tableEntityListViewModel = bindingContext.$data; + tableEntityListViewModel.notifyColumnChanges = onTableColumnChange; + var $dataTable = $(element); + var queryTablesTab = bindingContext.$parent; + var operationManager = new DataTableOperationManager( + $dataTable, + tableEntityListViewModel, + queryTablesTab.tableCommands + ); + + tableEntityListViewModelMap[queryTablesTab.tabId] = { + tableViewModel: tableEntityListViewModel, + operationManager: operationManager, + $dataTable: $dataTable, + }; + + createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start. + $(window).resize(updateTableScrollableRegionMetrics); + operationManager.focusTable(); // Also selects the first row if needed. +} + +function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) { + var columnsFilter: boolean[] = null; + var tableEntityListViewModel = tableEntityListViewModelMap[queryTablesTab.tabId].tableViewModel; + if (queryTablesTab.queryViewModel()) { + queryTablesTab.queryViewModel().queryBuilderViewModel().updateColumnOptions(); + } + createDataTable( + tableEntityListViewModel.tablePageStartIndex, + tableEntityListViewModel, + queryTablesTab, + true, + columnsFilter + ); +} + +function createDataTable( + startIndex: number, + tableEntityListViewModel: TableEntityListViewModel, + queryTablesTab: QueryTablesTab, + destroy: boolean = false, + columnsFilter: boolean[] = null +): void { + var $dataTable = tableEntityListViewModelMap[queryTablesTab.tabId].$dataTable; + if (destroy) { + // Find currently displayed columns. + var currentColumns: string[] = tableEntityListViewModel.headers; + + // Calculate how many more columns need to added to the current table. + var columnsToAdd: number = _.difference(tableEntityListViewModel.headers, currentColumns).length; + + // This is needed as current solution of adding column is more like a workaround + // The official support for dynamically add column is not yet there + // Please track github issue https://github.com/DataTables/DataTables/issues/273 for its offical support + for (var i = 0; i < columnsToAdd; i++) { + $(".dataTables_scrollHead table thead tr th").eq(0).after(""); + } + tableEntityListViewModel.table.destroy(); + $dataTable.empty(); + } + + var jsonColTable = []; + + for (var i = 0; i < tableEntityListViewModel.headers.length; i++) { + jsonColTable.push({ + sTitle: tableEntityListViewModel.headers[i], + data: tableEntityListViewModel.headers[i], + aTargets: [i], + mRender: bindColumn, + visible: !!columnsFilter ? columnsFilter[i] : true, + }); + } + + tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, { + // WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText) + // so that they don't get interpreted as HTML in our page. + colReorder: true, + aoColumnDefs: jsonColTable, + stateSave: false, + dom: "RZlfrtip", + oColReorder: { + iFixedColumns: 1, + }, + displayStart: startIndex, + bPaginate: true, + pagingType: "full_numbers", + bProcessing: true, + oLanguage: { + sInfo: "Results _START_ - _END_ of _TOTAL_", + oPaginate: { + sFirst: "<<", + sNext: ">", + sPrevious: "<", + sLast: ">>", + }, + sProcessing: '', + oAria: { + sSortAscending: "", + sSortDescending: "", + }, + }, + destroy: destroy, + bInfo: true, + bLength: false, + bLengthChange: false, + scrollX: true, + scrollCollapse: true, + iDisplayLength: 100, + serverSide: true, + ajax: queryTablesTab.tabId, // Using this settings to make sure for getServerData we update the table based on the appropriate tab + fnServerData: getServerData, + fnRowCallback: bindClientId, + fnInitComplete: initializeTable, + fnDrawCallback: updateSelectionStatus, + }); + + (tableEntityListViewModel.table.table(0).container() as Element) + .querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector) + .forEach((table) => { + table.setAttribute( + "summary", + `Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}` + ); + }); + + (tableEntityListViewModel.table.table(0).container() as Element) + .querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector) + .forEach((table) => { + table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`); + }); +} + +function bindColumn(data: any, type: string, full: any) { + var displayedValue: any = null; + if (data) { + displayedValue = data._; + + // SECURITY: Make sure we don't allow cross-site scripting by interpreting the values as HTML + displayedValue = Utilities.htmlEncode(displayedValue); + + // Css' empty psuedo class can only tell the difference of whether a cell has values. + // A cell has no values no matter it's empty or it has no such a property. + // To distinguish between an empty cell and a non-existing property cell, + // we add a whitespace to the empty cell so that css will treat it as a cell with values. + if (displayedValue === "" && data.$ === Constants.TableType.String) { + displayedValue = " "; + } + } + return displayedValue; +} + +function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: any) { + tableEntityListViewModelMap[oSettings.ajax].tableViewModel.renderNextPageAndupdateCache( + sSource, + aoData, + fnCallback, + oSettings + ); +} + +/** + * Bind table data information to row element so that we can track back to the table data + * from UI elements. + */ +function bindClientId(nRow: Node, aData: Entities.ITableEntity) { + $(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._); + return nRow; +} + +function selectionChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { + $(".dataTable tr.selected").attr("tabindex", "-1").removeClass("selected"); + + const selected = + bindingContext && bindingContext.$data && bindingContext.$data.selected && bindingContext.$data.selected(); + selected && + selected.forEach((b: Entities.ITableEntity) => { + var sel = DataTableOperations.getRowSelector([ + { + key: Constants.htmlAttributeNames.dataTableRowKeyAttr, + value: b.RowKey && b.RowKey._ && b.RowKey._.toString(), + }, + ]); + + $(sel).attr("tabindex", "0").focus().addClass("selected"); + }); + //selected = bindingContext.$data.selected(); +} + +function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { + // do nothing for now +} + +function initializeTable(): void { + updateTableScrollableRegionMetrics(); + initializeEventHandlers(); +} + +function updateTableScrollableRegionMetrics(): void { + updateTableScrollableRegionHeight(); + updateTableScrollableRegionWidth(); +} + +/* + * Update the table's scrollable region height. So the pagination control is always shown at the bottom of the page. + */ +function updateTableScrollableRegionHeight(): void { + $(".tab-pane").each(function (index, tabElement) { + if (!$(tabElement).hasClass("tableContainer")) { + return; + } + + // Add some padding to the table so it doesn't get too close to the container border. + var dataTablePaddingBottom = 10; + var bodyHeight = $(window).height(); + var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top; + var dataTablesInfoElem = $(tabElement).find(".dataTables_info"); + var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate"); + const explorer = window.dataExplorer; + const notificationConsoleHeight = explorer.isNotificationConsoleExpanded() + ? 252 /** 32px(header) + 220px(content height) **/ + : 32; /** Header height **/ + + var scrollHeight = + bodyHeight - + dataTablesScrollBodyPosY - + dataTablesPaginateElem.outerHeight(true) - + dataTablePaddingBottom - + notificationConsoleHeight; + + //info and paginate control are stacked + if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) { + scrollHeight -= dataTablesInfoElem.outerHeight(true); + } + + // TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue) + // in the current version of JQuery we are using. Ideally, we would upgrade JQuery and use this line instead: + // $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight); + var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0]; + var style = getComputedStyle(element); + var actualHeight = parseInt(style.height); + var change = element.offsetHeight - scrollHeight; + $(tabElement) + .find(Constants.htmlSelectors.dataTableScrollBodySelector) + .height(actualHeight - change); + }); +} + +/* + * Update the table's scrollable region width to make efficient use of the remaining space. + */ +function updateTableScrollableRegionWidth(): void { + $(".tab-pane").each(function (index, tabElement) { + if (!$(tabElement).hasClass("tableContainer")) { + return; + } + + var bodyWidth = $(window).width(); + var dataTablesScrollBodyPosLeft = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset() + .left; + var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft; + + // jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature. + // We work around that by setting the height for it's container instead. + $(tabElement).find(Constants.htmlSelectors.dataTableScrollContainerSelector).width(scrollWidth); + }); +} + +function initializeEventHandlers(): void { + var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector); + var $firstHeader: JQuery = $headers.first(); + var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex); + + $headers + .on("keydown", (event: JQueryEventObject) => { + Utilities.onEnter(event, ($sourceElement: JQuery) => { + $sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive); + }); + + // Bind shift+tab from first header back to search input field + Utilities.onTab( + event, + ($sourceElement: JQuery) => { + var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex); + + if (sourceIndex === firstIndex) { + event.preventDefault(); + } + }, + /* metaKey */ null, + /* shiftKey */ true, + /* altKey */ null + ); + + // Also reset color if [shift-] tabbing away from button while holding down 'enter' + Utilities.onTab(event, ($sourceElement: JQuery) => { + $sourceElement.css("background-color", ""); + }); + }) + .on("keyup", (event: JQueryEventObject) => { + Utilities.onEnter(event, ($sourceElement: JQuery) => { + $sourceElement.css("background-color", ""); + }); + }); +} + +function updateSelectionStatus(oSettings: any): void { + var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector); + if ($dataTableRows) { + for (var i = 0; i < $dataTableRows.length; i++) { + var $row: JQuery = $dataTableRows.eq(i); + var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr); + var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel; + if (table.isItemSelected(table.getTableEntityKeys(rowKey))) { + $row.attr("tabindex", "0"); + } + } + } + + updateDataTableFocus(oSettings.ajax); + + DataTableOperations.setPaginationButtonEventHandlers(); +} + +// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity. +// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects." +function updateDataTableFocus(queryTablesTabId: string): void { + var $activeElement: JQuery = $(document.activeElement); + var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element. + var storageExplorerFrameHasFocus: boolean = document.hasFocus(); + var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager; + if (operationManager) { + if (isFocusLost && storageExplorerFrameHasFocus) { + // We get here when no control is active, meaning that the table update was triggered + // from a dialog, the context menu or by clicking on a toolbar control or header. + // Note that giving focus to the table also selects the first row if needed. + // The document.hasFocus() ensures that the table will only get focus when the + // focus was lost (i.e. "body has the focus") within the Storage Explorer frame + // i.e. not when the focus is lost because it is in another frame + // e.g. a daytona dialog or in the Activity Log. + operationManager.focusTable(); + } + if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) { + // If table header is selected, focus is shifted to the selected element as part of accessibility + $activeElement && $activeElement.focus(); + } else { + // If some control is active, we don't give focus back to the table, + // just select the first row if needed (empty selection). + operationManager.selectFirstIfNeeded(); + } + } +} + +(ko.bindingHandlers).tableSource = { + init: bindDataTable, + update: dataChanged, +}; + +(ko.bindingHandlers).tableSelection = { + update: selectionChanged, +}; + +(ko.bindingHandlers).readOnly = { + update: function (element: any, valueAccessor: any) { + var value = ko.utils.unwrapObservable(valueAccessor()); + if (value) { + element.setAttribute("readOnly", true); + } else { + element.removeAttribute("readOnly"); + } + }, +}; diff --git a/src/Explorer/Tables/DataTable/DataTableBuilder.ts b/src/Explorer/Tables/DataTable/DataTableBuilder.ts index 9154fe690..2fc3000ba 100644 --- a/src/Explorer/Tables/DataTable/DataTableBuilder.ts +++ b/src/Explorer/Tables/DataTable/DataTableBuilder.ts @@ -1,52 +1,52 @@ -import * as Utilities from "../Utilities"; - -/** - * Wrapper function for creating data tables. Call this method, not the - * data tables constructor when you want to create a data table. This - * function makes sure that content without a render function is properly - * encoded to prevent XSS. - * @param{$dataTableElem} JQuery data table element - * @param{$settings} Settings to use when creating the data table - */ -export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable { - return $dataTableElem.DataTable(applyDefaultRendering(settings)); -} - -/** - * Go through the settings for a data table and apply a simple HTML encode to any column - * without a render function to prevent XSS. - * @param{settings} The settings to check - * @return The given settings with all columns having a rendering function - */ -function applyDefaultRendering(settings: any): DataTables.SettingsLegacy { - var tableColumns: DataTables.ColumnLegacy[] = null; - - if (settings.aoColumns) { - tableColumns = settings.aoColumns; - } else if (settings.aoColumnDefs) { - // for tables we use aoColumnDefs instead of aoColumns - tableColumns = settings.aoColumnDefs; - } - - // either the settings had no columns defined, or they were called - // by a property name which we have not used before - if (!tableColumns) { - return settings; - } - - for (var i = 0; i < tableColumns.length; i++) { - // the column does not have a render function - if (!tableColumns[i].mRender) { - tableColumns[i].mRender = defaultDataRender; - } - } - return settings; -} - -/** - * Default data render function, whatever is done to data in here - * will be done to any data which we do not specify a render for. - */ -function defaultDataRender(data: any, type: string, full: any) { - return Utilities.htmlEncode(data); -} +import * as Utilities from "../Utilities"; + +/** + * Wrapper function for creating data tables. Call this method, not the + * data tables constructor when you want to create a data table. This + * function makes sure that content without a render function is properly + * encoded to prevent XSS. + * @param{$dataTableElem} JQuery data table element + * @param{$settings} Settings to use when creating the data table + */ +export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable { + return $dataTableElem.DataTable(applyDefaultRendering(settings)); +} + +/** + * Go through the settings for a data table and apply a simple HTML encode to any column + * without a render function to prevent XSS. + * @param{settings} The settings to check + * @return The given settings with all columns having a rendering function + */ +function applyDefaultRendering(settings: any): DataTables.SettingsLegacy { + var tableColumns: DataTables.ColumnLegacy[] = null; + + if (settings.aoColumns) { + tableColumns = settings.aoColumns; + } else if (settings.aoColumnDefs) { + // for tables we use aoColumnDefs instead of aoColumns + tableColumns = settings.aoColumnDefs; + } + + // either the settings had no columns defined, or they were called + // by a property name which we have not used before + if (!tableColumns) { + return settings; + } + + for (var i = 0; i < tableColumns.length; i++) { + // the column does not have a render function + if (!tableColumns[i].mRender) { + tableColumns[i].mRender = defaultDataRender; + } + } + return settings; +} + +/** + * Default data render function, whatever is done to data in here + * will be done to any data which we do not specify a render for. + */ +function defaultDataRender(data: any, type: string, full: any) { + return Utilities.htmlEncode(data); +} diff --git a/src/Explorer/Tables/DataTable/DataTableOperationManager.ts b/src/Explorer/Tables/DataTable/DataTableOperationManager.ts index c3a195ef8..7be7c4aaa 100644 --- a/src/Explorer/Tables/DataTable/DataTableOperationManager.ts +++ b/src/Explorer/Tables/DataTable/DataTableOperationManager.ts @@ -1,300 +1,300 @@ -import ko from "knockout"; - -import * as DataTableOperations from "./DataTableOperations"; -import * as Constants from "../Constants"; -import TableCommands from "./TableCommands"; -import TableEntityListViewModel from "./TableEntityListViewModel"; -import * as Utilities from "../Utilities"; -import * as Entities from "../Entities"; - -/* - * Base class for data table row selection. - */ -export default class DataTableOperationManager { - private _tableEntityListViewModel: TableEntityListViewModel; - private _tableCommands: TableCommands; - private dataTable: JQuery; - - constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) { - this.dataTable = table; - this._tableEntityListViewModel = viewModel; - this._tableCommands = tableCommands; - - this.bind(); - this._tableEntityListViewModel.bind(this); - } - - private click = (event: JQueryEventObject) => { - var elem: JQuery = $(event.currentTarget); - this.updateLastSelectedItem(elem, event.shiftKey); - - if (Utilities.isEnvironmentCtrlPressed(event)) { - this.applyCtrlSelection(elem); - } else if (event.shiftKey) { - this.applyShiftSelection(elem); - } else { - this.applySingleSelection(elem); - } - }; - - private doubleClick = (event: JQueryEventObject) => { - this.tryOpenEditor(); - }; - - private keyDown = (event: JQueryEventObject): boolean => { - var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow, - isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow, - handled: boolean = false; - - if (isUpArrowKey || isDownArrowKey) { - var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem; - var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector); - var maximumIndex = dataTableRows.length - 1; - - // If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page. - // Simply select the first item in this case. - var lastSelectedItemIndex = lastSelectedItem - ? this._tableEntityListViewModel.getItemIndexFromCurrentPage( - this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._) - ) - : -1; - var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1; - var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex); - var selectedRowElement: JQuery = dataTableRows.eq(safeIndex); - - if (selectedRowElement) { - if (event.shiftKey) { - this.applyShiftSelection(selectedRowElement); - } else { - this.applySingleSelection(selectedRowElement); - } - - this.updateLastSelectedItem(selectedRowElement, event.shiftKey); - handled = true; - DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey); - } - } else if ( - Utilities.isEnvironmentCtrlPressed(event) && - !Utilities.isEnvironmentShiftPressed(event) && - !Utilities.isEnvironmentAltPressed(event) && - event.keyCode === Constants.keyCodes.A - ) { - this.applySelectAll(); - handled = true; - } - - return !handled; - }; - - // Note: There is one key up event each time a key is pressed; - // in contrast, there may be more than one key down and key - // pressed events. - private keyUp = (event: JQueryEventObject): boolean => { - var handled: boolean = false; - - switch (event.keyCode) { - case Constants.keyCodes.Enter: - handled = this.tryOpenEditor(); - break; - case Constants.keyCodes.Delete: - handled = this.tryHandleDeleteSelected(); - break; - } - - return !handled; - }; - - private itemDropped = (event: JQueryEventObject): boolean => { - var handled: boolean = false; - var items = (event.originalEvent).dataTransfer.items; - - if (!items) { - // On browsers outside of Chromium - // we can't discern between dirs and files - // so we will disable drag & drop for now - return null; - } - - for (var i = 0; i < items.length; i++) { - var item = items[i]; - var entry = item.webkitGetAsEntry(); - - if (entry.isFile) { - // TODO: parse the file and insert content as entities - } - } - - return !handled; - }; - - private tryOpenEditor(): boolean { - return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel); - } - - private tryHandleDeleteSelected(): boolean { - var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected(); - var handled: boolean = false; - - if (selectedEntities && selectedEntities.length) { - this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel); - handled = true; - } - - return handled; - } - - private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity { - return { - RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr) - }; - } - - private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) { - var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); - var entity = this._tableEntityListViewModel.getItemFromCurrentPage( - this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) - ); - - this._tableEntityListViewModel.lastSelectedItem = entity; - - if (!isShiftSelect) { - this._tableEntityListViewModel.lastSelectedAnchorItem = entity; - } - } - - private applySingleSelection($elem: JQuery) { - if ($elem) { - var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); - - this._tableEntityListViewModel.clearSelection(); - this.addToSelection(entityIdentity.RowKey); - } - } - - private applySelectAll() { - this._tableEntityListViewModel.clearSelection(); - ko.utils.arrayPushAll( - this._tableEntityListViewModel.selected, - this._tableEntityListViewModel.getAllItemsInCurrentPage() - ); - } - - private applyCtrlSelection($elem: JQuery): void { - var koSelected: ko.ObservableArray = this._tableEntityListViewModel - ? this._tableEntityListViewModel.selected - : null; - - if (koSelected) { - var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); - - if ( - !this._tableEntityListViewModel.isItemSelected( - this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) - ) - ) { - // Adding item not previously in selection - this.addToSelection(entityIdentity.RowKey); - } else { - koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey); - } - } - } - - private applyShiftSelection($elem: JQuery): void { - var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem; - - // If anchor item doesn't exist, use the first available item of current page instead - if (!anchorItem && this._tableEntityListViewModel.items().length > 0) { - anchorItem = this._tableEntityListViewModel.items()[0]; - } - - if (anchorItem) { - var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); - var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages( - this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) - ); - var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages( - this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._) - ); - - var startIndex = Math.min(elementIndex, anchorIndex); - var endIndex = Math.max(elementIndex, anchorIndex); - - this._tableEntityListViewModel.clearSelection(); - ko.utils.arrayPushAll( - this._tableEntityListViewModel.selected, - this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1) - ); - } - } - - private applyContextMenuSelection($elem: JQuery) { - var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); - - if ( - !this._tableEntityListViewModel.isItemSelected( - this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) - ) - ) { - if (this._tableEntityListViewModel.selected().length) { - this._tableEntityListViewModel.clearSelection(); - } - this.addToSelection(entityIdentity.RowKey); - } - } - - private addToSelection(rowKey: string) { - var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage( - this._tableEntityListViewModel.getTableEntityKeys(rowKey) - ); - - if (selectedEntity != null) { - this._tableEntityListViewModel.selected.push(selectedEntity); - } - } - - // Selecting first row if the selection is empty. - public selectFirstIfNeeded(): void { - var koSelected: ko.ObservableArray = this._tableEntityListViewModel - ? this._tableEntityListViewModel.selected - : null; - var koEntities: ko.ObservableArray = this._tableEntityListViewModel - ? this._tableEntityListViewModel.items - : null; - - if (!koSelected().length && koEntities().length) { - var firstEntity: Entities.ITableEntity = koEntities()[0]; - - // Clear last selection: lastSelectedItem and lastSelectedAnchorItem - this._tableEntityListViewModel.clearLastSelected(); - - this.addToSelection(firstEntity.RowKey._); - - // Update last selection - this._tableEntityListViewModel.lastSelectedItem = firstEntity; - - // Finally, make sure first row is visible - DataTableOperations.scrollToTopIfNeeded(); - } - } - - public bind() { - this.dataTable.on("click", "tr", this.click); - this.dataTable.on("dblclick", "tr", this.doubleClick); - this.dataTable.on("keydown", "td", this.keyDown); - this.dataTable.on("keyup", "td", this.keyUp); - - // Keyboard navigation - selecting first row if the selection is empty when the table gains focus. - this.dataTable.on("focus", () => { - this.selectFirstIfNeeded(); - return true; - }); - - // Bind drag & drop behavior - $("body").on("drop", this.itemDropped); - } - - public focusTable(): void { - this.dataTable.focus(); - } -} +import ko from "knockout"; + +import * as DataTableOperations from "./DataTableOperations"; +import * as Constants from "../Constants"; +import TableCommands from "./TableCommands"; +import TableEntityListViewModel from "./TableEntityListViewModel"; +import * as Utilities from "../Utilities"; +import * as Entities from "../Entities"; + +/* + * Base class for data table row selection. + */ +export default class DataTableOperationManager { + private _tableEntityListViewModel: TableEntityListViewModel; + private _tableCommands: TableCommands; + private dataTable: JQuery; + + constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) { + this.dataTable = table; + this._tableEntityListViewModel = viewModel; + this._tableCommands = tableCommands; + + this.bind(); + this._tableEntityListViewModel.bind(this); + } + + private click = (event: JQueryEventObject) => { + var elem: JQuery = $(event.currentTarget); + this.updateLastSelectedItem(elem, event.shiftKey); + + if (Utilities.isEnvironmentCtrlPressed(event)) { + this.applyCtrlSelection(elem); + } else if (event.shiftKey) { + this.applyShiftSelection(elem); + } else { + this.applySingleSelection(elem); + } + }; + + private doubleClick = (event: JQueryEventObject) => { + this.tryOpenEditor(); + }; + + private keyDown = (event: JQueryEventObject): boolean => { + var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow, + isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow, + handled: boolean = false; + + if (isUpArrowKey || isDownArrowKey) { + var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem; + var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector); + var maximumIndex = dataTableRows.length - 1; + + // If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page. + // Simply select the first item in this case. + var lastSelectedItemIndex = lastSelectedItem + ? this._tableEntityListViewModel.getItemIndexFromCurrentPage( + this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._) + ) + : -1; + var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1; + var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex); + var selectedRowElement: JQuery = dataTableRows.eq(safeIndex); + + if (selectedRowElement) { + if (event.shiftKey) { + this.applyShiftSelection(selectedRowElement); + } else { + this.applySingleSelection(selectedRowElement); + } + + this.updateLastSelectedItem(selectedRowElement, event.shiftKey); + handled = true; + DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey); + } + } else if ( + Utilities.isEnvironmentCtrlPressed(event) && + !Utilities.isEnvironmentShiftPressed(event) && + !Utilities.isEnvironmentAltPressed(event) && + event.keyCode === Constants.keyCodes.A + ) { + this.applySelectAll(); + handled = true; + } + + return !handled; + }; + + // Note: There is one key up event each time a key is pressed; + // in contrast, there may be more than one key down and key + // pressed events. + private keyUp = (event: JQueryEventObject): boolean => { + var handled: boolean = false; + + switch (event.keyCode) { + case Constants.keyCodes.Enter: + handled = this.tryOpenEditor(); + break; + case Constants.keyCodes.Delete: + handled = this.tryHandleDeleteSelected(); + break; + } + + return !handled; + }; + + private itemDropped = (event: JQueryEventObject): boolean => { + var handled: boolean = false; + var items = (event.originalEvent).dataTransfer.items; + + if (!items) { + // On browsers outside of Chromium + // we can't discern between dirs and files + // so we will disable drag & drop for now + return null; + } + + for (var i = 0; i < items.length; i++) { + var item = items[i]; + var entry = item.webkitGetAsEntry(); + + if (entry.isFile) { + // TODO: parse the file and insert content as entities + } + } + + return !handled; + }; + + private tryOpenEditor(): boolean { + return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel); + } + + private tryHandleDeleteSelected(): boolean { + var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected(); + var handled: boolean = false; + + if (selectedEntities && selectedEntities.length) { + this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel); + handled = true; + } + + return handled; + } + + private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity { + return { + RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr), + }; + } + + private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) { + var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); + var entity = this._tableEntityListViewModel.getItemFromCurrentPage( + this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) + ); + + this._tableEntityListViewModel.lastSelectedItem = entity; + + if (!isShiftSelect) { + this._tableEntityListViewModel.lastSelectedAnchorItem = entity; + } + } + + private applySingleSelection($elem: JQuery) { + if ($elem) { + var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); + + this._tableEntityListViewModel.clearSelection(); + this.addToSelection(entityIdentity.RowKey); + } + } + + private applySelectAll() { + this._tableEntityListViewModel.clearSelection(); + ko.utils.arrayPushAll( + this._tableEntityListViewModel.selected, + this._tableEntityListViewModel.getAllItemsInCurrentPage() + ); + } + + private applyCtrlSelection($elem: JQuery): void { + var koSelected: ko.ObservableArray = this._tableEntityListViewModel + ? this._tableEntityListViewModel.selected + : null; + + if (koSelected) { + var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); + + if ( + !this._tableEntityListViewModel.isItemSelected( + this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) + ) + ) { + // Adding item not previously in selection + this.addToSelection(entityIdentity.RowKey); + } else { + koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey); + } + } + } + + private applyShiftSelection($elem: JQuery): void { + var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem; + + // If anchor item doesn't exist, use the first available item of current page instead + if (!anchorItem && this._tableEntityListViewModel.items().length > 0) { + anchorItem = this._tableEntityListViewModel.items()[0]; + } + + if (anchorItem) { + var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); + var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages( + this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) + ); + var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages( + this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._) + ); + + var startIndex = Math.min(elementIndex, anchorIndex); + var endIndex = Math.max(elementIndex, anchorIndex); + + this._tableEntityListViewModel.clearSelection(); + ko.utils.arrayPushAll( + this._tableEntityListViewModel.selected, + this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1) + ); + } + } + + private applyContextMenuSelection($elem: JQuery) { + var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); + + if ( + !this._tableEntityListViewModel.isItemSelected( + this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey) + ) + ) { + if (this._tableEntityListViewModel.selected().length) { + this._tableEntityListViewModel.clearSelection(); + } + this.addToSelection(entityIdentity.RowKey); + } + } + + private addToSelection(rowKey: string) { + var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage( + this._tableEntityListViewModel.getTableEntityKeys(rowKey) + ); + + if (selectedEntity != null) { + this._tableEntityListViewModel.selected.push(selectedEntity); + } + } + + // Selecting first row if the selection is empty. + public selectFirstIfNeeded(): void { + var koSelected: ko.ObservableArray = this._tableEntityListViewModel + ? this._tableEntityListViewModel.selected + : null; + var koEntities: ko.ObservableArray = this._tableEntityListViewModel + ? this._tableEntityListViewModel.items + : null; + + if (!koSelected().length && koEntities().length) { + var firstEntity: Entities.ITableEntity = koEntities()[0]; + + // Clear last selection: lastSelectedItem and lastSelectedAnchorItem + this._tableEntityListViewModel.clearLastSelected(); + + this.addToSelection(firstEntity.RowKey._); + + // Update last selection + this._tableEntityListViewModel.lastSelectedItem = firstEntity; + + // Finally, make sure first row is visible + DataTableOperations.scrollToTopIfNeeded(); + } + } + + public bind() { + this.dataTable.on("click", "tr", this.click); + this.dataTable.on("dblclick", "tr", this.doubleClick); + this.dataTable.on("keydown", "td", this.keyDown); + this.dataTable.on("keyup", "td", this.keyUp); + + // Keyboard navigation - selecting first row if the selection is empty when the table gains focus. + this.dataTable.on("focus", () => { + this.selectFirstIfNeeded(); + return true; + }); + + // Bind drag & drop behavior + $("body").on("drop", this.itemDropped); + } + + public focusTable(): void { + this.dataTable.focus(); + } +} diff --git a/src/Explorer/Tables/DataTable/DataTableOperations.ts b/src/Explorer/Tables/DataTable/DataTableOperations.ts index ef2033db8..0111787a3 100644 --- a/src/Explorer/Tables/DataTable/DataTableOperations.ts +++ b/src/Explorer/Tables/DataTable/DataTableOperations.ts @@ -1,192 +1,192 @@ -import _ from "underscore"; -import Q from "q"; - -import * as Entities from "../Entities"; -import * as QueryBuilderConstants from "../Constants"; -import * as Utilities from "../Utilities"; - -export function getRowSelector(selectorSchema: Entities.IProperty[]): string { - var selector: string = ""; - selectorSchema && - selectorSchema.forEach((p: Entities.IProperty) => { - selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]'; - }); - return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector; -} - -export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean { - var isVisible = false; - - if (dataTableScrollBodyQuery.length && element) { - var elementRect: ClientRect = element.getBoundingClientRect(), - dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect(); - - isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top; - } - - return isVisible; -} - -export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void { - if (dataTableRows.length) { - var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector), - selectedRowElement: HTMLElement = dataTableRows.get(currentIndex); - - if (dataTableScrollBodyQuery.length && selectedRowElement) { - var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement); - - if (!isVisible) { - var selectedRowQuery: JQuery = $(selectedRowElement), - scrollPosition: number = dataTableScrollBodyQuery.scrollTop(), - selectedElementPosition: number = selectedRowQuery.position().top, - newScrollPosition: number = 0; - - if (isScrollUp) { - newScrollPosition = scrollPosition + selectedElementPosition; - } else { - newScrollPosition = - scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height()); - } - - dataTableScrollBodyQuery.scrollTop(newScrollPosition); - } - } - } -} - -export function scrollToTopIfNeeded(): void { - var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector), - $dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector); - - if ($dataTableRows.length && $dataTableScrollBody.length) { - $dataTableScrollBody.scrollTop(0); - } -} - -export function setPaginationButtonEventHandlers(): void { - $(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector) - .on("mousedown", (event: JQueryEventObject) => { - // Prevents the table contents from briefly jumping when clicking on "Load more" - event.preventDefault(); - }) - .attr("role", "button"); -} - -export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void { - settings && - settings.forEach((value: boolean, index: number) => { - table.column(index).visible(value, false); - }); - table.columns.adjust().draw(false); -} - -/** - * Reorder columns based on current order. - * If no current order is specified, reorder the columns based on intial order. - */ -export function reorderColumns( - table: DataTables.DataTable, - targetOrder: number[], - currentOrder?: number[] -): Q.Promise { - var columnsCount: number = targetOrder.length; - var isCurrentOrderPassedIn: boolean = !!currentOrder; - if (!isCurrentOrderPassedIn) { - currentOrder = getInitialOrder(columnsCount); - } - var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder); - - // if the targetOrder is the same as current order, do nothing. - if (!isSameOrder) { - // Otherwise, calculate the transformation order. - // If current order not specified, then it'll be set to initial order, - // i.e., either no reorder happened before or reordering to its initial order, - // Then the transformation order will be the same as target order. - // If current order is specified, then a transformation order is calculated. - // Refer to calculateTransformationOrder for details about transformation order. - var transformationOrder: number[] = isCurrentOrderPassedIn - ? calculateTransformationOrder(currentOrder, targetOrder) - : targetOrder; - try { - $.fn.dataTable.ColReorder(table).fnOrder(transformationOrder); - } catch (err) { - return Q.reject(err); - } - } - return Q.resolve(null); -} - -export function resetColumns(table: DataTables.DataTable): void { - $.fn.dataTable.ColReorder(table).fnReset(); -} - -/** - * A table's initial order is described in the form of a natural ascending order. - * E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8] - */ -export function getInitialOrder(columnsCount: number): number[] { - return _.range(columnsCount); -} - -/** - * Get current table's column order which is described based on initial table. E.g., - * Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8} - * Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8} - */ -export function getCurrentOrder(table: DataTables.DataTable): number[] { - return $.fn.dataTable.ColReorder(table).fnOrder(); -} - -/** - * Switch the index and value for each element of an array. e.g., - * InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8] - * Result: [0, 1, 2, 5, 6, 7, 3, 4, 8] - */ -export function invertIndexValues(inputArray: number[]): number[] { - var invertedArray: number[] = []; - if (inputArray) { - inputArray.forEach((value: number, index: number) => { - invertedArray[inputArray[index]] = index; - }); - } - - return invertedArray; -} - -/** - * DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table. - * An detailed example for this. Assume the table has 9 columns. - * Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8} - * Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8} - * Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4} - * Transformation order: an order passed to fnOrder API that transforms table from current order to target order. - * When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g., - * column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6. - * Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as - * the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated. - * Here is an example of how to calculate the transformation order: - * In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5. - * As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the - * transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6] - */ -export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] { - var transformationOrder: number[] = []; - if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) { - var invertedCurrentOrder: number[] = invertIndexValues(currentOrder); - transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]); - } - return transformationOrder; -} - -export function getDataTableHeaders(table: DataTables.DataTable): string[] { - var columns: DataTables.ColumnsMethods = table.columns(); - var headers: string[] = []; - if (columns) { - // table.columns() return ColumnsMethods which is an array of arrays - var columnIndexes: number[] = (columns)[0]; - if (columnIndexes) { - headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html()); - } - } - return headers; -} +import _ from "underscore"; +import Q from "q"; + +import * as Entities from "../Entities"; +import * as QueryBuilderConstants from "../Constants"; +import * as Utilities from "../Utilities"; + +export function getRowSelector(selectorSchema: Entities.IProperty[]): string { + var selector: string = ""; + selectorSchema && + selectorSchema.forEach((p: Entities.IProperty) => { + selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]'; + }); + return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector; +} + +export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean { + var isVisible = false; + + if (dataTableScrollBodyQuery.length && element) { + var elementRect: ClientRect = element.getBoundingClientRect(), + dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect(); + + isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top; + } + + return isVisible; +} + +export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void { + if (dataTableRows.length) { + var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector), + selectedRowElement: HTMLElement = dataTableRows.get(currentIndex); + + if (dataTableScrollBodyQuery.length && selectedRowElement) { + var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement); + + if (!isVisible) { + var selectedRowQuery: JQuery = $(selectedRowElement), + scrollPosition: number = dataTableScrollBodyQuery.scrollTop(), + selectedElementPosition: number = selectedRowQuery.position().top, + newScrollPosition: number = 0; + + if (isScrollUp) { + newScrollPosition = scrollPosition + selectedElementPosition; + } else { + newScrollPosition = + scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height()); + } + + dataTableScrollBodyQuery.scrollTop(newScrollPosition); + } + } + } +} + +export function scrollToTopIfNeeded(): void { + var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector), + $dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector); + + if ($dataTableRows.length && $dataTableScrollBody.length) { + $dataTableScrollBody.scrollTop(0); + } +} + +export function setPaginationButtonEventHandlers(): void { + $(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector) + .on("mousedown", (event: JQueryEventObject) => { + // Prevents the table contents from briefly jumping when clicking on "Load more" + event.preventDefault(); + }) + .attr("role", "button"); +} + +export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void { + settings && + settings.forEach((value: boolean, index: number) => { + table.column(index).visible(value, false); + }); + table.columns.adjust().draw(false); +} + +/** + * Reorder columns based on current order. + * If no current order is specified, reorder the columns based on intial order. + */ +export function reorderColumns( + table: DataTables.DataTable, + targetOrder: number[], + currentOrder?: number[] +): Q.Promise { + var columnsCount: number = targetOrder.length; + var isCurrentOrderPassedIn: boolean = !!currentOrder; + if (!isCurrentOrderPassedIn) { + currentOrder = getInitialOrder(columnsCount); + } + var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder); + + // if the targetOrder is the same as current order, do nothing. + if (!isSameOrder) { + // Otherwise, calculate the transformation order. + // If current order not specified, then it'll be set to initial order, + // i.e., either no reorder happened before or reordering to its initial order, + // Then the transformation order will be the same as target order. + // If current order is specified, then a transformation order is calculated. + // Refer to calculateTransformationOrder for details about transformation order. + var transformationOrder: number[] = isCurrentOrderPassedIn + ? calculateTransformationOrder(currentOrder, targetOrder) + : targetOrder; + try { + $.fn.dataTable.ColReorder(table).fnOrder(transformationOrder); + } catch (err) { + return Q.reject(err); + } + } + return Q.resolve(null); +} + +export function resetColumns(table: DataTables.DataTable): void { + $.fn.dataTable.ColReorder(table).fnReset(); +} + +/** + * A table's initial order is described in the form of a natural ascending order. + * E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8] + */ +export function getInitialOrder(columnsCount: number): number[] { + return _.range(columnsCount); +} + +/** + * Get current table's column order which is described based on initial table. E.g., + * Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8} + * Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8} + */ +export function getCurrentOrder(table: DataTables.DataTable): number[] { + return $.fn.dataTable.ColReorder(table).fnOrder(); +} + +/** + * Switch the index and value for each element of an array. e.g., + * InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8] + * Result: [0, 1, 2, 5, 6, 7, 3, 4, 8] + */ +export function invertIndexValues(inputArray: number[]): number[] { + var invertedArray: number[] = []; + if (inputArray) { + inputArray.forEach((value: number, index: number) => { + invertedArray[inputArray[index]] = index; + }); + } + + return invertedArray; +} + +/** + * DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table. + * An detailed example for this. Assume the table has 9 columns. + * Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8} + * Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8} + * Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4} + * Transformation order: an order passed to fnOrder API that transforms table from current order to target order. + * When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g., + * column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6. + * Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as + * the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated. + * Here is an example of how to calculate the transformation order: + * In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5. + * As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the + * transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6] + */ +export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] { + var transformationOrder: number[] = []; + if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) { + var invertedCurrentOrder: number[] = invertIndexValues(currentOrder); + transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]); + } + return transformationOrder; +} + +export function getDataTableHeaders(table: DataTables.DataTable): string[] { + var columns: DataTables.ColumnsMethods = table.columns(); + var headers: string[] = []; + if (columns) { + // table.columns() return ColumnsMethods which is an array of arrays + var columnIndexes: number[] = (columns)[0]; + if (columnIndexes) { + headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html()); + } + } + return headers; +} diff --git a/src/Explorer/Tables/DataTable/DataTableUtilities.ts b/src/Explorer/Tables/DataTable/DataTableUtilities.ts index f029c89d4..403d45d90 100644 --- a/src/Explorer/Tables/DataTable/DataTableUtilities.ts +++ b/src/Explorer/Tables/DataTable/DataTableUtilities.ts @@ -1,148 +1,148 @@ -import * as _ from "underscore"; -import * as Constants from "../Constants"; -import * as Entities from "../Entities"; -import * as TableEntityProcessor from "../TableEntityProcessor"; - -export enum IconState { - default, - hoverState, - toggleOn -} - -/** - * Represents an html input element shown in context menu. - * name: the input name - * type: the input type, e.g., "text", "checkbox", "radio", etc. - * selected: optional. Used when the input type is checkbox. True means checkbox is selected. Otherwise, unselected. - */ -export interface IContextMenuInputItem { - name: string; - type: string; - selected?: boolean; -} - -export interface IContextMenuOption { - [key: string]: IContextMenuInputItem; -} - -export function containMultipleItems(items: T[]): boolean { - return items && items.length > 1; -} - -export function containSingleItem(items: T[]): boolean { - return items && items.length === 1; -} - -export function containItems(items: T[]): boolean { - return items && items.length > 0; -} - -// export function setTargetIcon(idToIconHandlerMap: CloudHub.Common.IToolbarElementIdIconMap, $sourceElement: JQuery, toIconState: IconState): void { -// if (idToIconHandlerMap) { -// var iconId: string = $sourceElement.attr("id"); -// var iconHandler = idToIconHandlerMap[iconId]; -// switch (toIconState) { -// case IconState.default: -// iconHandler.observable(iconHandler.default); -// break; -// case IconState.hoverState: -// iconHandler.observable(iconHandler.hoverState); -// break; -// default: -// window.console.log("error"); -// } -// } -// } - -export function addCssClass($sourceElement: JQuery, cssClassName: string): void { - if (!$sourceElement.hasClass(cssClassName)) { - $sourceElement.addClass(cssClassName); - } -} - -export function removeCssClass($sourceElement: JQuery, cssClassName: string): void { - if ($sourceElement.hasClass(cssClassName)) { - $sourceElement.removeClass(cssClassName); - } -} - -/** - * Get the property union of input entities. - * Example: - * Input: - * Entities: [{ PrimaryKey, id, Prop1, Prop2 }, { PrimaryKey, id, Prop2, Prop3, Prop4 }] - * Return: - * Union: [PrimaryKey, id, Prop1, Prop2, Prop3, Prop4] - */ -export function getPropertyIntersectionFromTableEntities( - entities: Entities.ITableEntity[], - isCassandraApi: boolean -): string[] { - var headerUnion: string[] = []; - entities && - entities.forEach((row: any) => { - const keys = Object.keys(row); - keys && - keys.forEach((key: string) => { - if ( - key !== ".metadata" && - !_.contains(headerUnion, key) && - key !== TableEntityProcessor.keyProperties.attachments && - key !== TableEntityProcessor.keyProperties.etag && - key !== TableEntityProcessor.keyProperties.resourceId && - key !== TableEntityProcessor.keyProperties.self && - (!isCassandraApi || key !== Constants.EntityKeyNames.RowKey) - ) { - headerUnion.push(key); - } - }); - }); - return headerUnion; -} - -/** - * Compares the names of two Azure table columns and returns a number indicating which comes before the other. - * System-defined properties come before custom properties. Otherwise they are compared using string comparison. - */ -export function compareTableColumns(a: string, b: string): number { - if (a === "PartitionKey") { - if (b !== "PartitionKey") { - return -1; - } - } else if (a === "RowKey") { - if (b === "PartitionKey") { - return 1; - } else if (b !== "RowKey") { - return -1; - } - } else if (a === "Timestamp") { - if (b === "PartitionKey" || b === "RowKey") { - return 1; - } else if (b !== "Timestamp") { - return -1; - } - } else if (b === "PartitionKey" || b === "RowKey" || b === "Timestamp") { - return 1; - } - - return a.localeCompare(b); -} - -export function checkForDefaultHeader(headers: string[]): boolean { - return headers[0] === Constants.defaultHeader; -} - -/** - * DataTableBindingManager registers an event handler of body.resize and recalculates the data table size. - * This method forces the event to happen. - */ -export function forceRecalculateTableSize(): void { - $("body").trigger("resize"); -} - -/** - * Turns off the spinning progress indicator on the data table. - */ -export function turnOffProgressIndicator(): void { - $("div.dataTables_processing").hide(); -} +import * as _ from "underscore"; +import * as Constants from "../Constants"; +import * as Entities from "../Entities"; +import * as TableEntityProcessor from "../TableEntityProcessor"; + +export enum IconState { + default, + hoverState, + toggleOn, +} + +/** + * Represents an html input element shown in context menu. + * name: the input name + * type: the input type, e.g., "text", "checkbox", "radio", etc. + * selected: optional. Used when the input type is checkbox. True means checkbox is selected. Otherwise, unselected. + */ +export interface IContextMenuInputItem { + name: string; + type: string; + selected?: boolean; +} + +export interface IContextMenuOption { + [key: string]: IContextMenuInputItem; +} + +export function containMultipleItems(items: T[]): boolean { + return items && items.length > 1; +} + +export function containSingleItem(items: T[]): boolean { + return items && items.length === 1; +} + +export function containItems(items: T[]): boolean { + return items && items.length > 0; +} + +// export function setTargetIcon(idToIconHandlerMap: CloudHub.Common.IToolbarElementIdIconMap, $sourceElement: JQuery, toIconState: IconState): void { +// if (idToIconHandlerMap) { +// var iconId: string = $sourceElement.attr("id"); +// var iconHandler = idToIconHandlerMap[iconId]; +// switch (toIconState) { +// case IconState.default: +// iconHandler.observable(iconHandler.default); +// break; +// case IconState.hoverState: +// iconHandler.observable(iconHandler.hoverState); +// break; +// default: +// window.console.log("error"); +// } +// } +// } + +export function addCssClass($sourceElement: JQuery, cssClassName: string): void { + if (!$sourceElement.hasClass(cssClassName)) { + $sourceElement.addClass(cssClassName); + } +} + +export function removeCssClass($sourceElement: JQuery, cssClassName: string): void { + if ($sourceElement.hasClass(cssClassName)) { + $sourceElement.removeClass(cssClassName); + } +} + +/** + * Get the property union of input entities. + * Example: + * Input: + * Entities: [{ PrimaryKey, id, Prop1, Prop2 }, { PrimaryKey, id, Prop2, Prop3, Prop4 }] + * Return: + * Union: [PrimaryKey, id, Prop1, Prop2, Prop3, Prop4] + */ +export function getPropertyIntersectionFromTableEntities( + entities: Entities.ITableEntity[], + isCassandraApi: boolean +): string[] { + var headerUnion: string[] = []; + entities && + entities.forEach((row: any) => { + const keys = Object.keys(row); + keys && + keys.forEach((key: string) => { + if ( + key !== ".metadata" && + !_.contains(headerUnion, key) && + key !== TableEntityProcessor.keyProperties.attachments && + key !== TableEntityProcessor.keyProperties.etag && + key !== TableEntityProcessor.keyProperties.resourceId && + key !== TableEntityProcessor.keyProperties.self && + (!isCassandraApi || key !== Constants.EntityKeyNames.RowKey) + ) { + headerUnion.push(key); + } + }); + }); + return headerUnion; +} + +/** + * Compares the names of two Azure table columns and returns a number indicating which comes before the other. + * System-defined properties come before custom properties. Otherwise they are compared using string comparison. + */ +export function compareTableColumns(a: string, b: string): number { + if (a === "PartitionKey") { + if (b !== "PartitionKey") { + return -1; + } + } else if (a === "RowKey") { + if (b === "PartitionKey") { + return 1; + } else if (b !== "RowKey") { + return -1; + } + } else if (a === "Timestamp") { + if (b === "PartitionKey" || b === "RowKey") { + return 1; + } else if (b !== "Timestamp") { + return -1; + } + } else if (b === "PartitionKey" || b === "RowKey" || b === "Timestamp") { + return 1; + } + + return a.localeCompare(b); +} + +export function checkForDefaultHeader(headers: string[]): boolean { + return headers[0] === Constants.defaultHeader; +} + +/** + * DataTableBindingManager registers an event handler of body.resize and recalculates the data table size. + * This method forces the event to happen. + */ +export function forceRecalculateTableSize(): void { + $("body").trigger("resize"); +} + +/** + * Turns off the spinning progress indicator on the data table. + */ +export function turnOffProgressIndicator(): void { + $("div.dataTables_processing").hide(); +} diff --git a/src/Explorer/Tables/DataTable/DataTableViewModel.ts b/src/Explorer/Tables/DataTable/DataTableViewModel.ts index f36e6eba7..3cde33ff8 100644 --- a/src/Explorer/Tables/DataTable/DataTableViewModel.ts +++ b/src/Explorer/Tables/DataTable/DataTableViewModel.ts @@ -1,270 +1,270 @@ -import * as ko from "knockout"; -import * as _ from "underscore"; - -import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; -import CacheBase from "./CacheBase"; -import * as CommonConstants from "../../../Common/Constants"; -import * as Constants from "../Constants"; -import * as Entities from "../Entities"; -import QueryTablesTab from "../../Tabs/QueryTablesTab"; -import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; - -// This is the format of the data we will have to pass to Datatable render callback, -// and property names are defined by Datatable as well. -export interface IDataTableRenderData { - draw: number; - aaData: any; - recordsTotal: number; - recordsFiltered: number; -} - -abstract class DataTableViewModel { - private static lastPageLabel = ">>"; // Localize - private static loadMoreLabel = "Load more"; // Localize - - /* Observables */ - public items = ko.observableArray(); - public selected = ko.observableArray(); - - public table: DataTables.DataTable; - - // The anchor item is for shift selection. i.e., select all items between anchor item and a give item. - public lastSelectedAnchorItem: Entities.ITableEntity; - public lastSelectedItem: Entities.ITableEntity; - - public cache: CacheBase; - - protected continuationToken: any; - protected allDownloaded: boolean; - protected lastPrefetchTime: number; - protected downloadSize = 300; - protected _documentIterator: QueryIterator; - - // Used by table redraw throttling - protected pollingInterval = 1000; - private redrawInterval = 500; - private pendingRedraw = false; - private lastRedrawTime = new Date().getTime(); - - private dataTableOperationManager: IDataTableOperation; - - public queryTablesTab: QueryTablesTab; - - constructor() { - this.items([]); - this.selected([]); - // Late bound - this.dataTableOperationManager = null; - } - - public bind(dataTableOperationManager: IDataTableOperation): void { - this.dataTableOperationManager = dataTableOperationManager; - } - - public clearLastSelected(): void { - this.lastSelectedItem = null; - this.lastSelectedAnchorItem = null; - } - - public clearCache(): void { - this.cache.clear(); - this._documentIterator = null; - this.continuationToken = null; - this.allDownloaded = false; - } - - public clearSelection(): void { - this.selected.removeAll(); - } - - // Redraws the table, but guarantees that multiple sequential calls will not incur - // another redraw until a certain time interval has passed. - public redrawTableThrottled() { - if (!this.pendingRedraw) { - this.pendingRedraw = true; - - var current = new Date().getTime(); - var timeSinceLastRedraw = current - this.lastRedrawTime; - var redraw = () => { - this.table.draw(false /*reset*/); - this.lastRedrawTime = new Date().getTime(); - this.pendingRedraw = false; - }; - - if (timeSinceLastRedraw > this.redrawInterval) { - redraw(); - } else { - var timeUntilNextRedraw = this.redrawInterval - timeSinceLastRedraw; - setTimeout(() => redraw(), timeUntilNextRedraw); - } - } - } - - public focusDataTable(): void { - this.dataTableOperationManager.focusTable(); - } - - public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity { - return _.find(this.selected(), (item: Entities.ITableEntity) => { - return this.matchesKeys(item, itemKeys); - }); - } - - public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity { - return _.find(this.items(), (item: Entities.ITableEntity) => { - return this.matchesKeys(item, itemKeys); - }); - } - - public getItemIndexFromCurrentPage(itemKeys: Entities.IProperty[]): number { - return _.findIndex(this.items(), (item: Entities.ITableEntity) => { - return this.matchesKeys(item, itemKeys); - }); - } - - public getItemIndexFromAllPages(itemKeys: Entities.IProperty[]): number { - return _.findIndex(this.cache.data, (item: Entities.ITableEntity) => { - return this.matchesKeys(item, itemKeys); - }); - } - - public getItemsFromAllPagesWithinRange(start: number, end: number): Entities.ITableEntity[] { - return this.cache.data.slice(start, end); - } - - public isItemSelected(itemKeys: Entities.IProperty[]): boolean { - return _.some(this.selected(), (item: Entities.ITableEntity) => { - return this.matchesKeys(item, itemKeys); - }); - } - - public isItemCached(itemKeys: Entities.IProperty[]): boolean { - return _.some(this.cache.data, (item: Entities.ITableEntity) => { - return this.matchesKeys(item, itemKeys); - }); - } - - public getAllItemsInCurrentPage(): Entities.ITableEntity[] { - return this.items(); - } - - public getAllItemsInCache(): Entities.ITableEntity[] { - return this.cache.data; - } - - protected abstract dataComparer( - item1: Entities.ITableEntity, - item2: Entities.ITableEntity, - sortOrder: any, - oSettings: any - ): number; - protected abstract isCacheValid(validator: any): boolean; - - protected sortColumns(sortOrder: any, oSettings: any) { - var self = this; - this.clearSelection(); - this.cache.data.sort(function(a: any, b: any) { - return self.dataComparer(a, b, sortOrder, oSettings); - }); - this.cache.sortOrder = sortOrder; - } - - protected renderPage( - renderCallBack: any, - draw: number, - startIndex: number, - pageSize: number, - oSettings: any, - postRenderTasks: (startIndex: number, pageSize: number) => Promise = null - ) { - this.updatePaginationControls(oSettings); - - // pageSize < 0 means to show all data - var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize; - var renderData = this.cache.data.slice(startIndex, endIndex); - - this.items(renderData); - - var render: IDataTableRenderData = { - draw: draw, - aaData: renderData, - recordsTotal: this.cache.length, - recordsFiltered: this.cache.length - }; - - if (!!postRenderTasks) { - postRenderTasks(startIndex, pageSize).then(() => { - this.table.rows().invalidate(); - }); - } - renderCallBack(render); - if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name, - databaseName: this.queryTablesTab.collection.databaseId, - collectionName: this.queryTablesTab.collection.id(), - defaultExperience: this.queryTablesTab.collection.container.defaultExperience(), - dataExplorerArea: CommonConstants.Areas.Tab, - tabTitle: this.queryTablesTab.tabTitle() - }, - this.queryTablesTab.onLoadStartKey - ); - this.queryTablesTab.onLoadStartKey = null; - } - } - - protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean { - return itemKeys.every((property: Entities.IProperty) => { - var itemValue = item[property.key]; - - // if (itemValue && property.subkey) { - // itemValue = itemValue._[property.subkey]; - // if (!itemValue) { - // itemValue = ""; - // } - // } else if (property.subkey) { - // itemValue = ""; - // } - - return this.stringCompare(itemValue._, property.value); - }); - } - - /** - * Default string comparison is case sensitive as most Azure resources' names are case sensitive. - * Override this if a name, i.e., Azure File/Directory name, is case insensitive. - */ - protected stringCompare(s1: string, s2: string): boolean { - return s1 === s2; - } - - private updatePaginationControls(oSettings: any) { - var pageInfo = this.table.page.info(); - var pageSize = pageInfo.length; - var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector); - - if (this.allDownloaded) { - if (this.cache.length <= pageSize) { - // Hide pagination controls if everything fits in one page!. - paginateElement.hide(); - } else { - // Enable pagination controls. - paginateElement.show(); - oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel; - } - } else { - // Enable pagination controls and show load more button. - paginateElement.show(); - oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel; - } - } -} - -interface IDataTableOperation { - focusTable(): void; -} - -export default DataTableViewModel; +import * as ko from "knockout"; +import * as _ from "underscore"; + +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import CacheBase from "./CacheBase"; +import * as CommonConstants from "../../../Common/Constants"; +import * as Constants from "../Constants"; +import * as Entities from "../Entities"; +import QueryTablesTab from "../../Tabs/QueryTablesTab"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; + +// This is the format of the data we will have to pass to Datatable render callback, +// and property names are defined by Datatable as well. +export interface IDataTableRenderData { + draw: number; + aaData: any; + recordsTotal: number; + recordsFiltered: number; +} + +abstract class DataTableViewModel { + private static lastPageLabel = ">>"; // Localize + private static loadMoreLabel = "Load more"; // Localize + + /* Observables */ + public items = ko.observableArray(); + public selected = ko.observableArray(); + + public table: DataTables.DataTable; + + // The anchor item is for shift selection. i.e., select all items between anchor item and a give item. + public lastSelectedAnchorItem: Entities.ITableEntity; + public lastSelectedItem: Entities.ITableEntity; + + public cache: CacheBase; + + protected continuationToken: any; + protected allDownloaded: boolean; + protected lastPrefetchTime: number; + protected downloadSize = 300; + protected _documentIterator: QueryIterator; + + // Used by table redraw throttling + protected pollingInterval = 1000; + private redrawInterval = 500; + private pendingRedraw = false; + private lastRedrawTime = new Date().getTime(); + + private dataTableOperationManager: IDataTableOperation; + + public queryTablesTab: QueryTablesTab; + + constructor() { + this.items([]); + this.selected([]); + // Late bound + this.dataTableOperationManager = null; + } + + public bind(dataTableOperationManager: IDataTableOperation): void { + this.dataTableOperationManager = dataTableOperationManager; + } + + public clearLastSelected(): void { + this.lastSelectedItem = null; + this.lastSelectedAnchorItem = null; + } + + public clearCache(): void { + this.cache.clear(); + this._documentIterator = null; + this.continuationToken = null; + this.allDownloaded = false; + } + + public clearSelection(): void { + this.selected.removeAll(); + } + + // Redraws the table, but guarantees that multiple sequential calls will not incur + // another redraw until a certain time interval has passed. + public redrawTableThrottled() { + if (!this.pendingRedraw) { + this.pendingRedraw = true; + + var current = new Date().getTime(); + var timeSinceLastRedraw = current - this.lastRedrawTime; + var redraw = () => { + this.table.draw(false /*reset*/); + this.lastRedrawTime = new Date().getTime(); + this.pendingRedraw = false; + }; + + if (timeSinceLastRedraw > this.redrawInterval) { + redraw(); + } else { + var timeUntilNextRedraw = this.redrawInterval - timeSinceLastRedraw; + setTimeout(() => redraw(), timeUntilNextRedraw); + } + } + } + + public focusDataTable(): void { + this.dataTableOperationManager.focusTable(); + } + + public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity { + return _.find(this.selected(), (item: Entities.ITableEntity) => { + return this.matchesKeys(item, itemKeys); + }); + } + + public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity { + return _.find(this.items(), (item: Entities.ITableEntity) => { + return this.matchesKeys(item, itemKeys); + }); + } + + public getItemIndexFromCurrentPage(itemKeys: Entities.IProperty[]): number { + return _.findIndex(this.items(), (item: Entities.ITableEntity) => { + return this.matchesKeys(item, itemKeys); + }); + } + + public getItemIndexFromAllPages(itemKeys: Entities.IProperty[]): number { + return _.findIndex(this.cache.data, (item: Entities.ITableEntity) => { + return this.matchesKeys(item, itemKeys); + }); + } + + public getItemsFromAllPagesWithinRange(start: number, end: number): Entities.ITableEntity[] { + return this.cache.data.slice(start, end); + } + + public isItemSelected(itemKeys: Entities.IProperty[]): boolean { + return _.some(this.selected(), (item: Entities.ITableEntity) => { + return this.matchesKeys(item, itemKeys); + }); + } + + public isItemCached(itemKeys: Entities.IProperty[]): boolean { + return _.some(this.cache.data, (item: Entities.ITableEntity) => { + return this.matchesKeys(item, itemKeys); + }); + } + + public getAllItemsInCurrentPage(): Entities.ITableEntity[] { + return this.items(); + } + + public getAllItemsInCache(): Entities.ITableEntity[] { + return this.cache.data; + } + + protected abstract dataComparer( + item1: Entities.ITableEntity, + item2: Entities.ITableEntity, + sortOrder: any, + oSettings: any + ): number; + protected abstract isCacheValid(validator: any): boolean; + + protected sortColumns(sortOrder: any, oSettings: any) { + var self = this; + this.clearSelection(); + this.cache.data.sort(function (a: any, b: any) { + return self.dataComparer(a, b, sortOrder, oSettings); + }); + this.cache.sortOrder = sortOrder; + } + + protected renderPage( + renderCallBack: any, + draw: number, + startIndex: number, + pageSize: number, + oSettings: any, + postRenderTasks: (startIndex: number, pageSize: number) => Promise = null + ) { + this.updatePaginationControls(oSettings); + + // pageSize < 0 means to show all data + var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize; + var renderData = this.cache.data.slice(startIndex, endIndex); + + this.items(renderData); + + var render: IDataTableRenderData = { + draw: draw, + aaData: renderData, + recordsTotal: this.cache.length, + recordsFiltered: this.cache.length, + }; + + if (!!postRenderTasks) { + postRenderTasks(startIndex, pageSize).then(() => { + this.table.rows().invalidate(); + }); + } + renderCallBack(render); + if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) { + TelemetryProcessor.traceSuccess( + Action.Tab, + { + databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name, + databaseName: this.queryTablesTab.collection.databaseId, + collectionName: this.queryTablesTab.collection.id(), + defaultExperience: this.queryTablesTab.collection.container.defaultExperience(), + dataExplorerArea: CommonConstants.Areas.Tab, + tabTitle: this.queryTablesTab.tabTitle(), + }, + this.queryTablesTab.onLoadStartKey + ); + this.queryTablesTab.onLoadStartKey = null; + } + } + + protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean { + return itemKeys.every((property: Entities.IProperty) => { + var itemValue = item[property.key]; + + // if (itemValue && property.subkey) { + // itemValue = itemValue._[property.subkey]; + // if (!itemValue) { + // itemValue = ""; + // } + // } else if (property.subkey) { + // itemValue = ""; + // } + + return this.stringCompare(itemValue._, property.value); + }); + } + + /** + * Default string comparison is case sensitive as most Azure resources' names are case sensitive. + * Override this if a name, i.e., Azure File/Directory name, is case insensitive. + */ + protected stringCompare(s1: string, s2: string): boolean { + return s1 === s2; + } + + private updatePaginationControls(oSettings: any) { + var pageInfo = this.table.page.info(); + var pageSize = pageInfo.length; + var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector); + + if (this.allDownloaded) { + if (this.cache.length <= pageSize) { + // Hide pagination controls if everything fits in one page!. + paginateElement.hide(); + } else { + // Enable pagination controls. + paginateElement.show(); + oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel; + } + } else { + // Enable pagination controls and show load more button. + paginateElement.show(); + oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel; + } + } +} + +interface IDataTableOperation { + focusTable(): void; +} + +export default DataTableViewModel; diff --git a/src/Explorer/Tables/DataTable/TableCommands.ts b/src/Explorer/Tables/DataTable/TableCommands.ts index 70226fc62..c52c296d3 100644 --- a/src/Explorer/Tables/DataTable/TableCommands.ts +++ b/src/Explorer/Tables/DataTable/TableCommands.ts @@ -1,156 +1,156 @@ -import _ from "underscore"; -import Q from "q"; -import * as DataTableUtilities from "./DataTableUtilities"; -import * as DataTableOperations from "./DataTableOperations"; -import TableEntityListViewModel from "./TableEntityListViewModel"; -import * as Entities from "../Entities"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import * as TableColumnOptionsPane from "../../Panes/Tables/TableColumnOptionsPane"; -import Explorer from "../../Explorer"; - -export default class TableCommands { - // Command Ids - public static editEntityCommand: string = "edit"; - public static deleteEntitiesCommand: string = "delete"; - public static reorderColumnsCommand: string = "reorder"; - public static resetColumnsCommand: string = "reset"; - public static customizeColumnsCommand: string = "customizeColumns"; - - private _container: Explorer; - - constructor(container: Explorer) { - this._container = container; - } - - public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean { - var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites); - var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites); - switch (commandName) { - case TableCommands.editEntityCommand: - return singleItemSelected; - case TableCommands.deleteEntitiesCommand: - case TableCommands.reorderColumnsCommand: - return atLeastOneItemSelected; - default: - break; - } - - return false; - } - - public tryOpenEntityEditor(viewModel: TableEntityListViewModel): boolean { - if (this.isEnabled(TableCommands.editEntityCommand, viewModel.selected())) { - this.editEntityCommand(viewModel); - return true; - } - return false; - } - - /** - * Edit entity - */ - public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise { - if (!viewModel) { - return null; // Error - } - - if (!DataTableUtilities.containSingleItem(viewModel.selected())) { - return null; // Erorr - } - - var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0]; - var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag - - this._container.editTableEntityPane.originEntity = entityToUpdate; - this._container.editTableEntityPane.tableViewModel = viewModel; - this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties; - this._container.editTableEntityPane.open(); - return null; - } - - public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise { - if (!viewModel) { - return null; // Error - } - if (!DataTableUtilities.containItems(viewModel.selected())) { - return null; // Error - } - var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); - let deleteMessage: string = "Are you sure you want to delete the selected entities?"; - if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) { - deleteMessage = "Are you sure you want to delete the selected rows?"; - } - if (window.confirm(deleteMessage)) { - viewModel.queryTablesTab.container.tableDataClient - .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) - .then((results: any) => { - return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { - viewModel.redrawTableThrottled(); - }); - }); - } - return null; - } - - public customizeColumnsCommand(viewModel: TableEntityListViewModel): Q.Promise { - var table: DataTables.DataTable = viewModel.table; - var displayedColumnNames: string[] = DataTableOperations.getDataTableHeaders(table); - var columnsCount: number = displayedColumnNames.length; - var currentOrder: number[] = DataTableOperations.getInitialOrder(columnsCount); - //Debug.assert(!!table && !!currentOrder && displayedColumnNames.length === currentOrder.length); - - var currentSettings: boolean[]; - try { - currentSettings = currentOrder.map((value: number, index: number) => { - return table.column(index).visible(); - }); - } catch (err) { - // Error - } - - let parameters: TableColumnOptionsPane.IColumnSetting = { - columnNames: displayedColumnNames, - order: currentOrder, - visible: currentSettings - }; - - this._container.tableColumnOptionsPane.tableViewModel = viewModel; - this._container.tableColumnOptionsPane.parameters = parameters; - this._container.tableColumnOptionsPane.open(); - return null; - } - - public reorderColumnsBasedOnSelectedEntities(viewModel: TableEntityListViewModel): Q.Promise { - var selected = viewModel.selected(); - if (!selected || !selected.length) { - return null; - } - - var table = viewModel.table; - var currentColumnNames: string[] = DataTableOperations.getDataTableHeaders(table); - var headersCount: number = currentColumnNames.length; - - var headersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities( - selected, - viewModel.queryTablesTab.container.isPreferredApiCassandra() - ); - - // An array with elements representing indexes of selected entities' header union out of initial headers. - var orderOfLeftHeaders: number[] = headersUnion.map((item: string) => currentColumnNames.indexOf(item)); - - // An array with elements representing initial order of the table. - var initialOrder: number[] = DataTableOperations.getInitialOrder(headersCount); - - // An array with elements representing indexes of headers not present in selected entities' header union. - var orderOfRightHeaders: number[] = _.difference(initialOrder, orderOfLeftHeaders); - - // This will be the target order, with headers in selected entities on the left while others on the right, both in the initial order, respectively. - var targetOrder: number[] = orderOfLeftHeaders.concat(orderOfRightHeaders); - - return DataTableOperations.reorderColumns(table, targetOrder); - } - - public resetColumns(viewModel: TableEntityListViewModel): void { - viewModel.reloadTable(); - } -} +import _ from "underscore"; +import Q from "q"; +import * as DataTableUtilities from "./DataTableUtilities"; +import * as DataTableOperations from "./DataTableOperations"; +import TableEntityListViewModel from "./TableEntityListViewModel"; +import * as Entities from "../Entities"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import * as TableColumnOptionsPane from "../../Panes/Tables/TableColumnOptionsPane"; +import Explorer from "../../Explorer"; + +export default class TableCommands { + // Command Ids + public static editEntityCommand: string = "edit"; + public static deleteEntitiesCommand: string = "delete"; + public static reorderColumnsCommand: string = "reorder"; + public static resetColumnsCommand: string = "reset"; + public static customizeColumnsCommand: string = "customizeColumns"; + + private _container: Explorer; + + constructor(container: Explorer) { + this._container = container; + } + + public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean { + var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites); + var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites); + switch (commandName) { + case TableCommands.editEntityCommand: + return singleItemSelected; + case TableCommands.deleteEntitiesCommand: + case TableCommands.reorderColumnsCommand: + return atLeastOneItemSelected; + default: + break; + } + + return false; + } + + public tryOpenEntityEditor(viewModel: TableEntityListViewModel): boolean { + if (this.isEnabled(TableCommands.editEntityCommand, viewModel.selected())) { + this.editEntityCommand(viewModel); + return true; + } + return false; + } + + /** + * Edit entity + */ + public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise { + if (!viewModel) { + return null; // Error + } + + if (!DataTableUtilities.containSingleItem(viewModel.selected())) { + return null; // Erorr + } + + var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0]; + var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag + + this._container.editTableEntityPane.originEntity = entityToUpdate; + this._container.editTableEntityPane.tableViewModel = viewModel; + this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties; + this._container.editTableEntityPane.open(); + return null; + } + + public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise { + if (!viewModel) { + return null; // Error + } + if (!DataTableUtilities.containItems(viewModel.selected())) { + return null; // Error + } + var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); + let deleteMessage: string = "Are you sure you want to delete the selected entities?"; + if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) { + deleteMessage = "Are you sure you want to delete the selected rows?"; + } + if (window.confirm(deleteMessage)) { + viewModel.queryTablesTab.container.tableDataClient + .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) + .then((results: any) => { + return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { + viewModel.redrawTableThrottled(); + }); + }); + } + return null; + } + + public customizeColumnsCommand(viewModel: TableEntityListViewModel): Q.Promise { + var table: DataTables.DataTable = viewModel.table; + var displayedColumnNames: string[] = DataTableOperations.getDataTableHeaders(table); + var columnsCount: number = displayedColumnNames.length; + var currentOrder: number[] = DataTableOperations.getInitialOrder(columnsCount); + //Debug.assert(!!table && !!currentOrder && displayedColumnNames.length === currentOrder.length); + + var currentSettings: boolean[]; + try { + currentSettings = currentOrder.map((value: number, index: number) => { + return table.column(index).visible(); + }); + } catch (err) { + // Error + } + + let parameters: TableColumnOptionsPane.IColumnSetting = { + columnNames: displayedColumnNames, + order: currentOrder, + visible: currentSettings, + }; + + this._container.tableColumnOptionsPane.tableViewModel = viewModel; + this._container.tableColumnOptionsPane.parameters = parameters; + this._container.tableColumnOptionsPane.open(); + return null; + } + + public reorderColumnsBasedOnSelectedEntities(viewModel: TableEntityListViewModel): Q.Promise { + var selected = viewModel.selected(); + if (!selected || !selected.length) { + return null; + } + + var table = viewModel.table; + var currentColumnNames: string[] = DataTableOperations.getDataTableHeaders(table); + var headersCount: number = currentColumnNames.length; + + var headersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities( + selected, + viewModel.queryTablesTab.container.isPreferredApiCassandra() + ); + + // An array with elements representing indexes of selected entities' header union out of initial headers. + var orderOfLeftHeaders: number[] = headersUnion.map((item: string) => currentColumnNames.indexOf(item)); + + // An array with elements representing initial order of the table. + var initialOrder: number[] = DataTableOperations.getInitialOrder(headersCount); + + // An array with elements representing indexes of headers not present in selected entities' header union. + var orderOfRightHeaders: number[] = _.difference(initialOrder, orderOfLeftHeaders); + + // This will be the target order, with headers in selected entities on the left while others on the right, both in the initial order, respectively. + var targetOrder: number[] = orderOfLeftHeaders.concat(orderOfRightHeaders); + + return DataTableOperations.reorderColumns(table, targetOrder); + } + + public resetColumns(viewModel: TableEntityListViewModel): void { + viewModel.reloadTable(); + } +} diff --git a/src/Explorer/Tables/DataTable/TableEntityCache.ts b/src/Explorer/Tables/DataTable/TableEntityCache.ts index 22640de8c..282b21d19 100644 --- a/src/Explorer/Tables/DataTable/TableEntityCache.ts +++ b/src/Explorer/Tables/DataTable/TableEntityCache.ts @@ -1,27 +1,27 @@ -import * as Utilities from "../Utilities"; -import * as Entities from "../Entities"; -import CacheBase from "./CacheBase"; - -export default class TableEntityCache extends CacheBase { - private _tableQuery: Entities.ITableQuery; - - constructor() { - super(); - this.data = null; - this._tableQuery = null; - this.serverCallInProgress = false; - this.sortOrder = null; - } - - public get tableQuery(): Entities.ITableQuery { - return Utilities.copyTableQuery(this._tableQuery); - } - - public set tableQuery(tableQuery: Entities.ITableQuery) { - this._tableQuery = Utilities.copyTableQuery(tableQuery); - } - - public preClear() { - this.tableQuery = null; - } -} +import * as Utilities from "../Utilities"; +import * as Entities from "../Entities"; +import CacheBase from "./CacheBase"; + +export default class TableEntityCache extends CacheBase { + private _tableQuery: Entities.ITableQuery; + + constructor() { + super(); + this.data = null; + this._tableQuery = null; + this.serverCallInProgress = false; + this.sortOrder = null; + } + + public get tableQuery(): Entities.ITableQuery { + return Utilities.copyTableQuery(this._tableQuery); + } + + public set tableQuery(tableQuery: Entities.ITableQuery) { + this._tableQuery = Utilities.copyTableQuery(tableQuery); + } + + public preClear() { + this.tableQuery = null; + } +} diff --git a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts index 2a3e7765f..d727fd01d 100644 --- a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts +++ b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts @@ -1,602 +1,602 @@ -import * as ko from "knockout"; -import * as _ from "underscore"; -import Q from "q"; - -import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; -import { CassandraTableKey, CassandraAPIDataClient } from "../TableDataClient"; -import DataTableViewModel from "./DataTableViewModel"; -import * as DataTableUtilities from "./DataTableUtilities"; -import { getQuotedCqlIdentifier } from "../CqlUtilities"; -import TableCommands from "./TableCommands"; -import TableEntityCache from "./TableEntityCache"; -import * as Constants from "../Constants"; -import { Areas } from "../../../Common/Constants"; -import * as Utilities from "../Utilities"; -import * as Entities from "../Entities"; -import QueryTablesTab from "../../Tabs/QueryTablesTab"; -import * as TableEntityProcessor from "../TableEntityProcessor"; -import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import * as ViewModels from "../../../Contracts/ViewModels"; - -interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult { - ExceedMaximumRetries?: boolean; -} - -export interface ErrorDataModel { - message: string; - severity?: string; - location?: { - start: string; - end: string; - }; - code?: string; -} - -function parseError(err: any): ErrorDataModel[] { - try { - return _parse(err); - } catch (e) { - return [{ message: JSON.stringify(err) }]; - } -} - -function _parse(err: any): ErrorDataModel[] { - var normalizedErrors: ErrorDataModel[] = []; - if (err.message && !err.code) { - normalizedErrors.push(err); - } else { - const innerErrors: any[] = _getInnerErrors(err.message); - normalizedErrors = innerErrors.map(innerError => - typeof innerError === "string" ? { message: innerError } : innerError - ); - } - - return normalizedErrors; -} - -function _getInnerErrors(message: string): any[] { - /* - The backend error message has an inner-message which is a stringified object. - For SQL errors, the "errors" property is an array of SqlErrorDataModel. - Example: - "Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p" - For non-SQL errors the "Errors" propery is an array of string. - Example: - "Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s" - */ - - let innerMessage: any = null; - - const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, ""); - try { - // Multi-Partition error flavor - const regExp = /^(.*)ActivityId: (.*)/g; - const regString = regExp.exec(singleLineMessage); - const innerMessageString = regString[1]; - innerMessage = JSON.parse(innerMessageString); - } catch (e) { - // Single-partition error flavor - const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g; - const regString = regExp.exec(singleLineMessage); - const innerMessageString = regString[1]; - innerMessage = JSON.parse(innerMessageString); - } - - return innerMessage.errors ? innerMessage.errors : innerMessage.Errors; -} - -/** - * Storage Table Entity List ViewModel - */ -export default class TableEntityListViewModel extends DataTableViewModel { - // This is the number of retry attempts to fetch entities when the Azure Table service returns no results with a continuation token. - // This number should ideally accommodate the service default timeout for queries of 30s, where each individual query execution can - // take *up* to 5s (see https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx). - // To be on the safe side, we are setting the total number of attempts to 120, assuming up to 4 queries per second (120q = 30s * 4q/s). - // Experimentation also validates this "safe number": queries against a 10 million entity table took up to 13 fetch attempts. - private static _maximumNumberOfPrefetchRetries = 120 - 1; - - /* Observables */ - public headers: string[] = [Constants.defaultHeader]; - public useSetting: boolean = true; - - //public tableExplorerContext: TableExplorerContext; - public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: QueryTablesTab) => void; - public tablePageStartIndex: number; - public tableQuery: Entities.ITableQuery = {}; - public cqlQuery: ko.Observable; - public oDataQuery: ko.Observable; - public sqlQuery: ko.Observable; - public cache: TableEntityCache; - public isCancelled: boolean = false; - public queryErrorMessage: ko.Observable; - public id: string; - - constructor(tableCommands: TableCommands, queryTablesTab: QueryTablesTab) { - super(); - this.cache = new TableEntityCache(); - this.queryErrorMessage = ko.observable(); - this.queryTablesTab = queryTablesTab; - this.id = `tableEntityListViewModel${this.queryTablesTab.tabId}`; - this.cqlQuery = ko.observable( - `SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier( - this.queryTablesTab.collection.id() - )}` - ); - this.oDataQuery = ko.observable(); - this.sqlQuery = ko.observable("SELECT * FROM c"); - } - - public getTableEntityKeys(rowKey: string): Entities.IProperty[] { - return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }]; - } - - public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable { - this.clearCache(); - this.clearSelection(); - this.isCancelled = false; - - this.useSetting = useSetting; - if (resetHeaders) { - this.updateHeaders([Constants.defaultHeader]); - } - return this.table.ajax.reload(); - } - - public updateHeaders(newHeaders: string[], notifyColumnChanges: boolean = false, enablePrompt: boolean = true): void { - this.headers = newHeaders; - if (notifyColumnChanges) { - this.clearSelection(); - this.notifyColumnChanges(enablePrompt, this.queryTablesTab); - } - } - - /** - * This callback function called by datatable to fetch the next page of data and render. - * sSource - ajax URL of data source, ignored in our case as we are not using ajax. - * aoData - details about the next page of data datatable expected to render. - * fnCallback - is the render callback with data to render. - * oSetting: current settings used for table initialization. - */ - public renderNextPageAndupdateCache(sSource: any, aoData: any, fnCallback: any, oSettings: any) { - var tablePageSize: number; - var draw: number; - var prefetchNeeded = true; - var columnSortOrder: any; - // Threshold(pages) for triggering cache prefetch. - // If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered. - var prefetchThreshold = 10; - var tableQuery = this.tableQuery; - - for (var index in aoData) { - var data = aoData[index]; - if (data.name === "length") { - tablePageSize = data.value; - } - if (data.name === "start") { - this.tablePageStartIndex = data.value; - } - if (data.name === "draw") { - draw = data.value; - } - if (data.name === "order") { - columnSortOrder = data.value; - } - } - // Try cache if valid. - if (this.isCacheValid(tableQuery)) { - // Check if prefetch needed. - if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) { - prefetchNeeded = false; - if (columnSortOrder && (!this.cache.sortOrder || !_.isEqual(this.cache.sortOrder, columnSortOrder))) { - this.sortColumns(columnSortOrder, oSettings); - } - this.renderPage(fnCallback, draw, this.tablePageStartIndex, tablePageSize, oSettings); - if ( - !this.allDownloaded && - this.tablePageStartIndex > 0 && // This is a case now that we can hit this as we re-construct table when we update column - this.cache.length - this.tablePageStartIndex + tablePageSize < prefetchThreshold * tablePageSize - ) { - prefetchNeeded = true; - } - } else { - prefetchNeeded = true; - } - } else { - this.clearCache(); - } - - if (prefetchNeeded) { - var downloadSize = tableQuery.top || this.downloadSize; - this.prefetchAndRender( - tableQuery, - this.tablePageStartIndex, - tablePageSize, - downloadSize, - draw, - fnCallback, - oSettings, - columnSortOrder - ); - } - } - - public addEntityToCache(entity: Entities.ITableEntity): Q.Promise { - // Delay the add operation if we are fetching data from server, so as to avoid race condition. - if (this.cache.serverCallInProgress) { - return Utilities.delay(this.pollingInterval).then(() => { - return this.updateCachedEntity(entity); - }); - } - - // Find the first item which is greater than the added entity. - var oSettings: any = (this.table).context[0]; - var index: number = _.findIndex(this.cache.data, (data: any) => { - return this.dataComparer(data, entity, this.cache.sortOrder, oSettings) > 0; - }); - - // If no such item, then insert at last. - var insertIndex: number = Utilities.ensureBetweenBounds( - index < 0 ? this.cache.length : index, - 0, - this.cache.length - ); - - this.cache.data.splice(insertIndex, 0, entity); - - // Finally, select newly added entity - this.clearSelection(); - this.selected.push(entity); - - return Q.resolve(null); - } - - public updateCachedEntity(entity: Entities.ITableEntity): Q.Promise { - // Delay the add operation if we are fetching data from server, so as to avoid race condition. - if (this.cache.serverCallInProgress) { - return Utilities.delay(this.pollingInterval).then(() => { - return this.updateCachedEntity(entity); - }); - } - var oldEntityIndex: number = _.findIndex( - this.cache.data, - (data: Entities.ITableEntity) => data.RowKey._ === entity.RowKey._ - ); - - this.cache.data.splice(oldEntityIndex, 1, entity); - - return Q.resolve(null); - } - - public removeEntitiesFromCache(entities: Entities.ITableEntity[]): Q.Promise { - if (!entities) { - return Q.resolve(null); - } - - // Delay the remove operation if we are fetching data from server, so as to avoid race condition. - if (this.cache.serverCallInProgress) { - return Utilities.delay(this.pollingInterval).then(() => { - return this.removeEntitiesFromCache(entities); - }); - } - - entities && - entities.forEach((entity: Entities.ITableEntity) => { - var cachedIndex: number = _.findIndex( - this.cache.data, - (e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._ - ); - if (cachedIndex >= 0) { - this.cache.data.splice(cachedIndex, 1); - } - }); - this.clearSelection(); - - // Show last available page if there is not enough data - var pageInfo = this.table.page.info(); - if (this.cache.length <= pageInfo.start) { - var availablePages = Math.ceil(this.cache.length / pageInfo.length); - var pageToShow = availablePages > 0 ? availablePages - 1 : 0; - this.table.page(pageToShow); - } - - return Q.resolve(null); - } - - protected dataComparer( - item1: Entities.ITableEntity, - item2: Entities.ITableEntity, - sortOrder: any[], - oSettings: any - ): number { - var sort: any; - var itemA: any; - var itemB: any; - var length: number = $.isArray(sortOrder) ? sortOrder.length : 0; // sortOrder can be null - var rowA: Entities.ITableEntity = item1; - var rowB: Entities.ITableEntity = item2; - - for (var k = 0; k < length; k++) { - sort = sortOrder[k]; - var col = oSettings.aoColumns[sort.column].mData; - - // If the value is null or undefined, show them at last - var isItem1NullOrUndefined = _.isNull(rowA[col]) || _.isUndefined(rowA[col]); - var isItem2NullOrUndefined = _.isNull(rowB[col]) || _.isUndefined(rowB[col]); - - if (isItem1NullOrUndefined || isItem2NullOrUndefined) { - if (isItem1NullOrUndefined && isItem2NullOrUndefined) { - return 0; - } - return isItem1NullOrUndefined ? 1 : -1; - } - - switch ((rowA[col]).$) { - case Constants.TableType.Int32: - case Constants.TableType.Int64: - case Constants.CassandraType.Int: - case Constants.CassandraType.Bigint: - case Constants.CassandraType.Smallint: - case Constants.CassandraType.Varint: - case Constants.CassandraType.Tinyint: - itemA = parseInt((rowA[col])._, 0); - itemB = parseInt((rowB[col])._, 0); - break; - case Constants.TableType.Double: - case Constants.CassandraType.Double: - case Constants.CassandraType.Float: - case Constants.CassandraType.Decimal: - itemA = parseFloat((rowA[col])._); - itemB = parseFloat((rowB[col])._); - break; - case Constants.TableType.DateTime: - itemA = new Date((rowA[col])._); - itemB = new Date((rowB[col])._); - break; - default: - itemA = (rowA[col])._.toLowerCase(); - itemB = (rowB[col])._.toLowerCase(); - } - var compareResult: number = itemA < itemB ? -1 : itemA > itemB ? 1 : 0; - if (compareResult !== 0) { - return sort.dir === "asc" ? compareResult : -compareResult; - } - } - return 0; - } - - protected isCacheValid(tableQuery: Entities.ITableQuery): boolean { - // Return false if either cache has no data or the search criteria don't match! - if (!this.cache || !this.cache.data || this.cache.length === 0) { - return false; - } - - if (!tableQuery && !this.cache.tableQuery) { - return true; - } - - // Compare by value using JSON representation - if (JSON.stringify(this.cache.tableQuery) !== JSON.stringify(tableQuery)) { - return false; - } - return true; - } - - // Override as table entity has special keys for a Data Table row. - /** - * @override - */ - protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean { - return itemKeys.every((property: Entities.IProperty) => { - return this.stringCompare(item[property.key]._, property.value); - }); - } - - private prefetchAndRender( - tableQuery: Entities.ITableQuery, - tablePageStartIndex: number, - tablePageSize: number, - downloadSize: number, - draw: number, - renderCallBack: Function, - oSettings: any, - columnSortOrder: any - ): void { - this.queryErrorMessage(null); - if (this.cache.serverCallInProgress) { - return; - } - this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0) - .then((result: IListTableEntitiesSegmentedResult) => { - if (!result) { - return; - } - - var entities = this.cache.data; - if ( - this.queryTablesTab.container.isPreferredApiCassandra() && - DataTableUtilities.checkForDefaultHeader(this.headers) - ) { - (this.queryTablesTab.container.tableDataClient) - .getTableSchema(this.queryTablesTab.collection) - .then((headers: CassandraTableKey[]) => { - this.updateHeaders( - headers.map(header => header.property), - true - ); - }); - } else { - var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities( - entities, - this.queryTablesTab.container.isPreferredApiCassandra() - ); - var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers); - if (newHeaders.length > 0) { - // Any new columns found will be added into headers array, which will trigger a re-render of the DataTable. - // So there is no need to call it here. - this.updateHeaders(newHeaders, /* notifyColumnChanges */ true); - } else { - if (columnSortOrder) { - this.sortColumns(columnSortOrder, oSettings); - } - this.renderPage(renderCallBack, draw, tablePageStartIndex, tablePageSize, oSettings); - } - } - - if (result.ExceedMaximumRetries) { - var message: string = "We are having trouble getting your data. Please try again."; // localize - } - }) - .catch((error: any) => { - const parsedErrors = parseError(error); - var errors = parsedErrors.map(error => { - return { - message: error.message, - start: error.location ? error.location.start : undefined, - end: error.location ? error.location.end : undefined, - code: error.code, - severity: error.severity - }; - }); - this.queryErrorMessage(errors[0].message); - if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name, - databaseName: this.queryTablesTab.collection.databaseId, - collectionName: this.queryTablesTab.collection.id(), - defaultExperience: this.queryTablesTab.collection.container.defaultExperience(), - dataExplorerArea: Areas.Tab, - tabTitle: this.queryTablesTab.tabTitle(), - error: error - }, - this.queryTablesTab.onLoadStartKey - ); - this.queryTablesTab.onLoadStartKey = null; - } - DataTableUtilities.turnOffProgressIndicator(); - }); - } - - /** - * Keep recursively prefetching items if: - * 1. Continuation token is not null - * 2. And prefetched items hasn't reach predefined cache size. - * 3. And retry times hasn't reach the predefined maximum retry number. - * - * It is possible for a query to return no results but still return a continuation header (e.g. if the query takes too long). - * If this is the case, we try to fetch entities again. - * Note that this also means that we can get less entities than the requested download size in a successful call. - * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx - */ - private prefetchData( - tableQuery: Entities.ITableQuery, - downloadSize: number, - currentRetry: number = 0 - ): Q.Promise { - if (!this.cache.serverCallInProgress) { - this.cache.serverCallInProgress = true; - this.allDownloaded = false; - this.lastPrefetchTime = new Date().getTime(); - var time = this.lastPrefetchTime; - - var promise: Q.Promise; - if (this._documentIterator && this.continuationToken) { - // TODO handle Cassandra case - - promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then( - (documents: any[]) => { - let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); - let finalEntities: IListTableEntitiesSegmentedResult = { - Results: entities, - ContinuationToken: this._documentIterator.hasMoreResults() - }; - return Q.resolve(finalEntities); - } - ); - } else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { - promise = Q( - this.queryTablesTab.container.tableDataClient.queryDocuments( - this.queryTablesTab.collection, - this.cqlQuery(), - true, - this.continuationToken - ) - ); - } else { - let query = this.sqlQuery(); - if (this.queryTablesTab.container.isPreferredApiCassandra()) { - query = this.cqlQuery(); - } - promise = Q( - this.queryTablesTab.container.tableDataClient.queryDocuments(this.queryTablesTab.collection, query, true) - ); - } - return promise - .then((result: IListTableEntitiesSegmentedResult) => { - if (!this._documentIterator) { - this._documentIterator = result.iterator; - } - var actualDownloadSize: number = 0; - - // If we hit this, it means another service call is triggered. We only handle the latest call. - // And as another service call is during process, we don't set serverCallInProgress to false here. - // Thus, end the prefetch. - if (this.lastPrefetchTime !== time) { - return Q.resolve(null); - } - - var entities = result.Results; - actualDownloadSize = entities.length; - - // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. - this.continuationToken = this.isCancelled ? null : result.ContinuationToken; - - if (!this.continuationToken) { - this.allDownloaded = true; - } - - if (this.isCacheValid(tableQuery)) { - // Append to cache. - this.cache.data = this.cache.data.concat(entities.slice(0)); - } else { - // Create cache. - this.cache.data = entities; - } - - this.cache.tableQuery = tableQuery; - this.cache.serverCallInProgress = false; - - var nextDownloadSize: number = downloadSize - actualDownloadSize; - if (nextDownloadSize === 0 && tableQuery.top) { - this.allDownloaded = true; - } - - // There are three possible results for a prefetch: - // 1. Continuation token is null or fetched items' size reaches predefined. - // 2. Continuation token is not null and fetched items' size hasn't reach predefined. - // 2.1 Retry times has reached predefined maximum. - // 2.2 Retry times hasn't reached predefined maximum. - // Correspondingly, - // For #1, end prefetch. - // For #2.1, set prefetch exceeds maximum retry number and end prefetch. - // For #2.2, go to next round prefetch. - if (this.allDownloaded || nextDownloadSize === 0) { - return Q.resolve(result); - } - - if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { - result.ExceedMaximumRetries = true; - return Q.resolve(result); - } - return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); - }) - .catch((error: Error) => { - this.cache.serverCallInProgress = false; - return Q.reject(error); - }); - } - return null; - } -} +import * as ko from "knockout"; +import * as _ from "underscore"; +import Q from "q"; + +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import { CassandraTableKey, CassandraAPIDataClient } from "../TableDataClient"; +import DataTableViewModel from "./DataTableViewModel"; +import * as DataTableUtilities from "./DataTableUtilities"; +import { getQuotedCqlIdentifier } from "../CqlUtilities"; +import TableCommands from "./TableCommands"; +import TableEntityCache from "./TableEntityCache"; +import * as Constants from "../Constants"; +import { Areas } from "../../../Common/Constants"; +import * as Utilities from "../Utilities"; +import * as Entities from "../Entities"; +import QueryTablesTab from "../../Tabs/QueryTablesTab"; +import * as TableEntityProcessor from "../TableEntityProcessor"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import * as ViewModels from "../../../Contracts/ViewModels"; + +interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult { + ExceedMaximumRetries?: boolean; +} + +export interface ErrorDataModel { + message: string; + severity?: string; + location?: { + start: string; + end: string; + }; + code?: string; +} + +function parseError(err: any): ErrorDataModel[] { + try { + return _parse(err); + } catch (e) { + return [{ message: JSON.stringify(err) }]; + } +} + +function _parse(err: any): ErrorDataModel[] { + var normalizedErrors: ErrorDataModel[] = []; + if (err.message && !err.code) { + normalizedErrors.push(err); + } else { + const innerErrors: any[] = _getInnerErrors(err.message); + normalizedErrors = innerErrors.map((innerError) => + typeof innerError === "string" ? { message: innerError } : innerError + ); + } + + return normalizedErrors; +} + +function _getInnerErrors(message: string): any[] { + /* + The backend error message has an inner-message which is a stringified object. + For SQL errors, the "errors" property is an array of SqlErrorDataModel. + Example: + "Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p" + For non-SQL errors the "Errors" propery is an array of string. + Example: + "Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s" + */ + + let innerMessage: any = null; + + const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, ""); + try { + // Multi-Partition error flavor + const regExp = /^(.*)ActivityId: (.*)/g; + const regString = regExp.exec(singleLineMessage); + const innerMessageString = regString[1]; + innerMessage = JSON.parse(innerMessageString); + } catch (e) { + // Single-partition error flavor + const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g; + const regString = regExp.exec(singleLineMessage); + const innerMessageString = regString[1]; + innerMessage = JSON.parse(innerMessageString); + } + + return innerMessage.errors ? innerMessage.errors : innerMessage.Errors; +} + +/** + * Storage Table Entity List ViewModel + */ +export default class TableEntityListViewModel extends DataTableViewModel { + // This is the number of retry attempts to fetch entities when the Azure Table service returns no results with a continuation token. + // This number should ideally accommodate the service default timeout for queries of 30s, where each individual query execution can + // take *up* to 5s (see https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx). + // To be on the safe side, we are setting the total number of attempts to 120, assuming up to 4 queries per second (120q = 30s * 4q/s). + // Experimentation also validates this "safe number": queries against a 10 million entity table took up to 13 fetch attempts. + private static _maximumNumberOfPrefetchRetries = 120 - 1; + + /* Observables */ + public headers: string[] = [Constants.defaultHeader]; + public useSetting: boolean = true; + + //public tableExplorerContext: TableExplorerContext; + public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: QueryTablesTab) => void; + public tablePageStartIndex: number; + public tableQuery: Entities.ITableQuery = {}; + public cqlQuery: ko.Observable; + public oDataQuery: ko.Observable; + public sqlQuery: ko.Observable; + public cache: TableEntityCache; + public isCancelled: boolean = false; + public queryErrorMessage: ko.Observable; + public id: string; + + constructor(tableCommands: TableCommands, queryTablesTab: QueryTablesTab) { + super(); + this.cache = new TableEntityCache(); + this.queryErrorMessage = ko.observable(); + this.queryTablesTab = queryTablesTab; + this.id = `tableEntityListViewModel${this.queryTablesTab.tabId}`; + this.cqlQuery = ko.observable( + `SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier( + this.queryTablesTab.collection.id() + )}` + ); + this.oDataQuery = ko.observable(); + this.sqlQuery = ko.observable("SELECT * FROM c"); + } + + public getTableEntityKeys(rowKey: string): Entities.IProperty[] { + return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }]; + } + + public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable { + this.clearCache(); + this.clearSelection(); + this.isCancelled = false; + + this.useSetting = useSetting; + if (resetHeaders) { + this.updateHeaders([Constants.defaultHeader]); + } + return this.table.ajax.reload(); + } + + public updateHeaders(newHeaders: string[], notifyColumnChanges: boolean = false, enablePrompt: boolean = true): void { + this.headers = newHeaders; + if (notifyColumnChanges) { + this.clearSelection(); + this.notifyColumnChanges(enablePrompt, this.queryTablesTab); + } + } + + /** + * This callback function called by datatable to fetch the next page of data and render. + * sSource - ajax URL of data source, ignored in our case as we are not using ajax. + * aoData - details about the next page of data datatable expected to render. + * fnCallback - is the render callback with data to render. + * oSetting: current settings used for table initialization. + */ + public renderNextPageAndupdateCache(sSource: any, aoData: any, fnCallback: any, oSettings: any) { + var tablePageSize: number; + var draw: number; + var prefetchNeeded = true; + var columnSortOrder: any; + // Threshold(pages) for triggering cache prefetch. + // If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered. + var prefetchThreshold = 10; + var tableQuery = this.tableQuery; + + for (var index in aoData) { + var data = aoData[index]; + if (data.name === "length") { + tablePageSize = data.value; + } + if (data.name === "start") { + this.tablePageStartIndex = data.value; + } + if (data.name === "draw") { + draw = data.value; + } + if (data.name === "order") { + columnSortOrder = data.value; + } + } + // Try cache if valid. + if (this.isCacheValid(tableQuery)) { + // Check if prefetch needed. + if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) { + prefetchNeeded = false; + if (columnSortOrder && (!this.cache.sortOrder || !_.isEqual(this.cache.sortOrder, columnSortOrder))) { + this.sortColumns(columnSortOrder, oSettings); + } + this.renderPage(fnCallback, draw, this.tablePageStartIndex, tablePageSize, oSettings); + if ( + !this.allDownloaded && + this.tablePageStartIndex > 0 && // This is a case now that we can hit this as we re-construct table when we update column + this.cache.length - this.tablePageStartIndex + tablePageSize < prefetchThreshold * tablePageSize + ) { + prefetchNeeded = true; + } + } else { + prefetchNeeded = true; + } + } else { + this.clearCache(); + } + + if (prefetchNeeded) { + var downloadSize = tableQuery.top || this.downloadSize; + this.prefetchAndRender( + tableQuery, + this.tablePageStartIndex, + tablePageSize, + downloadSize, + draw, + fnCallback, + oSettings, + columnSortOrder + ); + } + } + + public addEntityToCache(entity: Entities.ITableEntity): Q.Promise { + // Delay the add operation if we are fetching data from server, so as to avoid race condition. + if (this.cache.serverCallInProgress) { + return Utilities.delay(this.pollingInterval).then(() => { + return this.updateCachedEntity(entity); + }); + } + + // Find the first item which is greater than the added entity. + var oSettings: any = (this.table).context[0]; + var index: number = _.findIndex(this.cache.data, (data: any) => { + return this.dataComparer(data, entity, this.cache.sortOrder, oSettings) > 0; + }); + + // If no such item, then insert at last. + var insertIndex: number = Utilities.ensureBetweenBounds( + index < 0 ? this.cache.length : index, + 0, + this.cache.length + ); + + this.cache.data.splice(insertIndex, 0, entity); + + // Finally, select newly added entity + this.clearSelection(); + this.selected.push(entity); + + return Q.resolve(null); + } + + public updateCachedEntity(entity: Entities.ITableEntity): Q.Promise { + // Delay the add operation if we are fetching data from server, so as to avoid race condition. + if (this.cache.serverCallInProgress) { + return Utilities.delay(this.pollingInterval).then(() => { + return this.updateCachedEntity(entity); + }); + } + var oldEntityIndex: number = _.findIndex( + this.cache.data, + (data: Entities.ITableEntity) => data.RowKey._ === entity.RowKey._ + ); + + this.cache.data.splice(oldEntityIndex, 1, entity); + + return Q.resolve(null); + } + + public removeEntitiesFromCache(entities: Entities.ITableEntity[]): Q.Promise { + if (!entities) { + return Q.resolve(null); + } + + // Delay the remove operation if we are fetching data from server, so as to avoid race condition. + if (this.cache.serverCallInProgress) { + return Utilities.delay(this.pollingInterval).then(() => { + return this.removeEntitiesFromCache(entities); + }); + } + + entities && + entities.forEach((entity: Entities.ITableEntity) => { + var cachedIndex: number = _.findIndex( + this.cache.data, + (e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._ + ); + if (cachedIndex >= 0) { + this.cache.data.splice(cachedIndex, 1); + } + }); + this.clearSelection(); + + // Show last available page if there is not enough data + var pageInfo = this.table.page.info(); + if (this.cache.length <= pageInfo.start) { + var availablePages = Math.ceil(this.cache.length / pageInfo.length); + var pageToShow = availablePages > 0 ? availablePages - 1 : 0; + this.table.page(pageToShow); + } + + return Q.resolve(null); + } + + protected dataComparer( + item1: Entities.ITableEntity, + item2: Entities.ITableEntity, + sortOrder: any[], + oSettings: any + ): number { + var sort: any; + var itemA: any; + var itemB: any; + var length: number = $.isArray(sortOrder) ? sortOrder.length : 0; // sortOrder can be null + var rowA: Entities.ITableEntity = item1; + var rowB: Entities.ITableEntity = item2; + + for (var k = 0; k < length; k++) { + sort = sortOrder[k]; + var col = oSettings.aoColumns[sort.column].mData; + + // If the value is null or undefined, show them at last + var isItem1NullOrUndefined = _.isNull(rowA[col]) || _.isUndefined(rowA[col]); + var isItem2NullOrUndefined = _.isNull(rowB[col]) || _.isUndefined(rowB[col]); + + if (isItem1NullOrUndefined || isItem2NullOrUndefined) { + if (isItem1NullOrUndefined && isItem2NullOrUndefined) { + return 0; + } + return isItem1NullOrUndefined ? 1 : -1; + } + + switch ((rowA[col]).$) { + case Constants.TableType.Int32: + case Constants.TableType.Int64: + case Constants.CassandraType.Int: + case Constants.CassandraType.Bigint: + case Constants.CassandraType.Smallint: + case Constants.CassandraType.Varint: + case Constants.CassandraType.Tinyint: + itemA = parseInt((rowA[col])._, 0); + itemB = parseInt((rowB[col])._, 0); + break; + case Constants.TableType.Double: + case Constants.CassandraType.Double: + case Constants.CassandraType.Float: + case Constants.CassandraType.Decimal: + itemA = parseFloat((rowA[col])._); + itemB = parseFloat((rowB[col])._); + break; + case Constants.TableType.DateTime: + itemA = new Date((rowA[col])._); + itemB = new Date((rowB[col])._); + break; + default: + itemA = (rowA[col])._.toLowerCase(); + itemB = (rowB[col])._.toLowerCase(); + } + var compareResult: number = itemA < itemB ? -1 : itemA > itemB ? 1 : 0; + if (compareResult !== 0) { + return sort.dir === "asc" ? compareResult : -compareResult; + } + } + return 0; + } + + protected isCacheValid(tableQuery: Entities.ITableQuery): boolean { + // Return false if either cache has no data or the search criteria don't match! + if (!this.cache || !this.cache.data || this.cache.length === 0) { + return false; + } + + if (!tableQuery && !this.cache.tableQuery) { + return true; + } + + // Compare by value using JSON representation + if (JSON.stringify(this.cache.tableQuery) !== JSON.stringify(tableQuery)) { + return false; + } + return true; + } + + // Override as table entity has special keys for a Data Table row. + /** + * @override + */ + protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean { + return itemKeys.every((property: Entities.IProperty) => { + return this.stringCompare(item[property.key]._, property.value); + }); + } + + private prefetchAndRender( + tableQuery: Entities.ITableQuery, + tablePageStartIndex: number, + tablePageSize: number, + downloadSize: number, + draw: number, + renderCallBack: Function, + oSettings: any, + columnSortOrder: any + ): void { + this.queryErrorMessage(null); + if (this.cache.serverCallInProgress) { + return; + } + this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0) + .then((result: IListTableEntitiesSegmentedResult) => { + if (!result) { + return; + } + + var entities = this.cache.data; + if ( + this.queryTablesTab.container.isPreferredApiCassandra() && + DataTableUtilities.checkForDefaultHeader(this.headers) + ) { + (this.queryTablesTab.container.tableDataClient) + .getTableSchema(this.queryTablesTab.collection) + .then((headers: CassandraTableKey[]) => { + this.updateHeaders( + headers.map((header) => header.property), + true + ); + }); + } else { + var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities( + entities, + this.queryTablesTab.container.isPreferredApiCassandra() + ); + var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers); + if (newHeaders.length > 0) { + // Any new columns found will be added into headers array, which will trigger a re-render of the DataTable. + // So there is no need to call it here. + this.updateHeaders(newHeaders, /* notifyColumnChanges */ true); + } else { + if (columnSortOrder) { + this.sortColumns(columnSortOrder, oSettings); + } + this.renderPage(renderCallBack, draw, tablePageStartIndex, tablePageSize, oSettings); + } + } + + if (result.ExceedMaximumRetries) { + var message: string = "We are having trouble getting your data. Please try again."; // localize + } + }) + .catch((error: any) => { + const parsedErrors = parseError(error); + var errors = parsedErrors.map((error) => { + return { + message: error.message, + start: error.location ? error.location.start : undefined, + end: error.location ? error.location.end : undefined, + code: error.code, + severity: error.severity, + }; + }); + this.queryErrorMessage(errors[0].message); + if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name, + databaseName: this.queryTablesTab.collection.databaseId, + collectionName: this.queryTablesTab.collection.id(), + defaultExperience: this.queryTablesTab.collection.container.defaultExperience(), + dataExplorerArea: Areas.Tab, + tabTitle: this.queryTablesTab.tabTitle(), + error: error, + }, + this.queryTablesTab.onLoadStartKey + ); + this.queryTablesTab.onLoadStartKey = null; + } + DataTableUtilities.turnOffProgressIndicator(); + }); + } + + /** + * Keep recursively prefetching items if: + * 1. Continuation token is not null + * 2. And prefetched items hasn't reach predefined cache size. + * 3. And retry times hasn't reach the predefined maximum retry number. + * + * It is possible for a query to return no results but still return a continuation header (e.g. if the query takes too long). + * If this is the case, we try to fetch entities again. + * Note that this also means that we can get less entities than the requested download size in a successful call. + * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx + */ + private prefetchData( + tableQuery: Entities.ITableQuery, + downloadSize: number, + currentRetry: number = 0 + ): Q.Promise { + if (!this.cache.serverCallInProgress) { + this.cache.serverCallInProgress = true; + this.allDownloaded = false; + this.lastPrefetchTime = new Date().getTime(); + var time = this.lastPrefetchTime; + + var promise: Q.Promise; + if (this._documentIterator && this.continuationToken) { + // TODO handle Cassandra case + + promise = Q(this._documentIterator.fetchNext().then((response) => response.resources)).then( + (documents: any[]) => { + let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); + let finalEntities: IListTableEntitiesSegmentedResult = { + Results: entities, + ContinuationToken: this._documentIterator.hasMoreResults(), + }; + return Q.resolve(finalEntities); + } + ); + } else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { + promise = Q( + this.queryTablesTab.container.tableDataClient.queryDocuments( + this.queryTablesTab.collection, + this.cqlQuery(), + true, + this.continuationToken + ) + ); + } else { + let query = this.sqlQuery(); + if (this.queryTablesTab.container.isPreferredApiCassandra()) { + query = this.cqlQuery(); + } + promise = Q( + this.queryTablesTab.container.tableDataClient.queryDocuments(this.queryTablesTab.collection, query, true) + ); + } + return promise + .then((result: IListTableEntitiesSegmentedResult) => { + if (!this._documentIterator) { + this._documentIterator = result.iterator; + } + var actualDownloadSize: number = 0; + + // If we hit this, it means another service call is triggered. We only handle the latest call. + // And as another service call is during process, we don't set serverCallInProgress to false here. + // Thus, end the prefetch. + if (this.lastPrefetchTime !== time) { + return Q.resolve(null); + } + + var entities = result.Results; + actualDownloadSize = entities.length; + + // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. + this.continuationToken = this.isCancelled ? null : result.ContinuationToken; + + if (!this.continuationToken) { + this.allDownloaded = true; + } + + if (this.isCacheValid(tableQuery)) { + // Append to cache. + this.cache.data = this.cache.data.concat(entities.slice(0)); + } else { + // Create cache. + this.cache.data = entities; + } + + this.cache.tableQuery = tableQuery; + this.cache.serverCallInProgress = false; + + var nextDownloadSize: number = downloadSize - actualDownloadSize; + if (nextDownloadSize === 0 && tableQuery.top) { + this.allDownloaded = true; + } + + // There are three possible results for a prefetch: + // 1. Continuation token is null or fetched items' size reaches predefined. + // 2. Continuation token is not null and fetched items' size hasn't reach predefined. + // 2.1 Retry times has reached predefined maximum. + // 2.2 Retry times hasn't reached predefined maximum. + // Correspondingly, + // For #1, end prefetch. + // For #2.1, set prefetch exceeds maximum retry number and end prefetch. + // For #2.2, go to next round prefetch. + if (this.allDownloaded || nextDownloadSize === 0) { + return Q.resolve(result); + } + + if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { + result.ExceedMaximumRetries = true; + return Q.resolve(result); + } + return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); + }) + .catch((error: Error) => { + this.cache.serverCallInProgress = false; + return Q.reject(error); + }); + } + return null; + } +} diff --git a/src/Explorer/Tables/Entities.ts b/src/Explorer/Tables/Entities.ts index f5f3f3386..fc9bef70b 100644 --- a/src/Explorer/Tables/Entities.ts +++ b/src/Explorer/Tables/Entities.ts @@ -1,38 +1,38 @@ -import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; - -export interface ITableEntity { - [property: string]: ITableEntityAttribute; -} - -export interface ITableEntityForTablesAPI extends ITableEntity { - PartitionKey: ITableEntityAttribute; - RowKey: ITableEntityAttribute; - Timestamp: ITableEntityAttribute; -} - -export interface ITableEntityAttribute { - _: string; // Value of a property - $?: string; // Edm Type -} - -export interface IListTableEntitiesResult { - Results: ITableEntity[]; - ContinuationToken: any; - iterator?: QueryIterator; -} - -export interface IProperty { - key: string; - subkey?: string; - value: string; -} - -export interface ITableQuery { - select?: string[]; - filter?: string; - top?: number; -} - -export interface ITableEntityIdentity { - RowKey: string; -} +import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; + +export interface ITableEntity { + [property: string]: ITableEntityAttribute; +} + +export interface ITableEntityForTablesAPI extends ITableEntity { + PartitionKey: ITableEntityAttribute; + RowKey: ITableEntityAttribute; + Timestamp: ITableEntityAttribute; +} + +export interface ITableEntityAttribute { + _: string; // Value of a property + $?: string; // Edm Type +} + +export interface IListTableEntitiesResult { + Results: ITableEntity[]; + ContinuationToken: any; + iterator?: QueryIterator; +} + +export interface IProperty { + key: string; + subkey?: string; + value: string; +} + +export interface ITableQuery { + select?: string[]; + filter?: string; + top?: number; +} + +export interface ITableEntityIdentity { + RowKey: string; +} diff --git a/src/Explorer/Tables/QueryBuilder/ClauseGroup.ts b/src/Explorer/Tables/QueryBuilder/ClauseGroup.ts index 4899166e5..6f677756a 100644 --- a/src/Explorer/Tables/QueryBuilder/ClauseGroup.ts +++ b/src/Explorer/Tables/QueryBuilder/ClauseGroup.ts @@ -1,327 +1,327 @@ -import QueryClauseViewModel from "./QueryClauseViewModel"; -import * as Utilities from "../Utilities"; - -export default class ClauseGroup { - public isRootGroup: boolean; - public children = new Array(); - public parentGroup: ClauseGroup; - private _id: string; - - constructor(isRootGroup: boolean, parentGroup: ClauseGroup, id?: string) { - this.isRootGroup = isRootGroup; - this.parentGroup = parentGroup; - this._id = id ? id : Utilities.guid(); - } - - /** - * Flattens the clause tree into an array, depth-first, left to right. - */ - public flattenClauses(targetArray: ko.ObservableArray): void { - var tempArray = new Array(); - - this.flattenClausesImpl(this, tempArray); - targetArray.removeAll(); - tempArray.forEach(element => { - targetArray.push(element); - }); - } - - public insertClauseBefore(newClause: QueryClauseViewModel, insertBefore?: QueryClauseViewModel): void { - if (!insertBefore) { - newClause.clauseGroup = this; - this.children.push(newClause); - } else { - var targetGroup = insertBefore.clauseGroup; - - if (targetGroup) { - var insertBeforeIndex = targetGroup.children.indexOf(insertBefore); - newClause.clauseGroup = targetGroup; - targetGroup.children.splice(insertBeforeIndex, 0, newClause); - } - } - } - - public deleteClause(clause: QueryClauseViewModel): void { - var targetGroup = clause.clauseGroup; - - if (targetGroup) { - var index = targetGroup.children.indexOf(clause); - targetGroup.children.splice(index, 1); - clause.dispose(); - - if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) { - var parent = targetGroup.parentGroup; - var targetGroupIndex = parent.children.indexOf(targetGroup); - - if (targetGroup.children.length === 1) { - var orphan = targetGroup.children.shift(); - - if (orphan instanceof QueryClauseViewModel) { - (orphan).clauseGroup = parent; - } else if (orphan instanceof ClauseGroup) { - (orphan).parentGroup = parent; - } - - parent.children.splice(targetGroupIndex, 1, orphan); - } else { - parent.children.splice(targetGroupIndex, 1); - } - } - } - } - - public removeAll(): void { - var allClauses: QueryClauseViewModel[] = new Array(); - - this.flattenClausesImpl(this, allClauses); - - while (allClauses.length > 0) { - allClauses.shift().dispose(); - } - - this.children = new Array(); - } - - /** - * Groups selected items. Returns True if a new group was created, otherwise False. - */ - public groupSelectedItems(): boolean { - // Find the selection start & end, also check for gaps between selected items (if found, cannot proceed). - var selection = this.getCheckedItemsInfo(); - - if (selection.canGroup) { - var newGroup = new ClauseGroup(false, this); - // Replace the selected items with the new group, and then move the selected items into the new group. - var groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup); - - groupedItems && - groupedItems.forEach(element => { - newGroup.children.push(element); - - if (element instanceof QueryClauseViewModel) { - (element).clauseGroup = newGroup; - } else if (element instanceof ClauseGroup) { - (element).parentGroup = newGroup; - } - }); - - this.unselectAll(); - - return true; - } - - return false; - } - - public ungroup(): void { - if (this.isRootGroup) { - return; - } - - var parentGroup = this.parentGroup; - var index = parentGroup.children.indexOf(this); - - if (index >= 0) { - parentGroup.children.splice(index, 1); - - var toPromote = this.children.splice(0, this.children.length); - - // Move all children one level up. - toPromote && - toPromote.forEach(element => { - if (element instanceof ClauseGroup) { - (element).parentGroup = parentGroup; - } else if (element instanceof QueryClauseViewModel) { - (element).clauseGroup = parentGroup; - } - - parentGroup.children.splice(index, 0, element); - index++; - }); - } - } - - public canGroupSelectedItems(): boolean { - return this.getCheckedItemsInfo().canGroup; - } - - public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup { - var deepest: ClauseGroup = this; - var level: number = 0; - var func = (currentGroup: ClauseGroup): void => { - level++; - if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) { - deepest = currentGroup; - } - - for (var i = 0; i < currentGroup.children.length; i++) { - var currentItem = currentGroup.children[i]; - - if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) { - func(currentItem); - } - } - level--; - }; - - func(this); - - return deepest; - } - - private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } { - var beginIndex = -1; - var endIndex = -1; - // In order to perform group, all selected items must be next to each other. - // If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group. - var gapFlag = false; - var count = 0; - - for (var i = 0; i < this.children.length; i++) { - var currentItem = this.children[i]; - var subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean }; - - if (currentItem instanceof ClauseGroup) { - subGroupSelectionState = (currentItem).getSelectionState(); - - if (subGroupSelectionState.partiallySelected) { - gapFlag = true; - break; - } - } - - if ( - beginIndex < 0 && - endIndex < 0 && - ((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) || - (currentItem instanceof ClauseGroup && subGroupSelectionState.allSelected)) - ) { - beginIndex = i; - } - - if ( - beginIndex >= 0 && - endIndex < 0 && - ((currentItem instanceof QueryClauseViewModel && !currentItem.checkedForGrouping.peek()) || - (currentItem instanceof ClauseGroup && !subGroupSelectionState.allSelected)) - ) { - endIndex = i - 1; - } - - if (beginIndex >= 0 && endIndex < 0) { - count++; - } - - if ( - beginIndex >= 0 && - endIndex >= 0 && - ((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) || - (currentItem instanceof ClauseGroup && !subGroupSelectionState.nonSelected)) - ) { - gapFlag = true; - break; - } - } - - if (!gapFlag && endIndex < 0) { - endIndex = this.children.length - 1; - } - - return { - canGroup: beginIndex >= 0 && !gapFlag && count > 1, - begin: beginIndex, - end: endIndex - }; - } - - private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } { - var selectedCount = 0; - - for (var i = 0; i < this.children.length; i++) { - var currentItem = this.children[i]; - - if (currentItem instanceof ClauseGroup && (currentItem).getSelectionState().allSelected) { - selectedCount++; - } - - if ( - currentItem instanceof QueryClauseViewModel && - (currentItem).checkedForGrouping.peek() - ) { - selectedCount++; - } - } - - return { - allSelected: selectedCount === this.children.length, - partiallySelected: selectedCount > 0 && selectedCount < this.children.length, - nonSelected: selectedCount === 0 - }; - } - - private unselectAll(): void { - for (var i = 0; i < this.children.length; i++) { - var currentItem = this.children[i]; - - if (currentItem instanceof ClauseGroup) { - (currentItem).unselectAll(); - } - - if (currentItem instanceof QueryClauseViewModel) { - (currentItem).checkedForGrouping(false); - } - } - } - - private flattenClausesImpl(queryGroup: ClauseGroup, targetArray: QueryClauseViewModel[]): void { - if (queryGroup.isRootGroup) { - targetArray.splice(0, targetArray.length); - } - - for (var i = 0; i < queryGroup.children.length; i++) { - var currentItem = queryGroup.children[i]; - - if (currentItem instanceof ClauseGroup) { - this.flattenClausesImpl(currentItem, targetArray); - } - - if (currentItem instanceof QueryClauseViewModel) { - targetArray.push(currentItem); - } - } - } - - public getTreeDepth(): number { - var currentDepth = this.getCurrentGroupDepth(); - - for (var i = 0; i < this.children.length; i++) { - var currentItem = this.children[i]; - - if (currentItem instanceof ClauseGroup) { - var newDepth = (currentItem).getTreeDepth(); - - if (newDepth > currentDepth) { - currentDepth = newDepth; - } - } - } - - return currentDepth; - } - - public getCurrentGroupDepth(): number { - var group = this; - var depth = 0; - - while (!group.isRootGroup) { - depth++; - group = group.parentGroup; - } - - return depth; - } - - public getId(): string { - return this._id; - } -} +import QueryClauseViewModel from "./QueryClauseViewModel"; +import * as Utilities from "../Utilities"; + +export default class ClauseGroup { + public isRootGroup: boolean; + public children = new Array(); + public parentGroup: ClauseGroup; + private _id: string; + + constructor(isRootGroup: boolean, parentGroup: ClauseGroup, id?: string) { + this.isRootGroup = isRootGroup; + this.parentGroup = parentGroup; + this._id = id ? id : Utilities.guid(); + } + + /** + * Flattens the clause tree into an array, depth-first, left to right. + */ + public flattenClauses(targetArray: ko.ObservableArray): void { + var tempArray = new Array(); + + this.flattenClausesImpl(this, tempArray); + targetArray.removeAll(); + tempArray.forEach((element) => { + targetArray.push(element); + }); + } + + public insertClauseBefore(newClause: QueryClauseViewModel, insertBefore?: QueryClauseViewModel): void { + if (!insertBefore) { + newClause.clauseGroup = this; + this.children.push(newClause); + } else { + var targetGroup = insertBefore.clauseGroup; + + if (targetGroup) { + var insertBeforeIndex = targetGroup.children.indexOf(insertBefore); + newClause.clauseGroup = targetGroup; + targetGroup.children.splice(insertBeforeIndex, 0, newClause); + } + } + } + + public deleteClause(clause: QueryClauseViewModel): void { + var targetGroup = clause.clauseGroup; + + if (targetGroup) { + var index = targetGroup.children.indexOf(clause); + targetGroup.children.splice(index, 1); + clause.dispose(); + + if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) { + var parent = targetGroup.parentGroup; + var targetGroupIndex = parent.children.indexOf(targetGroup); + + if (targetGroup.children.length === 1) { + var orphan = targetGroup.children.shift(); + + if (orphan instanceof QueryClauseViewModel) { + (orphan).clauseGroup = parent; + } else if (orphan instanceof ClauseGroup) { + (orphan).parentGroup = parent; + } + + parent.children.splice(targetGroupIndex, 1, orphan); + } else { + parent.children.splice(targetGroupIndex, 1); + } + } + } + } + + public removeAll(): void { + var allClauses: QueryClauseViewModel[] = new Array(); + + this.flattenClausesImpl(this, allClauses); + + while (allClauses.length > 0) { + allClauses.shift().dispose(); + } + + this.children = new Array(); + } + + /** + * Groups selected items. Returns True if a new group was created, otherwise False. + */ + public groupSelectedItems(): boolean { + // Find the selection start & end, also check for gaps between selected items (if found, cannot proceed). + var selection = this.getCheckedItemsInfo(); + + if (selection.canGroup) { + var newGroup = new ClauseGroup(false, this); + // Replace the selected items with the new group, and then move the selected items into the new group. + var groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup); + + groupedItems && + groupedItems.forEach((element) => { + newGroup.children.push(element); + + if (element instanceof QueryClauseViewModel) { + (element).clauseGroup = newGroup; + } else if (element instanceof ClauseGroup) { + (element).parentGroup = newGroup; + } + }); + + this.unselectAll(); + + return true; + } + + return false; + } + + public ungroup(): void { + if (this.isRootGroup) { + return; + } + + var parentGroup = this.parentGroup; + var index = parentGroup.children.indexOf(this); + + if (index >= 0) { + parentGroup.children.splice(index, 1); + + var toPromote = this.children.splice(0, this.children.length); + + // Move all children one level up. + toPromote && + toPromote.forEach((element) => { + if (element instanceof ClauseGroup) { + (element).parentGroup = parentGroup; + } else if (element instanceof QueryClauseViewModel) { + (element).clauseGroup = parentGroup; + } + + parentGroup.children.splice(index, 0, element); + index++; + }); + } + } + + public canGroupSelectedItems(): boolean { + return this.getCheckedItemsInfo().canGroup; + } + + public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup { + var deepest: ClauseGroup = this; + var level: number = 0; + var func = (currentGroup: ClauseGroup): void => { + level++; + if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) { + deepest = currentGroup; + } + + for (var i = 0; i < currentGroup.children.length; i++) { + var currentItem = currentGroup.children[i]; + + if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) { + func(currentItem); + } + } + level--; + }; + + func(this); + + return deepest; + } + + private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } { + var beginIndex = -1; + var endIndex = -1; + // In order to perform group, all selected items must be next to each other. + // If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group. + var gapFlag = false; + var count = 0; + + for (var i = 0; i < this.children.length; i++) { + var currentItem = this.children[i]; + var subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean }; + + if (currentItem instanceof ClauseGroup) { + subGroupSelectionState = (currentItem).getSelectionState(); + + if (subGroupSelectionState.partiallySelected) { + gapFlag = true; + break; + } + } + + if ( + beginIndex < 0 && + endIndex < 0 && + ((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) || + (currentItem instanceof ClauseGroup && subGroupSelectionState.allSelected)) + ) { + beginIndex = i; + } + + if ( + beginIndex >= 0 && + endIndex < 0 && + ((currentItem instanceof QueryClauseViewModel && !currentItem.checkedForGrouping.peek()) || + (currentItem instanceof ClauseGroup && !subGroupSelectionState.allSelected)) + ) { + endIndex = i - 1; + } + + if (beginIndex >= 0 && endIndex < 0) { + count++; + } + + if ( + beginIndex >= 0 && + endIndex >= 0 && + ((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) || + (currentItem instanceof ClauseGroup && !subGroupSelectionState.nonSelected)) + ) { + gapFlag = true; + break; + } + } + + if (!gapFlag && endIndex < 0) { + endIndex = this.children.length - 1; + } + + return { + canGroup: beginIndex >= 0 && !gapFlag && count > 1, + begin: beginIndex, + end: endIndex, + }; + } + + private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } { + var selectedCount = 0; + + for (var i = 0; i < this.children.length; i++) { + var currentItem = this.children[i]; + + if (currentItem instanceof ClauseGroup && (currentItem).getSelectionState().allSelected) { + selectedCount++; + } + + if ( + currentItem instanceof QueryClauseViewModel && + (currentItem).checkedForGrouping.peek() + ) { + selectedCount++; + } + } + + return { + allSelected: selectedCount === this.children.length, + partiallySelected: selectedCount > 0 && selectedCount < this.children.length, + nonSelected: selectedCount === 0, + }; + } + + private unselectAll(): void { + for (var i = 0; i < this.children.length; i++) { + var currentItem = this.children[i]; + + if (currentItem instanceof ClauseGroup) { + (currentItem).unselectAll(); + } + + if (currentItem instanceof QueryClauseViewModel) { + (currentItem).checkedForGrouping(false); + } + } + } + + private flattenClausesImpl(queryGroup: ClauseGroup, targetArray: QueryClauseViewModel[]): void { + if (queryGroup.isRootGroup) { + targetArray.splice(0, targetArray.length); + } + + for (var i = 0; i < queryGroup.children.length; i++) { + var currentItem = queryGroup.children[i]; + + if (currentItem instanceof ClauseGroup) { + this.flattenClausesImpl(currentItem, targetArray); + } + + if (currentItem instanceof QueryClauseViewModel) { + targetArray.push(currentItem); + } + } + } + + public getTreeDepth(): number { + var currentDepth = this.getCurrentGroupDepth(); + + for (var i = 0; i < this.children.length; i++) { + var currentItem = this.children[i]; + + if (currentItem instanceof ClauseGroup) { + var newDepth = (currentItem).getTreeDepth(); + + if (newDepth > currentDepth) { + currentDepth = newDepth; + } + } + } + + return currentDepth; + } + + public getCurrentGroupDepth(): number { + var group = this; + var depth = 0; + + while (!group.isRootGroup) { + depth++; + group = group.parentGroup; + } + + return depth; + } + + public getId(): string { + return this._id; + } +} diff --git a/src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts b/src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts index 3c904df38..5317a0303 100644 --- a/src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts +++ b/src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts @@ -1,49 +1,49 @@ -import * as ko from "knockout"; -import ClauseGroup from "./ClauseGroup"; -import QueryBuilderViewModel from "./QueryBuilderViewModel"; -import * as Constants from "../Constants"; - -/** - * View model for showing group indicators on UI, contains information such as group color and border styles. - */ -export default class ClauseGroupViewModel { - public ungroupClausesLabel = "Ungroup clauses"; // localize - - public backgroundColor: ko.Observable; - public canUngroup: ko.Observable; - public showTopBorder: ko.Observable; - public showLeftBorder: ko.Observable; - public showBottomBorder: ko.Observable; - public depth: ko.Observable; // for debugging purpose only, now showing the number on UI. - public borderBackgroundColor: ko.Observable; - - private _clauseGroup: ClauseGroup; - private _queryBuilderViewModel: QueryBuilderViewModel; - - constructor(clauseGroup: ClauseGroup, canUngroup: boolean, queryBuilderViewModel: QueryBuilderViewModel) { - this._clauseGroup = clauseGroup; - this._queryBuilderViewModel = queryBuilderViewModel; - this.backgroundColor = ko.observable(this.getGroupBackgroundColor(clauseGroup)); - this.canUngroup = ko.observable(canUngroup); - this.showTopBorder = ko.observable(false); - this.showLeftBorder = ko.observable(false); - this.showBottomBorder = ko.observable(false); - this.depth = ko.observable(clauseGroup.getCurrentGroupDepth()); - this.borderBackgroundColor = ko.observable("solid thin " + this.getGroupBackgroundColor(clauseGroup)); - } - - public ungroupClauses = (): void => { - this._clauseGroup.ungroup(); - this._queryBuilderViewModel.updateClauseArray(); - }; - - private getGroupBackgroundColor(group: ClauseGroup): string { - var colorCount = Constants.clauseGroupColors.length; - - if (group.isRootGroup) { - return Constants.transparentColor; - } else { - return Constants.clauseGroupColors[group.getCurrentGroupDepth() % colorCount]; - } - } -} +import * as ko from "knockout"; +import ClauseGroup from "./ClauseGroup"; +import QueryBuilderViewModel from "./QueryBuilderViewModel"; +import * as Constants from "../Constants"; + +/** + * View model for showing group indicators on UI, contains information such as group color and border styles. + */ +export default class ClauseGroupViewModel { + public ungroupClausesLabel = "Ungroup clauses"; // localize + + public backgroundColor: ko.Observable; + public canUngroup: ko.Observable; + public showTopBorder: ko.Observable; + public showLeftBorder: ko.Observable; + public showBottomBorder: ko.Observable; + public depth: ko.Observable; // for debugging purpose only, now showing the number on UI. + public borderBackgroundColor: ko.Observable; + + private _clauseGroup: ClauseGroup; + private _queryBuilderViewModel: QueryBuilderViewModel; + + constructor(clauseGroup: ClauseGroup, canUngroup: boolean, queryBuilderViewModel: QueryBuilderViewModel) { + this._clauseGroup = clauseGroup; + this._queryBuilderViewModel = queryBuilderViewModel; + this.backgroundColor = ko.observable(this.getGroupBackgroundColor(clauseGroup)); + this.canUngroup = ko.observable(canUngroup); + this.showTopBorder = ko.observable(false); + this.showLeftBorder = ko.observable(false); + this.showBottomBorder = ko.observable(false); + this.depth = ko.observable(clauseGroup.getCurrentGroupDepth()); + this.borderBackgroundColor = ko.observable("solid thin " + this.getGroupBackgroundColor(clauseGroup)); + } + + public ungroupClauses = (): void => { + this._clauseGroup.ungroup(); + this._queryBuilderViewModel.updateClauseArray(); + }; + + private getGroupBackgroundColor(group: ClauseGroup): string { + var colorCount = Constants.clauseGroupColors.length; + + if (group.isRootGroup) { + return Constants.transparentColor; + } else { + return Constants.clauseGroupColors[group.getCurrentGroupDepth() % colorCount]; + } + } +} diff --git a/src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts b/src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts index 5ee1036fd..a9879a56f 100644 --- a/src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts +++ b/src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts @@ -1,377 +1,377 @@ -import QueryBuilderViewModel from "./QueryBuilderViewModel"; -import QueryClauseViewModel from "./QueryClauseViewModel"; -import * as DateTimeUtilities from "./DateTimeUtilities"; - -/** - * Constants - */ -export var utc = "utc"; -export var local = "local"; - -export interface ITimestampQuery { - queryType: string; // valid values are "last" and "range" - lastNumber: number; // number value of a custom timestamp using the last option - lastTimeUnit: string; // timeunit of a custom timestamp using the last option - startTime: string; - endTime: string; - timeZone: string; // timezone of custom range timestamp, valid values are "local" and "utc" -} - -export interface ILastQuery { - lastNumber: number; - lastTimeUnit: string; -} - -export enum TimeUnit { - Seconds, - Minutes, - Hours, - Days -} - -/** - * Setting helpers - */ - -export function addRangeTimestamp( - timestamp: ITimestampQuery, - queryBuilderViewModel: QueryBuilderViewModel, - queryClauseViewModel: QueryClauseViewModel -): void { - queryBuilderViewModel.addCustomRange(timestamp, queryClauseViewModel); -} - -export function getDefaultStart(localTime: boolean, durationHours: number = 24): string { - var startTimestamp: string; - - var utcNowString: string = new Date().toISOString(); - var yesterday: Date = new Date(utcNowString); - - yesterday.setHours(yesterday.getHours() - durationHours); - startTimestamp = yesterday.toISOString(); - - if (localTime) { - startTimestamp = localFromUtcDateString(startTimestamp); - } - - return startTimestamp; -} - -export function getDefaultEnd(localTime: boolean): string { - var endTimestamp: string; - - var utcNowString: string = new Date().toISOString(); - - endTimestamp = utcNowString; - - if (localTime) { - endTimestamp = localFromUtcDateString(endTimestamp); - } - - return endTimestamp; -} - -export function parseDate(dateString: string, isUTC: boolean): Date { - // TODO validate dateString - var date: Date = null; - - if (dateString) { - try { - // Date string is assumed to be UTC in Storage Explorer Standalone. - // Behavior may vary in other browsers. - // Here's an example of how the string looks like "2015-10-24T21:44:12" - var millisecondTime = Date.parse(dateString), - parsed: Date = new Date(millisecondTime); - - if (isUTC) { - date = parsed; - } else { - // Since we parsed in UTC, accessors are flipped - we get local time from the getUTC* group - // Reinstating, the date is parsed above as UTC, and here we are creating a new date object - // in local time. - var year = parsed.getUTCFullYear(), - month = parsed.getUTCMonth(), - day = parsed.getUTCDate(), - hours = parsed.getUTCHours(), - minutes = parsed.getUTCMinutes(), - seconds = parsed.getUTCSeconds(), - milliseconds = parsed.getUTCMilliseconds(); - - date = new Date(year, month, day, hours, minutes, seconds, milliseconds); - } - } catch (error) { - //Debug.error("Error parsing date string: ", dateString, error); - } - } - - return date; -} - -export function utcFromLocalDateString(localDateString: string): string { - // TODO validate localDateString - var localDate = parseDate(localDateString, false), - utcDateString: string = null; - - if (localDate) { - utcDateString = localDate.toISOString(); - } - - return utcDateString; -} - -function padIfNeeded(value: number): string { - var padded: string = String(value); - - if (0 <= value && value < 10) { - padded = "0" + padded; - } - - return padded; -} - -function toLocalDateString(date: Date): string { - var localDateString: string = null; - - if (date) { - localDateString = - date.getFullYear() + - "-" + - padIfNeeded(date.getMonth() + 1) + - "-" + - padIfNeeded(date.getDate()) + - "T" + - padIfNeeded(date.getHours()) + - ":" + - padIfNeeded(date.getMinutes()) + - ":" + - padIfNeeded(date.getSeconds()); - } - - return localDateString; -} - -export function localFromUtcDateString(utcDateString: string): string { - // TODO validate utcDateString - var utcDate: Date = parseDate(utcDateString, true), - localDateString: string = null; - - if (utcDate) { - localDateString = toLocalDateString(utcDate); - } - - return localDateString; -} - -export function tryChangeTimestampTimeZone(koTimestamp: ko.Observable, toUTC: boolean): void { - if (koTimestamp) { - var currentDateString: string = koTimestamp(), - newDateString: string; - - if (currentDateString) { - if (toUTC) { - newDateString = utcFromLocalDateString(currentDateString); - // removing last character because cannot format it to html binding with the 'Z' at the end - newDateString = newDateString.substring(0, newDateString.length - 1); - } else { - newDateString = localFromUtcDateString(currentDateString); - } - - // utcFromLocalDateString and localFromUtcDateString could return null if currentDateString is invalid. - // Hence, only set koTimestamp if newDateString is not null. - if (newDateString) { - koTimestamp(newDateString); - } - } - } -} - -/** - * Input validation helpers - */ - -export var noTooltip = "", - invalidStartTimeTooltip = "Please provide a valid start time.", // localize - invalidExpiryTimeRequiredTooltip = "Required field. Please provide a valid expiry time.", // localize - invalidExpiryTimeGreaterThanStartTimeTooltip = "The expiry time must be greater than the start time."; // localize - -export function isDateString(dateString: string): boolean { - var success: boolean = false; - - if (dateString) { - var date: number = Date.parse(dateString); - - success = $.isNumeric(date); - } - - return success; -} - -// Is date string and earlier than expiry time; or is empty -// export function isInvalidStartTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult { -// var tooltip: string = noTooltip, -// isValid: boolean = isDateString(startTimestamp), -// startDate: Date, -// expiryDate: Date; - -// if (!isValid) { -// isValid = (startTimestamp === ""); -// } - -// if (!isValid) { -// tooltip = invalidStartTimeTooltip; -// } - -// if (isValid && !!startTimestamp && isDateString(expiryTimestamp)) { -// startDate = parseDate(startTimestamp, isUTC); -// expiryDate = parseDate(expiryTimestamp, isUTC); - -// isValid = (startDate < expiryDate); - -// if (!isValid) { -// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip; -// } -// } - -// return { isInvalid: !isValid, help: tooltip }; -// } - -// Is date string, and later than start time (if any) -// export function isInvalidExpiryTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult { -// var isValid: boolean = isDateString(expiryTimestamp), -// tooltip: string = isValid ? noTooltip : invalidExpiryTimeRequiredTooltip, -// startDate: Date, -// expiryDate: Date; - -// if (isValid && startTimestamp) { -// if (isDateString(startTimestamp)) { -// startDate = parseDate(startTimestamp, isUTC); -// expiryDate = parseDate(expiryTimestamp, isUTC); -// isValid = (startDate < expiryDate); - -// if (!isValid) { -// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip; -// } -// } -// } - -// return { isInvalid: !isValid, help: tooltip }; -// } - -/** - * Functions to calculate DateTime Strings - */ - -function _getLocalIsoDateTimeString(time: Date): string { - // yyyy-mm-ddThh:mm:ss.sss - // Not using the timezone offset (or 'Z'), which will make the - // date/time represent local time by default. - // var formatted = _string.sprintf( - // "%sT%02d:%02d:%02d.%03d", - // _getLocalIsoDateString(time), - // time.getHours(), - // time.getMinutes(), - // time.getSeconds(), - // time.getMilliseconds() - // ); - // return formatted; - return ( - _getLocalIsoDateString(time) + - "T" + - DateTimeUtilities.ensureDoubleDigits(time.getHours()) + - ":" + - DateTimeUtilities.ensureDoubleDigits(time.getMinutes()) + - ":" + - DateTimeUtilities.ensureDoubleDigits(time.getSeconds()) + - "." + - DateTimeUtilities.ensureTripleDigits(time.getMilliseconds()) - ); -} - -function _getLocalIsoDateString(date: Date): string { - return _getLocalIsoDateStringFromParts(date.getFullYear(), date.getMonth(), date.getDate()); -} - -function _getLocalIsoDateStringFromParts( - fullYear: number, - month: number /* 0..11 */, - date: number /* 1..31 */ -): string { - month = month + 1; - return ( - fullYear + "-" + DateTimeUtilities.ensureDoubleDigits(month) + "-" + DateTimeUtilities.ensureDoubleDigits(date) - ); - // return _string.sprintf( - // "%04d-%02d-%02d", - // fullYear, - // month + 1, // JS month is 0..11 - // date); // but date is 1..31 -} - -function _addDaysHours(time: Date, days: number, hours: number): Date { - var msPerHour = 1000 * 60 * 60; - var daysMs = days * msPerHour * 24; - var hoursMs = hours * msPerHour; - var newTimeMs = time.getTime() + daysMs + hoursMs; - return new Date(newTimeMs); -} - -function _daysHoursBeforeNow(days: number, hours: number): Date { - return _addDaysHours(new Date(), -days, -hours); -} - -export function _queryLastDaysHours(days: number, hours: number): string { - /* tslint:disable: no-unused-variable */ - var daysHoursAgo = _getLocalIsoDateTimeString(_daysHoursBeforeNow(days, hours)); - daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo); - - return daysHoursAgo; - /* tslint:enable: no-unused-variable */ -} - -export function _queryCurrentMonthLocal(): string { - var now = new Date(); - var start = _getLocalIsoDateStringFromParts(now.getFullYear(), now.getMonth(), 1); - start = DateTimeUtilities.getUTCDateTime(start); - return start; -} - -export function _queryCurrentYearLocal(): string { - var now = new Date(); - var start = _getLocalIsoDateStringFromParts(now.getFullYear(), 0, 1); // Month is 0..11, date is 1..31 - start = DateTimeUtilities.getUTCDateTime(start); - return start; -} - -function _addTime(time: Date, lastNumber: number, timeUnit: string): Date { - var timeMS: number; - switch (TimeUnit[Number(timeUnit)]) { - case TimeUnit.Days.toString(): - timeMS = lastNumber * 1000 * 60 * 60 * 24; - break; - case TimeUnit.Hours.toString(): - timeMS = lastNumber * 1000 * 60 * 60; - break; - case TimeUnit.Minutes.toString(): - timeMS = lastNumber * 1000 * 60; - break; - case TimeUnit.Seconds.toString(): - timeMS = lastNumber * 1000; - break; - default: - //throw new Errors.ArgumentOutOfRangeError(timeUnit); - } - var newTimeMS = time.getTime() + timeMS; - return new Date(newTimeMS); -} - -function _timeBeforeNow(lastNumber: number, timeUnit: string): Date { - return _addTime(new Date(), -lastNumber, timeUnit); -} - -export function _queryLastTime(lastNumber: number, timeUnit: string): string { - /* tslint:disable: no-unused-variable */ - var daysHoursAgo = _getLocalIsoDateTimeString(_timeBeforeNow(lastNumber, timeUnit)); - daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo); - return daysHoursAgo; - /* tslint:enable: no-unused-variable */ -} +import QueryBuilderViewModel from "./QueryBuilderViewModel"; +import QueryClauseViewModel from "./QueryClauseViewModel"; +import * as DateTimeUtilities from "./DateTimeUtilities"; + +/** + * Constants + */ +export var utc = "utc"; +export var local = "local"; + +export interface ITimestampQuery { + queryType: string; // valid values are "last" and "range" + lastNumber: number; // number value of a custom timestamp using the last option + lastTimeUnit: string; // timeunit of a custom timestamp using the last option + startTime: string; + endTime: string; + timeZone: string; // timezone of custom range timestamp, valid values are "local" and "utc" +} + +export interface ILastQuery { + lastNumber: number; + lastTimeUnit: string; +} + +export enum TimeUnit { + Seconds, + Minutes, + Hours, + Days, +} + +/** + * Setting helpers + */ + +export function addRangeTimestamp( + timestamp: ITimestampQuery, + queryBuilderViewModel: QueryBuilderViewModel, + queryClauseViewModel: QueryClauseViewModel +): void { + queryBuilderViewModel.addCustomRange(timestamp, queryClauseViewModel); +} + +export function getDefaultStart(localTime: boolean, durationHours: number = 24): string { + var startTimestamp: string; + + var utcNowString: string = new Date().toISOString(); + var yesterday: Date = new Date(utcNowString); + + yesterday.setHours(yesterday.getHours() - durationHours); + startTimestamp = yesterday.toISOString(); + + if (localTime) { + startTimestamp = localFromUtcDateString(startTimestamp); + } + + return startTimestamp; +} + +export function getDefaultEnd(localTime: boolean): string { + var endTimestamp: string; + + var utcNowString: string = new Date().toISOString(); + + endTimestamp = utcNowString; + + if (localTime) { + endTimestamp = localFromUtcDateString(endTimestamp); + } + + return endTimestamp; +} + +export function parseDate(dateString: string, isUTC: boolean): Date { + // TODO validate dateString + var date: Date = null; + + if (dateString) { + try { + // Date string is assumed to be UTC in Storage Explorer Standalone. + // Behavior may vary in other browsers. + // Here's an example of how the string looks like "2015-10-24T21:44:12" + var millisecondTime = Date.parse(dateString), + parsed: Date = new Date(millisecondTime); + + if (isUTC) { + date = parsed; + } else { + // Since we parsed in UTC, accessors are flipped - we get local time from the getUTC* group + // Reinstating, the date is parsed above as UTC, and here we are creating a new date object + // in local time. + var year = parsed.getUTCFullYear(), + month = parsed.getUTCMonth(), + day = parsed.getUTCDate(), + hours = parsed.getUTCHours(), + minutes = parsed.getUTCMinutes(), + seconds = parsed.getUTCSeconds(), + milliseconds = parsed.getUTCMilliseconds(); + + date = new Date(year, month, day, hours, minutes, seconds, milliseconds); + } + } catch (error) { + //Debug.error("Error parsing date string: ", dateString, error); + } + } + + return date; +} + +export function utcFromLocalDateString(localDateString: string): string { + // TODO validate localDateString + var localDate = parseDate(localDateString, false), + utcDateString: string = null; + + if (localDate) { + utcDateString = localDate.toISOString(); + } + + return utcDateString; +} + +function padIfNeeded(value: number): string { + var padded: string = String(value); + + if (0 <= value && value < 10) { + padded = "0" + padded; + } + + return padded; +} + +function toLocalDateString(date: Date): string { + var localDateString: string = null; + + if (date) { + localDateString = + date.getFullYear() + + "-" + + padIfNeeded(date.getMonth() + 1) + + "-" + + padIfNeeded(date.getDate()) + + "T" + + padIfNeeded(date.getHours()) + + ":" + + padIfNeeded(date.getMinutes()) + + ":" + + padIfNeeded(date.getSeconds()); + } + + return localDateString; +} + +export function localFromUtcDateString(utcDateString: string): string { + // TODO validate utcDateString + var utcDate: Date = parseDate(utcDateString, true), + localDateString: string = null; + + if (utcDate) { + localDateString = toLocalDateString(utcDate); + } + + return localDateString; +} + +export function tryChangeTimestampTimeZone(koTimestamp: ko.Observable, toUTC: boolean): void { + if (koTimestamp) { + var currentDateString: string = koTimestamp(), + newDateString: string; + + if (currentDateString) { + if (toUTC) { + newDateString = utcFromLocalDateString(currentDateString); + // removing last character because cannot format it to html binding with the 'Z' at the end + newDateString = newDateString.substring(0, newDateString.length - 1); + } else { + newDateString = localFromUtcDateString(currentDateString); + } + + // utcFromLocalDateString and localFromUtcDateString could return null if currentDateString is invalid. + // Hence, only set koTimestamp if newDateString is not null. + if (newDateString) { + koTimestamp(newDateString); + } + } + } +} + +/** + * Input validation helpers + */ + +export var noTooltip = "", + invalidStartTimeTooltip = "Please provide a valid start time.", // localize + invalidExpiryTimeRequiredTooltip = "Required field. Please provide a valid expiry time.", // localize + invalidExpiryTimeGreaterThanStartTimeTooltip = "The expiry time must be greater than the start time."; // localize + +export function isDateString(dateString: string): boolean { + var success: boolean = false; + + if (dateString) { + var date: number = Date.parse(dateString); + + success = $.isNumeric(date); + } + + return success; +} + +// Is date string and earlier than expiry time; or is empty +// export function isInvalidStartTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult { +// var tooltip: string = noTooltip, +// isValid: boolean = isDateString(startTimestamp), +// startDate: Date, +// expiryDate: Date; + +// if (!isValid) { +// isValid = (startTimestamp === ""); +// } + +// if (!isValid) { +// tooltip = invalidStartTimeTooltip; +// } + +// if (isValid && !!startTimestamp && isDateString(expiryTimestamp)) { +// startDate = parseDate(startTimestamp, isUTC); +// expiryDate = parseDate(expiryTimestamp, isUTC); + +// isValid = (startDate < expiryDate); + +// if (!isValid) { +// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip; +// } +// } + +// return { isInvalid: !isValid, help: tooltip }; +// } + +// Is date string, and later than start time (if any) +// export function isInvalidExpiryTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult { +// var isValid: boolean = isDateString(expiryTimestamp), +// tooltip: string = isValid ? noTooltip : invalidExpiryTimeRequiredTooltip, +// startDate: Date, +// expiryDate: Date; + +// if (isValid && startTimestamp) { +// if (isDateString(startTimestamp)) { +// startDate = parseDate(startTimestamp, isUTC); +// expiryDate = parseDate(expiryTimestamp, isUTC); +// isValid = (startDate < expiryDate); + +// if (!isValid) { +// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip; +// } +// } +// } + +// return { isInvalid: !isValid, help: tooltip }; +// } + +/** + * Functions to calculate DateTime Strings + */ + +function _getLocalIsoDateTimeString(time: Date): string { + // yyyy-mm-ddThh:mm:ss.sss + // Not using the timezone offset (or 'Z'), which will make the + // date/time represent local time by default. + // var formatted = _string.sprintf( + // "%sT%02d:%02d:%02d.%03d", + // _getLocalIsoDateString(time), + // time.getHours(), + // time.getMinutes(), + // time.getSeconds(), + // time.getMilliseconds() + // ); + // return formatted; + return ( + _getLocalIsoDateString(time) + + "T" + + DateTimeUtilities.ensureDoubleDigits(time.getHours()) + + ":" + + DateTimeUtilities.ensureDoubleDigits(time.getMinutes()) + + ":" + + DateTimeUtilities.ensureDoubleDigits(time.getSeconds()) + + "." + + DateTimeUtilities.ensureTripleDigits(time.getMilliseconds()) + ); +} + +function _getLocalIsoDateString(date: Date): string { + return _getLocalIsoDateStringFromParts(date.getFullYear(), date.getMonth(), date.getDate()); +} + +function _getLocalIsoDateStringFromParts( + fullYear: number, + month: number /* 0..11 */, + date: number /* 1..31 */ +): string { + month = month + 1; + return ( + fullYear + "-" + DateTimeUtilities.ensureDoubleDigits(month) + "-" + DateTimeUtilities.ensureDoubleDigits(date) + ); + // return _string.sprintf( + // "%04d-%02d-%02d", + // fullYear, + // month + 1, // JS month is 0..11 + // date); // but date is 1..31 +} + +function _addDaysHours(time: Date, days: number, hours: number): Date { + var msPerHour = 1000 * 60 * 60; + var daysMs = days * msPerHour * 24; + var hoursMs = hours * msPerHour; + var newTimeMs = time.getTime() + daysMs + hoursMs; + return new Date(newTimeMs); +} + +function _daysHoursBeforeNow(days: number, hours: number): Date { + return _addDaysHours(new Date(), -days, -hours); +} + +export function _queryLastDaysHours(days: number, hours: number): string { + /* tslint:disable: no-unused-variable */ + var daysHoursAgo = _getLocalIsoDateTimeString(_daysHoursBeforeNow(days, hours)); + daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo); + + return daysHoursAgo; + /* tslint:enable: no-unused-variable */ +} + +export function _queryCurrentMonthLocal(): string { + var now = new Date(); + var start = _getLocalIsoDateStringFromParts(now.getFullYear(), now.getMonth(), 1); + start = DateTimeUtilities.getUTCDateTime(start); + return start; +} + +export function _queryCurrentYearLocal(): string { + var now = new Date(); + var start = _getLocalIsoDateStringFromParts(now.getFullYear(), 0, 1); // Month is 0..11, date is 1..31 + start = DateTimeUtilities.getUTCDateTime(start); + return start; +} + +function _addTime(time: Date, lastNumber: number, timeUnit: string): Date { + var timeMS: number; + switch (TimeUnit[Number(timeUnit)]) { + case TimeUnit.Days.toString(): + timeMS = lastNumber * 1000 * 60 * 60 * 24; + break; + case TimeUnit.Hours.toString(): + timeMS = lastNumber * 1000 * 60 * 60; + break; + case TimeUnit.Minutes.toString(): + timeMS = lastNumber * 1000 * 60; + break; + case TimeUnit.Seconds.toString(): + timeMS = lastNumber * 1000; + break; + default: + //throw new Errors.ArgumentOutOfRangeError(timeUnit); + } + var newTimeMS = time.getTime() + timeMS; + return new Date(newTimeMS); +} + +function _timeBeforeNow(lastNumber: number, timeUnit: string): Date { + return _addTime(new Date(), -lastNumber, timeUnit); +} + +export function _queryLastTime(lastNumber: number, timeUnit: string): string { + /* tslint:disable: no-unused-variable */ + var daysHoursAgo = _getLocalIsoDateTimeString(_timeBeforeNow(lastNumber, timeUnit)); + daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo); + return daysHoursAgo; + /* tslint:enable: no-unused-variable */ +} diff --git a/src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts b/src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts index 57c75b129..810757f60 100644 --- a/src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts +++ b/src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts @@ -1,67 +1,67 @@ -const epochTicks = 621355968000000000; -const ticksPerMillisecond = 10000; - -export function getLocalDateTime(dateTime: string): string { - var dateTimeObject: Date = new Date(dateTime); - var year: number = dateTimeObject.getFullYear(); - var month: string = ensureDoubleDigits(dateTimeObject.getMonth() + 1); // Month ranges from 0 to 11 - var day: string = ensureDoubleDigits(dateTimeObject.getDate()); - var hours: string = ensureDoubleDigits(dateTimeObject.getHours()); - var minutes: string = ensureDoubleDigits(dateTimeObject.getMinutes()); - var seconds: string = ensureDoubleDigits(dateTimeObject.getSeconds()); - var milliseconds: string = ensureTripleDigits(dateTimeObject.getMilliseconds()); - - var localDateTime: string = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}`; - return localDateTime; -} - -export function getUTCDateTime(dateTime: string): string { - var dateTimeObject: Date = new Date(dateTime); - return dateTimeObject.toISOString(); -} - -export function ensureDoubleDigits(num: number): string { - var doubleDigitsString: string = num.toString(); - if (num < 10) { - doubleDigitsString = `0${doubleDigitsString}`; - } else if (num > 99) { - doubleDigitsString = doubleDigitsString.substring(0, 2); - } - return doubleDigitsString; -} - -export function ensureTripleDigits(num: number): string { - var tripleDigitsString: string = num.toString(); - if (num < 10) { - tripleDigitsString = `00${tripleDigitsString}`; - } else if (num < 100) { - tripleDigitsString = `0${tripleDigitsString}`; - } else if (num > 999) { - tripleDigitsString = tripleDigitsString.substring(0, 3); - } - return tripleDigitsString; -} - -export function convertUnixToJSDate(unixTime: number): Date { - return new Date(unixTime * 1000); -} - -export function convertJSDateToUnix(dateTime: string): number { - return Number((new Date(dateTime).getTime() / 1000).toFixed(0)); -} - -export function convertTicksToJSDate(ticks: string): Date { - var ticksJSBased = Number(ticks) - epochTicks; - var timeInMillisecond = ticksJSBased / ticksPerMillisecond; - return new Date(timeInMillisecond); -} - -export function convertJSDateToTicksWithPadding(dateTime: string): string { - var ticks = epochTicks + new Date(dateTime).getTime() * ticksPerMillisecond; - return padDateTicksWithZeros(ticks.toString()); -} - -function padDateTicksWithZeros(value: string): string { - var s = "0000000000000000000" + value; - return s.substr(s.length - 20); -} +const epochTicks = 621355968000000000; +const ticksPerMillisecond = 10000; + +export function getLocalDateTime(dateTime: string): string { + var dateTimeObject: Date = new Date(dateTime); + var year: number = dateTimeObject.getFullYear(); + var month: string = ensureDoubleDigits(dateTimeObject.getMonth() + 1); // Month ranges from 0 to 11 + var day: string = ensureDoubleDigits(dateTimeObject.getDate()); + var hours: string = ensureDoubleDigits(dateTimeObject.getHours()); + var minutes: string = ensureDoubleDigits(dateTimeObject.getMinutes()); + var seconds: string = ensureDoubleDigits(dateTimeObject.getSeconds()); + var milliseconds: string = ensureTripleDigits(dateTimeObject.getMilliseconds()); + + var localDateTime: string = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}`; + return localDateTime; +} + +export function getUTCDateTime(dateTime: string): string { + var dateTimeObject: Date = new Date(dateTime); + return dateTimeObject.toISOString(); +} + +export function ensureDoubleDigits(num: number): string { + var doubleDigitsString: string = num.toString(); + if (num < 10) { + doubleDigitsString = `0${doubleDigitsString}`; + } else if (num > 99) { + doubleDigitsString = doubleDigitsString.substring(0, 2); + } + return doubleDigitsString; +} + +export function ensureTripleDigits(num: number): string { + var tripleDigitsString: string = num.toString(); + if (num < 10) { + tripleDigitsString = `00${tripleDigitsString}`; + } else if (num < 100) { + tripleDigitsString = `0${tripleDigitsString}`; + } else if (num > 999) { + tripleDigitsString = tripleDigitsString.substring(0, 3); + } + return tripleDigitsString; +} + +export function convertUnixToJSDate(unixTime: number): Date { + return new Date(unixTime * 1000); +} + +export function convertJSDateToUnix(dateTime: string): number { + return Number((new Date(dateTime).getTime() / 1000).toFixed(0)); +} + +export function convertTicksToJSDate(ticks: string): Date { + var ticksJSBased = Number(ticks) - epochTicks; + var timeInMillisecond = ticksJSBased / ticksPerMillisecond; + return new Date(timeInMillisecond); +} + +export function convertJSDateToTicksWithPadding(dateTime: string): string { + var ticks = epochTicks + new Date(dateTime).getTime() * ticksPerMillisecond; + return padDateTicksWithZeros(ticks.toString()); +} + +function padDateTicksWithZeros(value: string): string { + var s = "0000000000000000000" + value; + return s.substr(s.length - 20); +} diff --git a/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts b/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts index 3d0c26092..523c83dee 100644 --- a/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts +++ b/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts @@ -1,797 +1,797 @@ -import * as ko from "knockout"; -import * as CustomTimestampHelper from "./CustomTimestampHelper"; -import { getQuotedCqlIdentifier } from "../CqlUtilities"; -import QueryClauseViewModel from "./QueryClauseViewModel"; -import ClauseGroup from "./ClauseGroup"; -import ClauseGroupViewModel from "./ClauseGroupViewModel"; -import QueryViewModel from "./QueryViewModel"; -import * as Constants from "../Constants"; -import TableEntityListViewModel from "../DataTable/TableEntityListViewModel"; -import * as DateTimeUtilities from "./DateTimeUtilities"; -import * as DataTableUtilities from "../DataTable/DataTableUtilities"; -import * as TableEntityProcessor from "../TableEntityProcessor"; -import * as Utilities from "../Utilities"; -import { KeyCodes } from "../../../Common/Constants"; - -export default class QueryBuilderViewModel { - /* Labels */ - public andLabel = "And/Or"; // localize - public actionLabel = "Action"; // localize - public fieldLabel = "Field"; // localize - public dataTypeLabel = "Type"; // localize - public operatorLabel = "Operator"; // localize - public valueLabel = "Value"; // localize - - /* controls */ - public addNewClauseLine = "Add new clause"; // localize - public insertNewFilterLine = "Insert new filter line"; // localize - public removeThisFilterLine = "Remove this filter line"; // localize - public groupSelectedClauses = "Group selected clauses"; // localize - public clauseArray = ko.observableArray(); // This is for storing the clauses in flattened form queryClauses for easier UI data binding. - public queryClauses = new ClauseGroup(true, null); // The actual data structure containing the clause information. - public columnOptions: ko.ObservableArray; - public canGroupClauses = ko.observable(false); - - /* Observables */ - public edmTypes = ko.observableArray([ - Constants.TableType.String, - Constants.TableType.Boolean, - Constants.TableType.Binary, - Constants.TableType.DateTime, - Constants.TableType.Double, - Constants.TableType.Guid, - Constants.TableType.Int32, - Constants.TableType.Int64, - "" - ]); - public operators = ko.observableArray([ - Constants.Operator.Equal, - Constants.Operator.GreaterThan, - Constants.Operator.GreaterThanOrEqualTo, - Constants.Operator.LessThan, - Constants.Operator.LessThanOrEqualTo, - Constants.Operator.NotEqualTo, - "" - ]); - public clauseRules = ko.observableArray([Constants.ClauseRule.And, Constants.ClauseRule.Or]); - public timeOptions = ko.observableArray([ - Constants.timeOptions.lastHour, - Constants.timeOptions.last24Hours, - Constants.timeOptions.last7Days, - Constants.timeOptions.last31Days, - Constants.timeOptions.last365Days, - Constants.timeOptions.currentMonth, - Constants.timeOptions.currentYear - //Constants.timeOptions.custom - ]); - public queryString = ko.observable(); - private _queryViewModel: QueryViewModel; - public tableEntityListViewModel: TableEntityListViewModel; - private scrollEventListener: boolean; - - constructor(queryViewModel: QueryViewModel, tableEntityListViewModel: TableEntityListViewModel) { - if (tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) { - this.edmTypes([ - Constants.CassandraType.Text, - Constants.CassandraType.Ascii, - Constants.CassandraType.Bigint, - Constants.CassandraType.Blob, - Constants.CassandraType.Boolean, - Constants.CassandraType.Decimal, - Constants.CassandraType.Double, - Constants.CassandraType.Float, - Constants.CassandraType.Int, - Constants.CassandraType.Uuid, - Constants.CassandraType.Varchar, - Constants.CassandraType.Varint, - Constants.CassandraType.Inet, - Constants.CassandraType.Smallint, - Constants.CassandraType.Tinyint - ]); - this.clauseRules([ - Constants.ClauseRule.And - // OR is not supported in CQL - ]); - this.andLabel = "And"; - } - this.clauseArray(); - - this._queryViewModel = queryViewModel; - this.tableEntityListViewModel = tableEntityListViewModel; - this.columnOptions = ko.observableArray(queryViewModel.columnOptions()); - - this.columnOptions.subscribe(newColumnOptions => { - queryViewModel.columnOptions(newColumnOptions); - }); - } - - public setExample() { - var example1 = new QueryClauseViewModel( - this, - "", - "PartitionKey", - this.edmTypes()[0], - Constants.Operator.Equal, - this.tableEntityListViewModel.items()[0].PartitionKey._, - false, - "", - "", - "", - //null, - true - ); - var example2 = new QueryClauseViewModel( - this, - "And", - "RowKey", - this.edmTypes()[0], - Constants.Operator.Equal, - this.tableEntityListViewModel.items()[0].RowKey._, - true, - "", - "", - "", - //null, - true - ); - this.addClauseImpl(example1, 0); - this.addClauseImpl(example2, 1); - } - - public getODataFilterFromClauses = (): string => { - var filterString: string = ""; - var treeTraversal = (group: ClauseGroup): void => { - for (var i = 0; i < group.children.length; i++) { - var currentItem = group.children[i]; - - if (currentItem instanceof QueryClauseViewModel) { - var clause = currentItem; - this.timestampToValue(clause); - filterString = filterString.concat( - this.constructODataClause( - filterString === "" ? "" : clause.and_or(), - this.generateLeftParentheses(clause), - clause.field(), - clause.type(), - clause.operator(), - clause.value(), - this.generateRightParentheses(clause) - ) - ); - } - - if (currentItem instanceof ClauseGroup) { - treeTraversal(currentItem); - } - } - }; - - treeTraversal(this.queryClauses); - - return filterString.trim(); - }; - - public getSqlFilterFromClauses = (): string => { - var filterString: string = "SELECT * FROM c"; - if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) { - filterString = "SELECT"; - const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText(); - selectText && - selectText.forEach((value: string) => { - if (value === Constants.EntityKeyNames.PartitionKey) { - value = `["${TableEntityProcessor.keyProperties.PartitionKey}"]`; - filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c"); - } else if (value === Constants.EntityKeyNames.RowKey) { - value = `["${TableEntityProcessor.keyProperties.Id2}"]`; - filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c"); - } else { - if (value === Constants.EntityKeyNames.Timestamp) { - value = TableEntityProcessor.keyProperties.Timestamp; - } - filterString = filterString.concat(filterString === "SELECT" ? " c." : ", c."); - } - filterString = filterString.concat(value); - }); - filterString = filterString.concat(" FROM c"); - } - if (this.queryClauses.children.length === 0) { - return filterString; - } - filterString = filterString.concat(" WHERE"); - var first = true; - var treeTraversal = (group: ClauseGroup): void => { - for (var i = 0; i < group.children.length; i++) { - var currentItem = group.children[i]; - - if (currentItem instanceof QueryClauseViewModel) { - var clause = currentItem; - let timeStampValue: string = this.timestampToSqlValue(clause); - var value = clause.value(); - if (!clause.isValue()) { - value = timeStampValue; - } - filterString = filterString.concat( - this.constructSqlClause( - first ? "" : clause.and_or(), - this.generateLeftParentheses(clause), - clause.field(), - clause.type(), - clause.operator(), - value, - this.generateRightParentheses(clause) - ) - ); - first = false; - } - - if (currentItem instanceof ClauseGroup) { - treeTraversal(currentItem); - } - } - }; - - treeTraversal(this.queryClauses); - - return filterString.trim(); - }; - - public getCqlFilterFromClauses = (): string => { - const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId; - const collectionId = this._queryViewModel.queryTablesTab.collection.id(); - const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`; - var filterString: string = `SELECT * FROM ${tableToQuery}`; - if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) { - filterString = "SELECT"; - const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText(); - selectText && - selectText.forEach((value: string) => { - filterString = filterString.concat(filterString === "SELECT" ? " " : ", "); - filterString = filterString.concat(value); - }); - filterString = filterString.concat(` FROM ${tableToQuery}`); - } - if (this.queryClauses.children.length === 0) { - return filterString; - } - filterString = filterString.concat(" WHERE"); - var first = true; - var treeTraversal = (group: ClauseGroup): void => { - for (var i = 0; i < group.children.length; i++) { - var currentItem = group.children[i]; - - if (currentItem instanceof QueryClauseViewModel) { - var clause = currentItem; - let timeStampValue: string = this.timestampToSqlValue(clause); - var value = clause.value(); - if (!clause.isValue()) { - value = timeStampValue; - } - filterString = filterString.concat( - this.constructCqlClause( - first ? "" : clause.and_or(), - this.generateLeftParentheses(clause), - clause.field(), - clause.type(), - clause.operator(), - value, - this.generateRightParentheses(clause) - ) - ); - first = false; - } - - if (currentItem instanceof ClauseGroup) { - treeTraversal(currentItem); - } - } - }; - - treeTraversal(this.queryClauses); - - return filterString.trim(); - }; - - public updateColumnOptions = (): void => { - let originalHeaders = this.columnOptions(); - let newHeaders = this.tableEntityListViewModel.headers; - this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns)); - }; - - private generateLeftParentheses(clause: QueryClauseViewModel): string { - var result = ""; - - if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) { - return result; - } else { - result = result.concat("("); - } - - var currentGroup: ClauseGroup = clause.clauseGroup; - - while ( - !currentGroup.isRootGroup && - !currentGroup.parentGroup.isRootGroup && - currentGroup.parentGroup.children.indexOf(currentGroup) === 0 - ) { - result = result.concat("("); - currentGroup = currentGroup.parentGroup; - } - - return result; - } - - private generateRightParentheses(clause: QueryClauseViewModel): string { - var result = ""; - - if ( - clause.clauseGroup.isRootGroup || - clause.clauseGroup.children.indexOf(clause) !== clause.clauseGroup.children.length - 1 - ) { - return result; - } else { - result = result.concat(")"); - } - - var currentGroup: ClauseGroup = clause.clauseGroup; - - while ( - !currentGroup.isRootGroup && - !currentGroup.parentGroup.isRootGroup && - currentGroup.parentGroup.children.indexOf(currentGroup) === currentGroup.parentGroup.children.length - 1 - ) { - result = result.concat(")"); - currentGroup = currentGroup.parentGroup; - } - - return result; - } - - private constructODataClause = ( - clauseRule: string, - leftParentheses: string, - propertyName: string, - type: string, - operator: string, - value: string, - rightParentheses: string - ): string => { - switch (type) { - case Constants.TableType.DateTime: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( - operator - )} ${value}${rightParentheses}`; - case Constants.TableType.String: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( - operator - )} \'${value}\'${rightParentheses}`; - case Constants.TableType.Guid: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( - operator - )} guid\'${value}\'${rightParentheses}`; - case Constants.TableType.Binary: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( - operator - )} binary\'${value}\'${rightParentheses}`; - default: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( - operator - )} ${value}${rightParentheses}`; - } - }; - - private constructSqlClause = ( - clauseRule: string, - leftParentheses: string, - propertyName: string, - type: string, - operator: string, - value: string, - rightParentheses: string - ): string => { - if (propertyName === Constants.EntityKeyNames.PartitionKey) { - propertyName = TableEntityProcessor.keyProperties.PartitionKey; - return ` ${clauseRule.toLowerCase()} ${leftParentheses}c["${propertyName}"] ${operator} \'${value}\'${rightParentheses}`; - } else if (propertyName === Constants.EntityKeyNames.RowKey) { - propertyName = TableEntityProcessor.keyProperties.Id; - return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} \'${value}\'${rightParentheses}`; - } else if (propertyName === Constants.EntityKeyNames.Timestamp) { - propertyName = TableEntityProcessor.keyProperties.Timestamp; - return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} ${DateTimeUtilities.convertJSDateToUnix( - value - )}${rightParentheses}`; - } - switch (type) { - case Constants.TableType.DateTime: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${DateTimeUtilities.convertJSDateToTicksWithPadding( - value - )}\'${rightParentheses}`; - case Constants.TableType.Int64: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${Utilities.padLongWithZeros( - value - )}\'${rightParentheses}`; - case Constants.TableType.String: - case Constants.TableType.Guid: - case Constants.TableType.Binary: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${value}\'${rightParentheses}`; - default: - return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} ${value}${rightParentheses}`; - } - }; - - private constructCqlClause = ( - clauseRule: string, - leftParentheses: string, - propertyName: string, - type: string, - operator: string, - value: string, - rightParentheses: string - ): string => { - if ( - type === Constants.CassandraType.Text || - type === Constants.CassandraType.Inet || - type === Constants.CassandraType.Ascii || - type === Constants.CassandraType.Varchar - ) { - return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} \'${value}\'${rightParentheses}`; - } - return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} ${value}${rightParentheses}`; - }; - - private operatorConverter = (operator: string): string => { - switch (operator) { - case Constants.Operator.Equal: - return Constants.ODataOperator.EqualTo; - case Constants.Operator.GreaterThan: - return Constants.ODataOperator.GreaterThan; - case Constants.Operator.GreaterThanOrEqualTo: - return Constants.ODataOperator.GreaterThanOrEqualTo; - case Constants.Operator.LessThan: - return Constants.ODataOperator.LessThan; - case Constants.Operator.LessThanOrEqualTo: - return Constants.ODataOperator.LessThanOrEqualTo; - case Constants.Operator.NotEqualTo: - return Constants.ODataOperator.NotEqualTo; - } - return null; - }; - - public groupClauses = (): void => { - this.queryClauses.groupSelectedItems(); - this.updateClauseArray(); - this.updateCanGroupClauses(); - }; - - public addClauseIndex = (index: number, data: any): void => { - if (index < 0) { - index = 0; - } - var newClause = new QueryClauseViewModel( - this, - "And", - "", - this.edmTypes()[0], - Constants.Operator.EqualTo, - "", - true, - "", - "", - "", - //null, - true - ); - this.addClauseImpl(newClause, index); - if (index === this.clauseArray().length - 1) { - this.scrollToBottom(); - } - this.updateCanGroupClauses(); - newClause.isAndOrFocused(true); - $(window).resize(); - }; - - // adds a new clause to the end of the array - public addNewClause = (): void => { - this.addClauseIndex(this.clauseArray().length, null); - }; - - public onAddClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.addClauseIndex(index, data); - event.stopPropagation(); - return false; - } - return true; - }; - - public onAddNewClauseKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.addClauseIndex(this.clauseArray().length - 1, null); - event.stopPropagation(); - return false; - } - return true; - }; - - public deleteClause = (index: number, data: any): void => { - this.deleteClauseImpl(index); - if (this.clauseArray().length !== 0) { - this.clauseArray()[0].and_or(""); - this.clauseArray()[0].canAnd(false); - } - this.updateCanGroupClauses(); - $(window).resize(); - }; - - public onDeleteClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.deleteClause(index, data); - event.stopPropagation(); - return false; - } - return true; - }; - - /** - * Generates an array of ClauseGroupViewModel objects for UI to display group information for this clause. - * All clauses have the same number of ClauseGroupViewModel objects, which is the depth of the clause tree. - * If the current clause is not the deepest in the tree, then the array will be filled by either a placeholder - * (transparent) or its parent group view models. - */ - public getClauseGroupViewModels = (clause: QueryClauseViewModel): ClauseGroupViewModel[] => { - var placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this); - var treeDepth = this.queryClauses.getTreeDepth(); - var groupViewModels = new Array(treeDepth); - - // Prefill the arry with placeholders. - for (var i = 0; i < groupViewModels.length; i++) { - groupViewModels[i] = placeHolderGroupViewModel; - } - - var currentGroup = clause.clauseGroup; - - // This function determines whether the path from clause to the current group is on the left most. - var isLeftMostPath = (): boolean => { - var group = clause.clauseGroup; - - if (group.children.indexOf(clause) !== 0) { - return false; - } - - while (true) { - if (group.getId() === currentGroup.getId()) { - break; - } - - if (group.parentGroup.children.indexOf(group) !== 0) { - return false; - } - - group = group.parentGroup; - } - return true; - }; - - // This function determines whether the path from clause to the current group is on the right most. - var isRightMostPath = (): boolean => { - var group = clause.clauseGroup; - - if (group.children.indexOf(clause) !== group.children.length - 1) { - return false; - } - - while (true) { - if (group.getId() === currentGroup.getId()) { - break; - } - - if (group.parentGroup.children.indexOf(group) !== group.parentGroup.children.length - 1) { - return false; - } - - group = group.parentGroup; - } - return true; - }; - - var vmIndex = groupViewModels.length - 1; - var skipIndex = -1; - var lastDepth = clause.groupDepth; - - while (!currentGroup.isRootGroup) { - // The current group will be rendered at least once, and if there are any sibling groups deeper - // than the current group, we will repeat rendering the current group to fill up the gap between - // current & deepest sibling. - var deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth(); - // Find out the depth difference between the deepest group under the siblings of currentGroup and - // the deepest group under currentGroup. If the result n is a positive number, it means there are - // deeper groups in siblings and we need to draw n + 1 group blocks on UI to fill up the depth - // differences. If the result n is a negative number, it means current group contains the deepest - // sub-group, we only need to draw the group block once. - var repeatCount = Math.max(deepestInSiblings - lastDepth, 0); - - for (var i = 0; i <= repeatCount; i++) { - var isLeftMost = isLeftMostPath(); - var isRightMost = isRightMostPath(); - var groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this); - - groupViewModel.showTopBorder(isLeftMost); - groupViewModel.showBottomBorder(isRightMost); - groupViewModel.showLeftBorder(i === repeatCount); - groupViewModels[vmIndex] = groupViewModel; - vmIndex--; - } - - skipIndex = currentGroup.parentGroup.children.indexOf(currentGroup); - currentGroup = currentGroup.parentGroup; - lastDepth = Math.max(deepestInSiblings, lastDepth); - } - - return groupViewModels; - }; - - public runQuery = (): DataTables.DataTable => { - return this._queryViewModel.runQuery(); - }; - - public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void { - var index = this.clauseArray.peek().indexOf(clauseToAdd); - - var newClause = new QueryClauseViewModel( - this, - //this._tableEntityListViewModel.tableExplorerContext.hostProxy, - "And", - clauseToAdd.field(), - "DateTime", - Constants.Operator.LessThan, - "", - true, - Constants.timeOptions.custom, - timestamp.endTime, - "range", - //null, - true - ); - - newClause.isLocal = ko.observable(timestamp.timeZone === "local"); - this.addClauseImpl(newClause, index + 1); - - if (index + 1 === this.clauseArray().length - 1) { - this.scrollToBottom(); - } - } - - private scrollToBottom(): void { - var scrollBox = document.getElementById("scroll"); - if (!this.scrollEventListener) { - scrollBox.addEventListener("scroll", function() { - var translate = "translate(0," + this.scrollTop + "px)"; - const allTh = >this.querySelectorAll("thead td"); - for (let i = 0; i < allTh.length; i++) { - allTh[i].style.transform = translate; - } - }); - this.scrollEventListener = true; - } - var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1; - if (isScrolledToBottom) { - scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight; - } - } - - private addClauseImpl(clause: QueryClauseViewModel, position: number): void { - this.queryClauses.insertClauseBefore(clause, this.clauseArray()[position]); - this.updateClauseArray(); - } - - private deleteClauseImpl(index: number): void { - var clause = this.clauseArray()[index]; - var previousClause = index === 0 ? 0 : index - 1; - this.queryClauses.deleteClause(clause); - this.updateClauseArray(); - if (this.clauseArray()[previousClause]) { - this.clauseArray()[previousClause].isDeleteButtonFocused(true); - } - } - - public updateCanGroupClauses(): void { - this.canGroupClauses(this.queryClauses.canGroupSelectedItems()); - } - - public updateClauseArray(): void { - if (this.clauseArray().length > 0) { - this.clauseArray()[0].canAnd(true); - } - - this.queryClauses.flattenClauses(this.clauseArray); - - if (this.clauseArray().length > 0) { - this.clauseArray()[0].canAnd(false); - } - - // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size. - //DataTableUtilities.forceRecalculateTableSize(); - } - - private timestampToValue(clause: QueryClauseViewModel): void { - if (clause.isValue()) { - return; - } else if (clause.isTimestamp()) { - this.getTimeStampToQuery(clause); - // } else if (clause.isCustomLastTimestamp()) { - // clause.value(`datetime'${CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)}'`); - } else if (clause.isCustomRangeTimestamp()) { - if (clause.isLocal()) { - clause.value(`datetime'${DateTimeUtilities.getUTCDateTime(clause.customTimeValue())}'`); - } else { - clause.value(`datetime'${clause.customTimeValue()}Z'`); - } - } - } - - private timestampToSqlValue(clause: QueryClauseViewModel): string { - if (clause.isValue()) { - return null; - } else if (clause.isTimestamp()) { - return this.getTimeStampToSqlQuery(clause); - // } else if (clause.isCustomLastTimestamp()) { - // clause.value(CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)); - } else if (clause.isCustomRangeTimestamp()) { - if (clause.isLocal()) { - return DateTimeUtilities.getUTCDateTime(clause.customTimeValue()); - } else { - return clause.customTimeValue(); - } - } - return null; - } - - private getTimeStampToQuery(clause: QueryClauseViewModel): void { - switch (clause.timeValue()) { - case Constants.timeOptions.lastHour: - clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 1)}'`); - break; - case Constants.timeOptions.last24Hours: - clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 24)}'`); - break; - case Constants.timeOptions.last7Days: - clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(7, 0)}'`); - break; - case Constants.timeOptions.last31Days: - clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(31, 0)}'`); - break; - case Constants.timeOptions.last365Days: - clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(365, 0)}'`); - break; - case Constants.timeOptions.currentMonth: - clause.value(`datetime'${CustomTimestampHelper._queryCurrentMonthLocal()}'`); - break; - case Constants.timeOptions.currentYear: - clause.value(`datetime'${CustomTimestampHelper._queryCurrentYearLocal()}'`); - break; - } - } - - private getTimeStampToSqlQuery(clause: QueryClauseViewModel): string { - switch (clause.timeValue()) { - case Constants.timeOptions.lastHour: - return CustomTimestampHelper._queryLastDaysHours(0, 1); - case Constants.timeOptions.last24Hours: - return CustomTimestampHelper._queryLastDaysHours(0, 24); - case Constants.timeOptions.last7Days: - return CustomTimestampHelper._queryLastDaysHours(7, 0); - case Constants.timeOptions.last31Days: - return CustomTimestampHelper._queryLastDaysHours(31, 0); - case Constants.timeOptions.last365Days: - return CustomTimestampHelper._queryLastDaysHours(365, 0); - case Constants.timeOptions.currentMonth: - return CustomTimestampHelper._queryCurrentMonthLocal(); - case Constants.timeOptions.currentYear: - return CustomTimestampHelper._queryCurrentYearLocal(); - } - return null; - } - - public checkIfClauseChanged(clause: QueryClauseViewModel): void { - this._queryViewModel.checkIfBuilderChanged(clause); - } -} +import * as ko from "knockout"; +import * as CustomTimestampHelper from "./CustomTimestampHelper"; +import { getQuotedCqlIdentifier } from "../CqlUtilities"; +import QueryClauseViewModel from "./QueryClauseViewModel"; +import ClauseGroup from "./ClauseGroup"; +import ClauseGroupViewModel from "./ClauseGroupViewModel"; +import QueryViewModel from "./QueryViewModel"; +import * as Constants from "../Constants"; +import TableEntityListViewModel from "../DataTable/TableEntityListViewModel"; +import * as DateTimeUtilities from "./DateTimeUtilities"; +import * as DataTableUtilities from "../DataTable/DataTableUtilities"; +import * as TableEntityProcessor from "../TableEntityProcessor"; +import * as Utilities from "../Utilities"; +import { KeyCodes } from "../../../Common/Constants"; + +export default class QueryBuilderViewModel { + /* Labels */ + public andLabel = "And/Or"; // localize + public actionLabel = "Action"; // localize + public fieldLabel = "Field"; // localize + public dataTypeLabel = "Type"; // localize + public operatorLabel = "Operator"; // localize + public valueLabel = "Value"; // localize + + /* controls */ + public addNewClauseLine = "Add new clause"; // localize + public insertNewFilterLine = "Insert new filter line"; // localize + public removeThisFilterLine = "Remove this filter line"; // localize + public groupSelectedClauses = "Group selected clauses"; // localize + public clauseArray = ko.observableArray(); // This is for storing the clauses in flattened form queryClauses for easier UI data binding. + public queryClauses = new ClauseGroup(true, null); // The actual data structure containing the clause information. + public columnOptions: ko.ObservableArray; + public canGroupClauses = ko.observable(false); + + /* Observables */ + public edmTypes = ko.observableArray([ + Constants.TableType.String, + Constants.TableType.Boolean, + Constants.TableType.Binary, + Constants.TableType.DateTime, + Constants.TableType.Double, + Constants.TableType.Guid, + Constants.TableType.Int32, + Constants.TableType.Int64, + "", + ]); + public operators = ko.observableArray([ + Constants.Operator.Equal, + Constants.Operator.GreaterThan, + Constants.Operator.GreaterThanOrEqualTo, + Constants.Operator.LessThan, + Constants.Operator.LessThanOrEqualTo, + Constants.Operator.NotEqualTo, + "", + ]); + public clauseRules = ko.observableArray([Constants.ClauseRule.And, Constants.ClauseRule.Or]); + public timeOptions = ko.observableArray([ + Constants.timeOptions.lastHour, + Constants.timeOptions.last24Hours, + Constants.timeOptions.last7Days, + Constants.timeOptions.last31Days, + Constants.timeOptions.last365Days, + Constants.timeOptions.currentMonth, + Constants.timeOptions.currentYear, + //Constants.timeOptions.custom + ]); + public queryString = ko.observable(); + private _queryViewModel: QueryViewModel; + public tableEntityListViewModel: TableEntityListViewModel; + private scrollEventListener: boolean; + + constructor(queryViewModel: QueryViewModel, tableEntityListViewModel: TableEntityListViewModel) { + if (tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) { + this.edmTypes([ + Constants.CassandraType.Text, + Constants.CassandraType.Ascii, + Constants.CassandraType.Bigint, + Constants.CassandraType.Blob, + Constants.CassandraType.Boolean, + Constants.CassandraType.Decimal, + Constants.CassandraType.Double, + Constants.CassandraType.Float, + Constants.CassandraType.Int, + Constants.CassandraType.Uuid, + Constants.CassandraType.Varchar, + Constants.CassandraType.Varint, + Constants.CassandraType.Inet, + Constants.CassandraType.Smallint, + Constants.CassandraType.Tinyint, + ]); + this.clauseRules([ + Constants.ClauseRule.And, + // OR is not supported in CQL + ]); + this.andLabel = "And"; + } + this.clauseArray(); + + this._queryViewModel = queryViewModel; + this.tableEntityListViewModel = tableEntityListViewModel; + this.columnOptions = ko.observableArray(queryViewModel.columnOptions()); + + this.columnOptions.subscribe((newColumnOptions) => { + queryViewModel.columnOptions(newColumnOptions); + }); + } + + public setExample() { + var example1 = new QueryClauseViewModel( + this, + "", + "PartitionKey", + this.edmTypes()[0], + Constants.Operator.Equal, + this.tableEntityListViewModel.items()[0].PartitionKey._, + false, + "", + "", + "", + //null, + true + ); + var example2 = new QueryClauseViewModel( + this, + "And", + "RowKey", + this.edmTypes()[0], + Constants.Operator.Equal, + this.tableEntityListViewModel.items()[0].RowKey._, + true, + "", + "", + "", + //null, + true + ); + this.addClauseImpl(example1, 0); + this.addClauseImpl(example2, 1); + } + + public getODataFilterFromClauses = (): string => { + var filterString: string = ""; + var treeTraversal = (group: ClauseGroup): void => { + for (var i = 0; i < group.children.length; i++) { + var currentItem = group.children[i]; + + if (currentItem instanceof QueryClauseViewModel) { + var clause = currentItem; + this.timestampToValue(clause); + filterString = filterString.concat( + this.constructODataClause( + filterString === "" ? "" : clause.and_or(), + this.generateLeftParentheses(clause), + clause.field(), + clause.type(), + clause.operator(), + clause.value(), + this.generateRightParentheses(clause) + ) + ); + } + + if (currentItem instanceof ClauseGroup) { + treeTraversal(currentItem); + } + } + }; + + treeTraversal(this.queryClauses); + + return filterString.trim(); + }; + + public getSqlFilterFromClauses = (): string => { + var filterString: string = "SELECT * FROM c"; + if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) { + filterString = "SELECT"; + const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText(); + selectText && + selectText.forEach((value: string) => { + if (value === Constants.EntityKeyNames.PartitionKey) { + value = `["${TableEntityProcessor.keyProperties.PartitionKey}"]`; + filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c"); + } else if (value === Constants.EntityKeyNames.RowKey) { + value = `["${TableEntityProcessor.keyProperties.Id2}"]`; + filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c"); + } else { + if (value === Constants.EntityKeyNames.Timestamp) { + value = TableEntityProcessor.keyProperties.Timestamp; + } + filterString = filterString.concat(filterString === "SELECT" ? " c." : ", c."); + } + filterString = filterString.concat(value); + }); + filterString = filterString.concat(" FROM c"); + } + if (this.queryClauses.children.length === 0) { + return filterString; + } + filterString = filterString.concat(" WHERE"); + var first = true; + var treeTraversal = (group: ClauseGroup): void => { + for (var i = 0; i < group.children.length; i++) { + var currentItem = group.children[i]; + + if (currentItem instanceof QueryClauseViewModel) { + var clause = currentItem; + let timeStampValue: string = this.timestampToSqlValue(clause); + var value = clause.value(); + if (!clause.isValue()) { + value = timeStampValue; + } + filterString = filterString.concat( + this.constructSqlClause( + first ? "" : clause.and_or(), + this.generateLeftParentheses(clause), + clause.field(), + clause.type(), + clause.operator(), + value, + this.generateRightParentheses(clause) + ) + ); + first = false; + } + + if (currentItem instanceof ClauseGroup) { + treeTraversal(currentItem); + } + } + }; + + treeTraversal(this.queryClauses); + + return filterString.trim(); + }; + + public getCqlFilterFromClauses = (): string => { + const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId; + const collectionId = this._queryViewModel.queryTablesTab.collection.id(); + const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`; + var filterString: string = `SELECT * FROM ${tableToQuery}`; + if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) { + filterString = "SELECT"; + const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText(); + selectText && + selectText.forEach((value: string) => { + filterString = filterString.concat(filterString === "SELECT" ? " " : ", "); + filterString = filterString.concat(value); + }); + filterString = filterString.concat(` FROM ${tableToQuery}`); + } + if (this.queryClauses.children.length === 0) { + return filterString; + } + filterString = filterString.concat(" WHERE"); + var first = true; + var treeTraversal = (group: ClauseGroup): void => { + for (var i = 0; i < group.children.length; i++) { + var currentItem = group.children[i]; + + if (currentItem instanceof QueryClauseViewModel) { + var clause = currentItem; + let timeStampValue: string = this.timestampToSqlValue(clause); + var value = clause.value(); + if (!clause.isValue()) { + value = timeStampValue; + } + filterString = filterString.concat( + this.constructCqlClause( + first ? "" : clause.and_or(), + this.generateLeftParentheses(clause), + clause.field(), + clause.type(), + clause.operator(), + value, + this.generateRightParentheses(clause) + ) + ); + first = false; + } + + if (currentItem instanceof ClauseGroup) { + treeTraversal(currentItem); + } + } + }; + + treeTraversal(this.queryClauses); + + return filterString.trim(); + }; + + public updateColumnOptions = (): void => { + let originalHeaders = this.columnOptions(); + let newHeaders = this.tableEntityListViewModel.headers; + this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns)); + }; + + private generateLeftParentheses(clause: QueryClauseViewModel): string { + var result = ""; + + if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) { + return result; + } else { + result = result.concat("("); + } + + var currentGroup: ClauseGroup = clause.clauseGroup; + + while ( + !currentGroup.isRootGroup && + !currentGroup.parentGroup.isRootGroup && + currentGroup.parentGroup.children.indexOf(currentGroup) === 0 + ) { + result = result.concat("("); + currentGroup = currentGroup.parentGroup; + } + + return result; + } + + private generateRightParentheses(clause: QueryClauseViewModel): string { + var result = ""; + + if ( + clause.clauseGroup.isRootGroup || + clause.clauseGroup.children.indexOf(clause) !== clause.clauseGroup.children.length - 1 + ) { + return result; + } else { + result = result.concat(")"); + } + + var currentGroup: ClauseGroup = clause.clauseGroup; + + while ( + !currentGroup.isRootGroup && + !currentGroup.parentGroup.isRootGroup && + currentGroup.parentGroup.children.indexOf(currentGroup) === currentGroup.parentGroup.children.length - 1 + ) { + result = result.concat(")"); + currentGroup = currentGroup.parentGroup; + } + + return result; + } + + private constructODataClause = ( + clauseRule: string, + leftParentheses: string, + propertyName: string, + type: string, + operator: string, + value: string, + rightParentheses: string + ): string => { + switch (type) { + case Constants.TableType.DateTime: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( + operator + )} ${value}${rightParentheses}`; + case Constants.TableType.String: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( + operator + )} \'${value}\'${rightParentheses}`; + case Constants.TableType.Guid: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( + operator + )} guid\'${value}\'${rightParentheses}`; + case Constants.TableType.Binary: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( + operator + )} binary\'${value}\'${rightParentheses}`; + default: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( + operator + )} ${value}${rightParentheses}`; + } + }; + + private constructSqlClause = ( + clauseRule: string, + leftParentheses: string, + propertyName: string, + type: string, + operator: string, + value: string, + rightParentheses: string + ): string => { + if (propertyName === Constants.EntityKeyNames.PartitionKey) { + propertyName = TableEntityProcessor.keyProperties.PartitionKey; + return ` ${clauseRule.toLowerCase()} ${leftParentheses}c["${propertyName}"] ${operator} \'${value}\'${rightParentheses}`; + } else if (propertyName === Constants.EntityKeyNames.RowKey) { + propertyName = TableEntityProcessor.keyProperties.Id; + return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} \'${value}\'${rightParentheses}`; + } else if (propertyName === Constants.EntityKeyNames.Timestamp) { + propertyName = TableEntityProcessor.keyProperties.Timestamp; + return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} ${DateTimeUtilities.convertJSDateToUnix( + value + )}${rightParentheses}`; + } + switch (type) { + case Constants.TableType.DateTime: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${DateTimeUtilities.convertJSDateToTicksWithPadding( + value + )}\'${rightParentheses}`; + case Constants.TableType.Int64: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${Utilities.padLongWithZeros( + value + )}\'${rightParentheses}`; + case Constants.TableType.String: + case Constants.TableType.Guid: + case Constants.TableType.Binary: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${value}\'${rightParentheses}`; + default: + return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} ${value}${rightParentheses}`; + } + }; + + private constructCqlClause = ( + clauseRule: string, + leftParentheses: string, + propertyName: string, + type: string, + operator: string, + value: string, + rightParentheses: string + ): string => { + if ( + type === Constants.CassandraType.Text || + type === Constants.CassandraType.Inet || + type === Constants.CassandraType.Ascii || + type === Constants.CassandraType.Varchar + ) { + return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} \'${value}\'${rightParentheses}`; + } + return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} ${value}${rightParentheses}`; + }; + + private operatorConverter = (operator: string): string => { + switch (operator) { + case Constants.Operator.Equal: + return Constants.ODataOperator.EqualTo; + case Constants.Operator.GreaterThan: + return Constants.ODataOperator.GreaterThan; + case Constants.Operator.GreaterThanOrEqualTo: + return Constants.ODataOperator.GreaterThanOrEqualTo; + case Constants.Operator.LessThan: + return Constants.ODataOperator.LessThan; + case Constants.Operator.LessThanOrEqualTo: + return Constants.ODataOperator.LessThanOrEqualTo; + case Constants.Operator.NotEqualTo: + return Constants.ODataOperator.NotEqualTo; + } + return null; + }; + + public groupClauses = (): void => { + this.queryClauses.groupSelectedItems(); + this.updateClauseArray(); + this.updateCanGroupClauses(); + }; + + public addClauseIndex = (index: number, data: any): void => { + if (index < 0) { + index = 0; + } + var newClause = new QueryClauseViewModel( + this, + "And", + "", + this.edmTypes()[0], + Constants.Operator.EqualTo, + "", + true, + "", + "", + "", + //null, + true + ); + this.addClauseImpl(newClause, index); + if (index === this.clauseArray().length - 1) { + this.scrollToBottom(); + } + this.updateCanGroupClauses(); + newClause.isAndOrFocused(true); + $(window).resize(); + }; + + // adds a new clause to the end of the array + public addNewClause = (): void => { + this.addClauseIndex(this.clauseArray().length, null); + }; + + public onAddClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.addClauseIndex(index, data); + event.stopPropagation(); + return false; + } + return true; + }; + + public onAddNewClauseKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.addClauseIndex(this.clauseArray().length - 1, null); + event.stopPropagation(); + return false; + } + return true; + }; + + public deleteClause = (index: number, data: any): void => { + this.deleteClauseImpl(index); + if (this.clauseArray().length !== 0) { + this.clauseArray()[0].and_or(""); + this.clauseArray()[0].canAnd(false); + } + this.updateCanGroupClauses(); + $(window).resize(); + }; + + public onDeleteClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.deleteClause(index, data); + event.stopPropagation(); + return false; + } + return true; + }; + + /** + * Generates an array of ClauseGroupViewModel objects for UI to display group information for this clause. + * All clauses have the same number of ClauseGroupViewModel objects, which is the depth of the clause tree. + * If the current clause is not the deepest in the tree, then the array will be filled by either a placeholder + * (transparent) or its parent group view models. + */ + public getClauseGroupViewModels = (clause: QueryClauseViewModel): ClauseGroupViewModel[] => { + var placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this); + var treeDepth = this.queryClauses.getTreeDepth(); + var groupViewModels = new Array(treeDepth); + + // Prefill the arry with placeholders. + for (var i = 0; i < groupViewModels.length; i++) { + groupViewModels[i] = placeHolderGroupViewModel; + } + + var currentGroup = clause.clauseGroup; + + // This function determines whether the path from clause to the current group is on the left most. + var isLeftMostPath = (): boolean => { + var group = clause.clauseGroup; + + if (group.children.indexOf(clause) !== 0) { + return false; + } + + while (true) { + if (group.getId() === currentGroup.getId()) { + break; + } + + if (group.parentGroup.children.indexOf(group) !== 0) { + return false; + } + + group = group.parentGroup; + } + return true; + }; + + // This function determines whether the path from clause to the current group is on the right most. + var isRightMostPath = (): boolean => { + var group = clause.clauseGroup; + + if (group.children.indexOf(clause) !== group.children.length - 1) { + return false; + } + + while (true) { + if (group.getId() === currentGroup.getId()) { + break; + } + + if (group.parentGroup.children.indexOf(group) !== group.parentGroup.children.length - 1) { + return false; + } + + group = group.parentGroup; + } + return true; + }; + + var vmIndex = groupViewModels.length - 1; + var skipIndex = -1; + var lastDepth = clause.groupDepth; + + while (!currentGroup.isRootGroup) { + // The current group will be rendered at least once, and if there are any sibling groups deeper + // than the current group, we will repeat rendering the current group to fill up the gap between + // current & deepest sibling. + var deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth(); + // Find out the depth difference between the deepest group under the siblings of currentGroup and + // the deepest group under currentGroup. If the result n is a positive number, it means there are + // deeper groups in siblings and we need to draw n + 1 group blocks on UI to fill up the depth + // differences. If the result n is a negative number, it means current group contains the deepest + // sub-group, we only need to draw the group block once. + var repeatCount = Math.max(deepestInSiblings - lastDepth, 0); + + for (var i = 0; i <= repeatCount; i++) { + var isLeftMost = isLeftMostPath(); + var isRightMost = isRightMostPath(); + var groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this); + + groupViewModel.showTopBorder(isLeftMost); + groupViewModel.showBottomBorder(isRightMost); + groupViewModel.showLeftBorder(i === repeatCount); + groupViewModels[vmIndex] = groupViewModel; + vmIndex--; + } + + skipIndex = currentGroup.parentGroup.children.indexOf(currentGroup); + currentGroup = currentGroup.parentGroup; + lastDepth = Math.max(deepestInSiblings, lastDepth); + } + + return groupViewModels; + }; + + public runQuery = (): DataTables.DataTable => { + return this._queryViewModel.runQuery(); + }; + + public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void { + var index = this.clauseArray.peek().indexOf(clauseToAdd); + + var newClause = new QueryClauseViewModel( + this, + //this._tableEntityListViewModel.tableExplorerContext.hostProxy, + "And", + clauseToAdd.field(), + "DateTime", + Constants.Operator.LessThan, + "", + true, + Constants.timeOptions.custom, + timestamp.endTime, + "range", + //null, + true + ); + + newClause.isLocal = ko.observable(timestamp.timeZone === "local"); + this.addClauseImpl(newClause, index + 1); + + if (index + 1 === this.clauseArray().length - 1) { + this.scrollToBottom(); + } + } + + private scrollToBottom(): void { + var scrollBox = document.getElementById("scroll"); + if (!this.scrollEventListener) { + scrollBox.addEventListener("scroll", function () { + var translate = "translate(0," + this.scrollTop + "px)"; + const allTh = >this.querySelectorAll("thead td"); + for (let i = 0; i < allTh.length; i++) { + allTh[i].style.transform = translate; + } + }); + this.scrollEventListener = true; + } + var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1; + if (isScrolledToBottom) { + scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight; + } + } + + private addClauseImpl(clause: QueryClauseViewModel, position: number): void { + this.queryClauses.insertClauseBefore(clause, this.clauseArray()[position]); + this.updateClauseArray(); + } + + private deleteClauseImpl(index: number): void { + var clause = this.clauseArray()[index]; + var previousClause = index === 0 ? 0 : index - 1; + this.queryClauses.deleteClause(clause); + this.updateClauseArray(); + if (this.clauseArray()[previousClause]) { + this.clauseArray()[previousClause].isDeleteButtonFocused(true); + } + } + + public updateCanGroupClauses(): void { + this.canGroupClauses(this.queryClauses.canGroupSelectedItems()); + } + + public updateClauseArray(): void { + if (this.clauseArray().length > 0) { + this.clauseArray()[0].canAnd(true); + } + + this.queryClauses.flattenClauses(this.clauseArray); + + if (this.clauseArray().length > 0) { + this.clauseArray()[0].canAnd(false); + } + + // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size. + //DataTableUtilities.forceRecalculateTableSize(); + } + + private timestampToValue(clause: QueryClauseViewModel): void { + if (clause.isValue()) { + return; + } else if (clause.isTimestamp()) { + this.getTimeStampToQuery(clause); + // } else if (clause.isCustomLastTimestamp()) { + // clause.value(`datetime'${CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)}'`); + } else if (clause.isCustomRangeTimestamp()) { + if (clause.isLocal()) { + clause.value(`datetime'${DateTimeUtilities.getUTCDateTime(clause.customTimeValue())}'`); + } else { + clause.value(`datetime'${clause.customTimeValue()}Z'`); + } + } + } + + private timestampToSqlValue(clause: QueryClauseViewModel): string { + if (clause.isValue()) { + return null; + } else if (clause.isTimestamp()) { + return this.getTimeStampToSqlQuery(clause); + // } else if (clause.isCustomLastTimestamp()) { + // clause.value(CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)); + } else if (clause.isCustomRangeTimestamp()) { + if (clause.isLocal()) { + return DateTimeUtilities.getUTCDateTime(clause.customTimeValue()); + } else { + return clause.customTimeValue(); + } + } + return null; + } + + private getTimeStampToQuery(clause: QueryClauseViewModel): void { + switch (clause.timeValue()) { + case Constants.timeOptions.lastHour: + clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 1)}'`); + break; + case Constants.timeOptions.last24Hours: + clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 24)}'`); + break; + case Constants.timeOptions.last7Days: + clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(7, 0)}'`); + break; + case Constants.timeOptions.last31Days: + clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(31, 0)}'`); + break; + case Constants.timeOptions.last365Days: + clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(365, 0)}'`); + break; + case Constants.timeOptions.currentMonth: + clause.value(`datetime'${CustomTimestampHelper._queryCurrentMonthLocal()}'`); + break; + case Constants.timeOptions.currentYear: + clause.value(`datetime'${CustomTimestampHelper._queryCurrentYearLocal()}'`); + break; + } + } + + private getTimeStampToSqlQuery(clause: QueryClauseViewModel): string { + switch (clause.timeValue()) { + case Constants.timeOptions.lastHour: + return CustomTimestampHelper._queryLastDaysHours(0, 1); + case Constants.timeOptions.last24Hours: + return CustomTimestampHelper._queryLastDaysHours(0, 24); + case Constants.timeOptions.last7Days: + return CustomTimestampHelper._queryLastDaysHours(7, 0); + case Constants.timeOptions.last31Days: + return CustomTimestampHelper._queryLastDaysHours(31, 0); + case Constants.timeOptions.last365Days: + return CustomTimestampHelper._queryLastDaysHours(365, 0); + case Constants.timeOptions.currentMonth: + return CustomTimestampHelper._queryCurrentMonthLocal(); + case Constants.timeOptions.currentYear: + return CustomTimestampHelper._queryCurrentYearLocal(); + } + return null; + } + + public checkIfClauseChanged(clause: QueryClauseViewModel): void { + this._queryViewModel.checkIfBuilderChanged(clause); + } +} diff --git a/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts b/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts index 277ae019f..fc1055b7d 100644 --- a/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts +++ b/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts @@ -1,285 +1,285 @@ -import * as ko from "knockout"; -import _ from "underscore"; -import * as QueryBuilderConstants from "../Constants"; -import QueryBuilderViewModel from "./QueryBuilderViewModel"; -import ClauseGroup from "./ClauseGroup"; -import * as Utilities from "../Utilities"; - -export default class QueryClauseViewModel { - public checkedForGrouping: ko.Observable; - public isFirstInGroup: ko.Observable; - public clauseGroup: ClauseGroup; - public and_or: ko.Observable; - public field: ko.Observable; - public type: ko.Observable; - public operator: ko.Observable; - public value: ko.Observable; - public timeValue: ko.Observable; - public customTimeValue: ko.Observable; - public canAnd: ko.Observable; - public timestampType: ko.Observable; - //public customLastTimestamp: ko.Observable; - public isLocal: ko.Observable; - public isOperaterEditable: ko.PureComputed; - public isTypeEditable: ko.PureComputed; - public isValue: ko.Observable; - public isTimestamp: ko.Observable; - public isCustomLastTimestamp: ko.Observable; - public isCustomRangeTimestamp: ko.Observable; - private _queryBuilderViewModel: QueryBuilderViewModel; - private _groupCheckSubscription: ko.Subscription; - private _id: string; - public isAndOrFocused: ko.Observable; - public isDeleteButtonFocused: ko.Observable; - - constructor( - queryBuilderViewModel: QueryBuilderViewModel, - and_or: string, - field: string, - type: string, - operator: string, - value: any, - canAnd: boolean, - timeValue: string, - customTimeValue: string, - timestampType: string, - //customLastTimestamp: CustomTimestampHelper.ILastQuery, - isLocal: boolean, - id?: string - ) { - this._queryBuilderViewModel = queryBuilderViewModel; - this.checkedForGrouping = ko.observable(false); - this.isFirstInGroup = ko.observable(false); - this.and_or = ko.observable(and_or); - this.field = ko.observable(field); - this.type = ko.observable(type); - this.operator = ko.observable(operator); - this.value = ko.observable(value); - this.timeValue = ko.observable(timeValue); - this.customTimeValue = ko.observable(customTimeValue); - this.canAnd = ko.observable(canAnd); - this.isLocal = ko.observable(isLocal); - this._id = id ? id : Utilities.guid(); - - //this.customLastTimestamp = ko.observable(customLastTimestamp); - //this.setCustomLastTimestamp(); - - this.timestampType = ko.observable(timestampType); - this.getValueType(); - - this.isOperaterEditable = ko.pureComputed(() => { - const isPreferredApiCassandra = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra(); - const cassandraKeys = isPreferredApiCassandra - ? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map( - key => key.property - ) - : []; - return ( - (this.isValue() || this.isCustomRangeTimestamp()) && - (!isPreferredApiCassandra || !_.contains(cassandraKeys, this.field())) - ); - }); - this.isTypeEditable = ko.pureComputed( - () => - this.field() !== "Timestamp" && - this.field() !== "PartitionKey" && - this.field() !== "RowKey" && - !this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra() - ); - - this.and_or.subscribe(value => { - this._queryBuilderViewModel.checkIfClauseChanged(this); - }); - this.field.subscribe(value => { - this.changeField(); - }); - this.type.subscribe(value => { - this.changeType(); - }); - this.timeValue.subscribe(value => { - // if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) { - // this.customTimestampDialog(); - // } - }); - this.customTimeValue.subscribe(value => { - this._queryBuilderViewModel.checkIfClauseChanged(this); - }); - this.value.subscribe(value => { - this._queryBuilderViewModel.checkIfClauseChanged(this); - }); - this.operator.subscribe(value => { - this._queryBuilderViewModel.checkIfClauseChanged(this); - }); - this._groupCheckSubscription = this.checkedForGrouping.subscribe(value => { - this._queryBuilderViewModel.updateCanGroupClauses(); - }); - this.isAndOrFocused = ko.observable(false); - this.isDeleteButtonFocused = ko.observable(false); - } - - // private setCustomLastTimestamp() : void { - // if (this.customLastTimestamp() === null) { - // var lastNumberandType: CustomTimestampHelper.ILastQuery = { - // lastNumber: 7, - // lastTimeUnit: "Days" - // }; - // this.customLastTimestamp(lastNumberandType); - // } - // } - - private getValueType(): void { - switch (this.timestampType()) { - case "time": - this.isValue = ko.observable(false); - this.isTimestamp = ko.observable(true); - this.isCustomLastTimestamp = ko.observable(false); - this.isCustomRangeTimestamp = ko.observable(false); - break; - case "last": - this.isValue = ko.observable(false); - this.isTimestamp = ko.observable(false); - this.isCustomLastTimestamp = ko.observable(true); - this.isCustomRangeTimestamp = ko.observable(false); - break; - case "range": - this.isValue = ko.observable(false); - this.isTimestamp = ko.observable(false); - this.isCustomLastTimestamp = ko.observable(false); - this.isCustomRangeTimestamp = ko.observable(true); - break; - default: - this.isValue = ko.observable(true); - this.isTimestamp = ko.observable(false); - this.isCustomLastTimestamp = ko.observable(false); - this.isCustomRangeTimestamp = ko.observable(false); - } - } - - private changeField(): void { - this.isCustomLastTimestamp(false); - this.isCustomRangeTimestamp(false); - - if (this.field() === "Timestamp") { - this.isValue(false); - this.isTimestamp(true); - this.type(QueryBuilderConstants.TableType.DateTime); - this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo); - this.timestampType("time"); - } else if (this.field() === "PartitionKey" || this.field() === "RowKey") { - this.resetFromTimestamp(); - this.type(QueryBuilderConstants.TableType.String); - } else { - this.resetFromTimestamp(); - if (this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) { - const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection - .cassandraSchema; - for (let i = 0, len = cassandraSchema.length; i < len; i++) { - if (cassandraSchema[i].property === this.field()) { - this.type(cassandraSchema[i].type); - i = len; - } - } - } else { - this.type(QueryBuilderConstants.TableType.String); - } - } - this._queryBuilderViewModel.checkIfClauseChanged(this); - } - - private resetFromTimestamp(): void { - this.isValue(true); - this.isTimestamp(false); - this.operator(QueryBuilderConstants.Operator.Equal); - this.value(""); - this.timestampType(""); - this.timeValue(""); - this.customTimeValue(""); - } - - private changeType(): void { - this.isCustomLastTimestamp(false); - this.isCustomRangeTimestamp(false); - - if (this.type() === QueryBuilderConstants.TableType.DateTime) { - this.isValue(false); - this.isTimestamp(true); - this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo); - this.timestampType("time"); - } else { - this.isValue(true); - this.isTimestamp(false); - this.timeValue(""); - this.operator(QueryBuilderConstants.Operator.EqualTo); - this.value(""); - this.timestampType(""); - this.timeValue(""); - this.customTimeValue(""); - } - this._queryBuilderViewModel.checkIfClauseChanged(this); - } - - // private customTimestampDialog(): Promise { - // var lastNumber = this.customLastTimestamp().lastNumber; - // var lastTimeUnit = this.customLastTimestamp().lastTimeUnit; - - // return this._host.executeOperation("Environment.openDialog", [{ - // id: AzureConstants.registeredDialogs.customTimestampQueryDialog, - // width: 500, - // height: 300, - // parameters: { lastNumber, lastTimeUnit } - // }]).then((timestamp: CustomTimestampHelper.ITimestampQuery) => { - // if (timestamp) { - // this.isValue(false); - // this.isTimestamp(false); - // this.timestampType(timestamp.queryType); - - // if (timestamp.queryType === "last") { - // this.isCustomLastTimestamp(true); - // this.isCustomRangeTimestamp(false); - - // var lastNumberandType: CustomTimestampHelper.ILastQuery = { - // lastNumber: timestamp.lastNumber, - // lastTimeUnit: timestamp.lastTimeUnit - // }; - - // this.customLastTimestamp(lastNumberandType); - // this.customTimeValue(`Last ${timestamp.lastNumber} ${timestamp.lastTimeUnit}`); - - // } else { - // if (timestamp.timeZone === "local") { - // this.isLocal = ko.observable(true); - // } else { - // this.isLocal = ko.observable(false); - // } - // this.isCustomLastTimestamp(false); - // this.isCustomRangeTimestamp(true); - // this.customTimeValue(timestamp.startTime); - // CustomTimestampHelper.addRangeTimestamp(timestamp, this._queryBuilderViewModel, this); - // } - // } else { - // this.timeValue(QueryBuilderConstants.timeOptions.lastHour); - // } - // }); - // } - - public getId(): string { - return this._id; - } - - public get groupDepth(): number { - if (this.clauseGroup) { - return this.clauseGroup.getCurrentGroupDepth(); - } - - return -1; - } - - public dispose(): void { - if (this._groupCheckSubscription) { - this._groupCheckSubscription.dispose(); - } - - this.clauseGroup = null; - this._queryBuilderViewModel = null; - } -} +import * as ko from "knockout"; +import _ from "underscore"; +import * as QueryBuilderConstants from "../Constants"; +import QueryBuilderViewModel from "./QueryBuilderViewModel"; +import ClauseGroup from "./ClauseGroup"; +import * as Utilities from "../Utilities"; + +export default class QueryClauseViewModel { + public checkedForGrouping: ko.Observable; + public isFirstInGroup: ko.Observable; + public clauseGroup: ClauseGroup; + public and_or: ko.Observable; + public field: ko.Observable; + public type: ko.Observable; + public operator: ko.Observable; + public value: ko.Observable; + public timeValue: ko.Observable; + public customTimeValue: ko.Observable; + public canAnd: ko.Observable; + public timestampType: ko.Observable; + //public customLastTimestamp: ko.Observable; + public isLocal: ko.Observable; + public isOperaterEditable: ko.PureComputed; + public isTypeEditable: ko.PureComputed; + public isValue: ko.Observable; + public isTimestamp: ko.Observable; + public isCustomLastTimestamp: ko.Observable; + public isCustomRangeTimestamp: ko.Observable; + private _queryBuilderViewModel: QueryBuilderViewModel; + private _groupCheckSubscription: ko.Subscription; + private _id: string; + public isAndOrFocused: ko.Observable; + public isDeleteButtonFocused: ko.Observable; + + constructor( + queryBuilderViewModel: QueryBuilderViewModel, + and_or: string, + field: string, + type: string, + operator: string, + value: any, + canAnd: boolean, + timeValue: string, + customTimeValue: string, + timestampType: string, + //customLastTimestamp: CustomTimestampHelper.ILastQuery, + isLocal: boolean, + id?: string + ) { + this._queryBuilderViewModel = queryBuilderViewModel; + this.checkedForGrouping = ko.observable(false); + this.isFirstInGroup = ko.observable(false); + this.and_or = ko.observable(and_or); + this.field = ko.observable(field); + this.type = ko.observable(type); + this.operator = ko.observable(operator); + this.value = ko.observable(value); + this.timeValue = ko.observable(timeValue); + this.customTimeValue = ko.observable(customTimeValue); + this.canAnd = ko.observable(canAnd); + this.isLocal = ko.observable(isLocal); + this._id = id ? id : Utilities.guid(); + + //this.customLastTimestamp = ko.observable(customLastTimestamp); + //this.setCustomLastTimestamp(); + + this.timestampType = ko.observable(timestampType); + this.getValueType(); + + this.isOperaterEditable = ko.pureComputed(() => { + const isPreferredApiCassandra = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra(); + const cassandraKeys = isPreferredApiCassandra + ? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map( + (key) => key.property + ) + : []; + return ( + (this.isValue() || this.isCustomRangeTimestamp()) && + (!isPreferredApiCassandra || !_.contains(cassandraKeys, this.field())) + ); + }); + this.isTypeEditable = ko.pureComputed( + () => + this.field() !== "Timestamp" && + this.field() !== "PartitionKey" && + this.field() !== "RowKey" && + !this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra() + ); + + this.and_or.subscribe((value) => { + this._queryBuilderViewModel.checkIfClauseChanged(this); + }); + this.field.subscribe((value) => { + this.changeField(); + }); + this.type.subscribe((value) => { + this.changeType(); + }); + this.timeValue.subscribe((value) => { + // if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) { + // this.customTimestampDialog(); + // } + }); + this.customTimeValue.subscribe((value) => { + this._queryBuilderViewModel.checkIfClauseChanged(this); + }); + this.value.subscribe((value) => { + this._queryBuilderViewModel.checkIfClauseChanged(this); + }); + this.operator.subscribe((value) => { + this._queryBuilderViewModel.checkIfClauseChanged(this); + }); + this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => { + this._queryBuilderViewModel.updateCanGroupClauses(); + }); + this.isAndOrFocused = ko.observable(false); + this.isDeleteButtonFocused = ko.observable(false); + } + + // private setCustomLastTimestamp() : void { + // if (this.customLastTimestamp() === null) { + // var lastNumberandType: CustomTimestampHelper.ILastQuery = { + // lastNumber: 7, + // lastTimeUnit: "Days" + // }; + // this.customLastTimestamp(lastNumberandType); + // } + // } + + private getValueType(): void { + switch (this.timestampType()) { + case "time": + this.isValue = ko.observable(false); + this.isTimestamp = ko.observable(true); + this.isCustomLastTimestamp = ko.observable(false); + this.isCustomRangeTimestamp = ko.observable(false); + break; + case "last": + this.isValue = ko.observable(false); + this.isTimestamp = ko.observable(false); + this.isCustomLastTimestamp = ko.observable(true); + this.isCustomRangeTimestamp = ko.observable(false); + break; + case "range": + this.isValue = ko.observable(false); + this.isTimestamp = ko.observable(false); + this.isCustomLastTimestamp = ko.observable(false); + this.isCustomRangeTimestamp = ko.observable(true); + break; + default: + this.isValue = ko.observable(true); + this.isTimestamp = ko.observable(false); + this.isCustomLastTimestamp = ko.observable(false); + this.isCustomRangeTimestamp = ko.observable(false); + } + } + + private changeField(): void { + this.isCustomLastTimestamp(false); + this.isCustomRangeTimestamp(false); + + if (this.field() === "Timestamp") { + this.isValue(false); + this.isTimestamp(true); + this.type(QueryBuilderConstants.TableType.DateTime); + this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo); + this.timestampType("time"); + } else if (this.field() === "PartitionKey" || this.field() === "RowKey") { + this.resetFromTimestamp(); + this.type(QueryBuilderConstants.TableType.String); + } else { + this.resetFromTimestamp(); + if (this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) { + const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection + .cassandraSchema; + for (let i = 0, len = cassandraSchema.length; i < len; i++) { + if (cassandraSchema[i].property === this.field()) { + this.type(cassandraSchema[i].type); + i = len; + } + } + } else { + this.type(QueryBuilderConstants.TableType.String); + } + } + this._queryBuilderViewModel.checkIfClauseChanged(this); + } + + private resetFromTimestamp(): void { + this.isValue(true); + this.isTimestamp(false); + this.operator(QueryBuilderConstants.Operator.Equal); + this.value(""); + this.timestampType(""); + this.timeValue(""); + this.customTimeValue(""); + } + + private changeType(): void { + this.isCustomLastTimestamp(false); + this.isCustomRangeTimestamp(false); + + if (this.type() === QueryBuilderConstants.TableType.DateTime) { + this.isValue(false); + this.isTimestamp(true); + this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo); + this.timestampType("time"); + } else { + this.isValue(true); + this.isTimestamp(false); + this.timeValue(""); + this.operator(QueryBuilderConstants.Operator.EqualTo); + this.value(""); + this.timestampType(""); + this.timeValue(""); + this.customTimeValue(""); + } + this._queryBuilderViewModel.checkIfClauseChanged(this); + } + + // private customTimestampDialog(): Promise { + // var lastNumber = this.customLastTimestamp().lastNumber; + // var lastTimeUnit = this.customLastTimestamp().lastTimeUnit; + + // return this._host.executeOperation("Environment.openDialog", [{ + // id: AzureConstants.registeredDialogs.customTimestampQueryDialog, + // width: 500, + // height: 300, + // parameters: { lastNumber, lastTimeUnit } + // }]).then((timestamp: CustomTimestampHelper.ITimestampQuery) => { + // if (timestamp) { + // this.isValue(false); + // this.isTimestamp(false); + // this.timestampType(timestamp.queryType); + + // if (timestamp.queryType === "last") { + // this.isCustomLastTimestamp(true); + // this.isCustomRangeTimestamp(false); + + // var lastNumberandType: CustomTimestampHelper.ILastQuery = { + // lastNumber: timestamp.lastNumber, + // lastTimeUnit: timestamp.lastTimeUnit + // }; + + // this.customLastTimestamp(lastNumberandType); + // this.customTimeValue(`Last ${timestamp.lastNumber} ${timestamp.lastTimeUnit}`); + + // } else { + // if (timestamp.timeZone === "local") { + // this.isLocal = ko.observable(true); + // } else { + // this.isLocal = ko.observable(false); + // } + // this.isCustomLastTimestamp(false); + // this.isCustomRangeTimestamp(true); + // this.customTimeValue(timestamp.startTime); + // CustomTimestampHelper.addRangeTimestamp(timestamp, this._queryBuilderViewModel, this); + // } + // } else { + // this.timeValue(QueryBuilderConstants.timeOptions.lastHour); + // } + // }); + // } + + public getId(): string { + return this._id; + } + + public get groupDepth(): number { + if (this.clauseGroup) { + return this.clauseGroup.getCurrentGroupDepth(); + } + + return -1; + } + + public dispose(): void { + if (this._groupCheckSubscription) { + this._groupCheckSubscription.dispose(); + } + + this.clauseGroup = null; + this._queryBuilderViewModel = null; + } +} diff --git a/src/Explorer/Tables/QueryBuilder/QueryViewModel.ts b/src/Explorer/Tables/QueryBuilder/QueryViewModel.ts index 058c46f93..d9344469f 100644 --- a/src/Explorer/Tables/QueryBuilder/QueryViewModel.ts +++ b/src/Explorer/Tables/QueryBuilder/QueryViewModel.ts @@ -1,237 +1,237 @@ -import * as ko from "knockout"; -import * as _ from "underscore"; - -import QueryBuilderViewModel from "./QueryBuilderViewModel"; -import QueryClauseViewModel from "./QueryClauseViewModel"; -import TableEntityListViewModel from "../DataTable/TableEntityListViewModel"; -import QueryTablesTab from "../../Tabs/QueryTablesTab"; -import * as DataTableUtilities from "../DataTable/DataTableUtilities"; -import { KeyCodes } from "../../../Common/Constants"; -import { getQuotedCqlIdentifier } from "../CqlUtilities"; - -export default class QueryViewModel { - public topValueLimitMessage: string = "Please input a number between 0 and 1000."; - public queryBuilderViewModel = ko.observable(); - public isHelperActive = ko.observable(true); - public isEditorActive = ko.observable(false); - public isExpanded = ko.observable(false); - public isWarningBox = ko.observable(); - public hasQueryError: ko.Computed; - public queryErrorMessage: ko.Computed; - public isSaveEnabled: ko.PureComputed; - public isExceedingLimit: ko.Computed; - public canRunQuery: ko.Computed; - public queryTextIsReadOnly: ko.Computed; - public queryText = ko.observable(); - public topValue = ko.observable(); - public selectText = ko.observableArray(); - public unchangedText = ko.observable(); - public unchangedSaveText = ko.observable(); - public unchangedSaveTop = ko.observable(); - public unchangedSaveSelect = ko.observableArray(); - public focusTopResult: ko.Observable; - public focusExpandIcon: ko.Observable; - - public savedQueryName = ko.observable(); - public selectMessage = ko.observable(); - - public columnOptions: ko.ObservableArray; - - public queryTablesTab: QueryTablesTab; - public id: string; - private _tableEntityListViewModel: TableEntityListViewModel; - - constructor(queryTablesTab: QueryTablesTab) { - this.queryTablesTab = queryTablesTab; - this.id = `queryViewModel${this.queryTablesTab.tabId}`; - this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel(); - - this.queryTextIsReadOnly = ko.computed(() => { - return !this.queryTablesTab.container.isPreferredApiCassandra(); - }); - let initialOptions = this._tableEntityListViewModel.headers; - this.columnOptions = ko.observableArray(initialOptions); - this.focusTopResult = ko.observable(false); - this.focusExpandIcon = ko.observable(false); - - this.queryBuilderViewModel(new QueryBuilderViewModel(this, this._tableEntityListViewModel)); - - this.isSaveEnabled = ko.pureComputed( - () => - this.queryText() !== this.unchangedSaveText() || - this.selectText() !== this.unchangedSaveSelect() || - this.topValue() !== this.unchangedSaveTop() - ); - - this.queryBuilderViewModel().clauseArray.subscribe(value => { - this.setFilter(); - }); - - this.isExceedingLimit = ko.computed(() => { - var currentTopValue: number = this.topValue(); - return currentTopValue < 0 || currentTopValue > 1000; - }); - - this.canRunQuery = ko.computed(() => { - return !this.isExceedingLimit(); - }); - - this.hasQueryError = ko.computed(() => { - return !!this._tableEntityListViewModel.queryErrorMessage(); - }); - - this.queryErrorMessage = ko.computed(() => { - return this._tableEntityListViewModel.queryErrorMessage(); - }); - } - - public selectHelper = (): void => { - this.isHelperActive(true); - this.isEditorActive(false); - DataTableUtilities.forceRecalculateTableSize(); - }; - - public selectEditor = (): void => { - this.setFilter(); - if (!this.isEditorActive()) { - this.unchangedText(this.queryText()); - } - this.isEditorActive(true); - this.isHelperActive(false); - DataTableUtilities.forceRecalculateTableSize(); - }; - - public toggleAdvancedOptions = () => { - this.isExpanded(!this.isExpanded()); - if (this.isExpanded()) { - this.focusTopResult(true); - } else { - this.focusExpandIcon(true); - } - DataTableUtilities.forceRecalculateTableSize(); // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size. - }; - - public ontoggleAdvancedOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.toggleAdvancedOptions(); - event.stopPropagation(); - return false; - } - return true; - }; - - private _getSelectedResults = (): Array => { - return this.selectText(); - }; - - private setFilter = (): string => { - var queryString = this.isEditorActive() - ? this.queryText() - : this.queryTablesTab.container.isPreferredApiCassandra() - ? this.queryBuilderViewModel().getCqlFilterFromClauses() - : this.queryBuilderViewModel().getODataFilterFromClauses(); - var filter = queryString; - this.queryText(filter); - return this.queryText(); - }; - - private setSqlFilter = (): string => { - var filter = this.queryBuilderViewModel().getSqlFilterFromClauses(); - return filter; - }; - - private setCqlFilter = (): string => { - var filter = this.queryBuilderViewModel().getCqlFilterFromClauses(); - return filter; - }; - - public isHelperEnabled = ko - .computed(() => { - return ( - this.queryText() === this.unchangedText() || - this.queryText() === null || - this.queryText() === "" || - this.isHelperActive() - ); - }) - .extend({ - notify: "always" - }); - - public runQuery = (): DataTables.DataTable => { - var filter = this.setFilter(); - if (filter && !this.queryTablesTab.container.isPreferredApiCassandra()) { - filter = filter.replace(/"/g, "'"); - } - var top = this.topValue(); - var selectOptions = this._getSelectedResults(); - var select = selectOptions; - this._tableEntityListViewModel.tableQuery.filter = filter; - this._tableEntityListViewModel.tableQuery.top = top; - this._tableEntityListViewModel.tableQuery.select = select; - this._tableEntityListViewModel.oDataQuery(filter); - this._tableEntityListViewModel.sqlQuery(this.setSqlFilter()); - this._tableEntityListViewModel.cqlQuery(filter); - - return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false); - }; - - public clearQuery = (): DataTables.DataTable => { - this.queryText(null); - this.topValue(null); - this.selectText(null); - this.selectMessage(""); - // clears the queryBuilder and adds a new blank clause - this.queryBuilderViewModel().queryClauses.removeAll(); - this.queryBuilderViewModel().addNewClause(); - this._tableEntityListViewModel.tableQuery.filter = null; - this._tableEntityListViewModel.tableQuery.top = null; - this._tableEntityListViewModel.tableQuery.select = null; - this._tableEntityListViewModel.oDataQuery(""); - this._tableEntityListViewModel.sqlQuery("SELECT * FROM c"); - this._tableEntityListViewModel.cqlQuery( - `SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier( - this.queryTablesTab.collection.id() - )}` - ); - return this._tableEntityListViewModel.reloadTable(false); - }; - - public selectQueryOptions(): Promise { - this.queryTablesTab.container.querySelectPane.queryViewModel = this; - this.queryTablesTab.container.querySelectPane.open(); - return null; - } - - public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.selectQueryOptions(); - event.stopPropagation(); - return false; - } - return true; - }; - - public getSelectMessage(): void { - if (_.isEmpty(this.selectText()) || this.selectText() === null) { - this.selectMessage(""); - } else { - this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`); - } - } - - public isSelected = ko.computed(() => { - return !(_.isEmpty(this.selectText()) || this.selectText() === null); - }); - - private setCheckToSave(): void { - this.unchangedSaveText(this.setFilter()); - this.unchangedSaveTop(this.topValue()); - this.unchangedSaveSelect(this.selectText()); - this.isSaveEnabled(false); - } - - public checkIfBuilderChanged(clause: QueryClauseViewModel): void { - this.setFilter(); - } -} +import * as ko from "knockout"; +import * as _ from "underscore"; + +import QueryBuilderViewModel from "./QueryBuilderViewModel"; +import QueryClauseViewModel from "./QueryClauseViewModel"; +import TableEntityListViewModel from "../DataTable/TableEntityListViewModel"; +import QueryTablesTab from "../../Tabs/QueryTablesTab"; +import * as DataTableUtilities from "../DataTable/DataTableUtilities"; +import { KeyCodes } from "../../../Common/Constants"; +import { getQuotedCqlIdentifier } from "../CqlUtilities"; + +export default class QueryViewModel { + public topValueLimitMessage: string = "Please input a number between 0 and 1000."; + public queryBuilderViewModel = ko.observable(); + public isHelperActive = ko.observable(true); + public isEditorActive = ko.observable(false); + public isExpanded = ko.observable(false); + public isWarningBox = ko.observable(); + public hasQueryError: ko.Computed; + public queryErrorMessage: ko.Computed; + public isSaveEnabled: ko.PureComputed; + public isExceedingLimit: ko.Computed; + public canRunQuery: ko.Computed; + public queryTextIsReadOnly: ko.Computed; + public queryText = ko.observable(); + public topValue = ko.observable(); + public selectText = ko.observableArray(); + public unchangedText = ko.observable(); + public unchangedSaveText = ko.observable(); + public unchangedSaveTop = ko.observable(); + public unchangedSaveSelect = ko.observableArray(); + public focusTopResult: ko.Observable; + public focusExpandIcon: ko.Observable; + + public savedQueryName = ko.observable(); + public selectMessage = ko.observable(); + + public columnOptions: ko.ObservableArray; + + public queryTablesTab: QueryTablesTab; + public id: string; + private _tableEntityListViewModel: TableEntityListViewModel; + + constructor(queryTablesTab: QueryTablesTab) { + this.queryTablesTab = queryTablesTab; + this.id = `queryViewModel${this.queryTablesTab.tabId}`; + this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel(); + + this.queryTextIsReadOnly = ko.computed(() => { + return !this.queryTablesTab.container.isPreferredApiCassandra(); + }); + let initialOptions = this._tableEntityListViewModel.headers; + this.columnOptions = ko.observableArray(initialOptions); + this.focusTopResult = ko.observable(false); + this.focusExpandIcon = ko.observable(false); + + this.queryBuilderViewModel(new QueryBuilderViewModel(this, this._tableEntityListViewModel)); + + this.isSaveEnabled = ko.pureComputed( + () => + this.queryText() !== this.unchangedSaveText() || + this.selectText() !== this.unchangedSaveSelect() || + this.topValue() !== this.unchangedSaveTop() + ); + + this.queryBuilderViewModel().clauseArray.subscribe((value) => { + this.setFilter(); + }); + + this.isExceedingLimit = ko.computed(() => { + var currentTopValue: number = this.topValue(); + return currentTopValue < 0 || currentTopValue > 1000; + }); + + this.canRunQuery = ko.computed(() => { + return !this.isExceedingLimit(); + }); + + this.hasQueryError = ko.computed(() => { + return !!this._tableEntityListViewModel.queryErrorMessage(); + }); + + this.queryErrorMessage = ko.computed(() => { + return this._tableEntityListViewModel.queryErrorMessage(); + }); + } + + public selectHelper = (): void => { + this.isHelperActive(true); + this.isEditorActive(false); + DataTableUtilities.forceRecalculateTableSize(); + }; + + public selectEditor = (): void => { + this.setFilter(); + if (!this.isEditorActive()) { + this.unchangedText(this.queryText()); + } + this.isEditorActive(true); + this.isHelperActive(false); + DataTableUtilities.forceRecalculateTableSize(); + }; + + public toggleAdvancedOptions = () => { + this.isExpanded(!this.isExpanded()); + if (this.isExpanded()) { + this.focusTopResult(true); + } else { + this.focusExpandIcon(true); + } + DataTableUtilities.forceRecalculateTableSize(); // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size. + }; + + public ontoggleAdvancedOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.toggleAdvancedOptions(); + event.stopPropagation(); + return false; + } + return true; + }; + + private _getSelectedResults = (): Array => { + return this.selectText(); + }; + + private setFilter = (): string => { + var queryString = this.isEditorActive() + ? this.queryText() + : this.queryTablesTab.container.isPreferredApiCassandra() + ? this.queryBuilderViewModel().getCqlFilterFromClauses() + : this.queryBuilderViewModel().getODataFilterFromClauses(); + var filter = queryString; + this.queryText(filter); + return this.queryText(); + }; + + private setSqlFilter = (): string => { + var filter = this.queryBuilderViewModel().getSqlFilterFromClauses(); + return filter; + }; + + private setCqlFilter = (): string => { + var filter = this.queryBuilderViewModel().getCqlFilterFromClauses(); + return filter; + }; + + public isHelperEnabled = ko + .computed(() => { + return ( + this.queryText() === this.unchangedText() || + this.queryText() === null || + this.queryText() === "" || + this.isHelperActive() + ); + }) + .extend({ + notify: "always", + }); + + public runQuery = (): DataTables.DataTable => { + var filter = this.setFilter(); + if (filter && !this.queryTablesTab.container.isPreferredApiCassandra()) { + filter = filter.replace(/"/g, "'"); + } + var top = this.topValue(); + var selectOptions = this._getSelectedResults(); + var select = selectOptions; + this._tableEntityListViewModel.tableQuery.filter = filter; + this._tableEntityListViewModel.tableQuery.top = top; + this._tableEntityListViewModel.tableQuery.select = select; + this._tableEntityListViewModel.oDataQuery(filter); + this._tableEntityListViewModel.sqlQuery(this.setSqlFilter()); + this._tableEntityListViewModel.cqlQuery(filter); + + return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false); + }; + + public clearQuery = (): DataTables.DataTable => { + this.queryText(null); + this.topValue(null); + this.selectText(null); + this.selectMessage(""); + // clears the queryBuilder and adds a new blank clause + this.queryBuilderViewModel().queryClauses.removeAll(); + this.queryBuilderViewModel().addNewClause(); + this._tableEntityListViewModel.tableQuery.filter = null; + this._tableEntityListViewModel.tableQuery.top = null; + this._tableEntityListViewModel.tableQuery.select = null; + this._tableEntityListViewModel.oDataQuery(""); + this._tableEntityListViewModel.sqlQuery("SELECT * FROM c"); + this._tableEntityListViewModel.cqlQuery( + `SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier( + this.queryTablesTab.collection.id() + )}` + ); + return this._tableEntityListViewModel.reloadTable(false); + }; + + public selectQueryOptions(): Promise { + this.queryTablesTab.container.querySelectPane.queryViewModel = this; + this.queryTablesTab.container.querySelectPane.open(); + return null; + } + + public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.selectQueryOptions(); + event.stopPropagation(); + return false; + } + return true; + }; + + public getSelectMessage(): void { + if (_.isEmpty(this.selectText()) || this.selectText() === null) { + this.selectMessage(""); + } else { + this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`); + } + } + + public isSelected = ko.computed(() => { + return !(_.isEmpty(this.selectText()) || this.selectText() === null); + }); + + private setCheckToSave(): void { + this.unchangedSaveText(this.setFilter()); + this.unchangedSaveTop(this.topValue()); + this.unchangedSaveSelect(this.selectText()); + this.isSaveEnabled(false); + } + + public checkIfBuilderChanged(clause: QueryClauseViewModel): void { + this.setFilter(); + } +} diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 47e0ca03b..a0f455537 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -1,632 +1,632 @@ -import * as ko from "knockout"; -import Q from "q"; - -import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; -import { AuthType } from "../../AuthType"; -import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { FeedOptions } from "@azure/cosmos"; -import * as Constants from "../../Common/Constants"; -import * as Entities from "./Entities"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import * as TableConstants from "./Constants"; -import * as TableEntityProcessor from "./TableEntityProcessor"; -import * as ViewModels from "../../Contracts/ViewModels"; -import Explorer from "../Explorer"; -import { configContext } from "../../ConfigContext"; -import { handleError } from "../../Common/ErrorHandlingUtils"; -import { createDocument } from "../../Common/dataAccess/createDocument"; -import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; -import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; -import { updateDocument } from "../../Common/dataAccess/updateDocument"; - -export interface CassandraTableKeys { - partitionKeys: CassandraTableKey[]; - clusteringKeys: CassandraTableKey[]; -} - -export interface CassandraTableKey { - property: string; - type: string; -} - -export abstract class TableDataClient { - constructor() {} - - public abstract createDocument( - collection: ViewModels.Collection, - entity: Entities.ITableEntity - ): Q.Promise; - - public abstract updateDocument( - collection: ViewModels.Collection, - originalDocument: any, - newEntity: Entities.ITableEntity - ): Promise; - - public abstract queryDocuments( - collection: ViewModels.Collection, - query: string, - shouldNotify?: boolean, - paginationToken?: string - ): Promise; - - public abstract deleteDocuments( - collection: ViewModels.Collection, - entitiesToDelete: Entities.ITableEntity[] - ): Promise; -} - -export class TablesAPIDataClient extends TableDataClient { - public createDocument( - collection: ViewModels.Collection, - entity: Entities.ITableEntity - ): Q.Promise { - const deferred = Q.defer(); - createDocument( - collection, - TableEntityProcessor.convertEntityToNewDocument(entity) - ).then( - (newDocument: any) => { - const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; - deferred.resolve(newEntity); - }, - reason => { - deferred.reject(reason); - } - ); - return deferred.promise; - } - - public async updateDocument( - collection: ViewModels.Collection, - originalDocument: any, - entity: Entities.ITableEntity - ): Promise { - try { - const newDocument = await updateDocument( - collection, - originalDocument, - TableEntityProcessor.convertEntityToNewDocument(entity) - ); - return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; - } catch (error) { - handleError(error, "TablesAPIDataClient/updateDocument"); - throw error; - } - } - - public async queryDocuments( - collection: ViewModels.Collection, - query: string - ): Promise { - try { - const options = { - enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() - } as FeedOptions; - const iterator = queryDocuments(collection.databaseId, collection.id(), query, options); - const response = await iterator.fetchNext(); - const documents = response?.resources; - const entities = TableEntityProcessor.convertDocumentsToEntities(documents); - - return { - Results: entities, - ContinuationToken: iterator.hasMoreResults(), - iterator: iterator - }; - } catch (error) { - handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed"); - throw error; - } - } - - public async deleteDocuments( - collection: ViewModels.Collection, - entitiesToDelete: Entities.ITableEntity[] - ): Promise { - const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( - entitiesToDelete, - collection - ); - - await Promise.all( - documentsToDelete?.map(async document => { - document.id = ko.observable(document.id); - await deleteDocument(collection, document); - }) - ); - } -} - -export class CassandraAPIDataClient extends TableDataClient { - public createDocument( - collection: ViewModels.Collection, - entity: Entities.ITableEntity - ): Q.Promise { - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Adding new row to table ${collection.id()}` - ); - let properties = "("; - let values = "("; - for (let property in entity) { - if (entity[property]._ === null) { - continue; - } - properties = properties.concat(`${property}, `); - const propertyType = entity[property].$; - if (this.isStringType(propertyType)) { - values = values.concat(`'${entity[property]._}', `); - } else { - values = values.concat(`${entity[property]._}, `); - } - } - properties = properties.slice(0, properties.length - 2) + ")"; - values = values.slice(0, values.length - 2) + ")"; - const query = `INSERT INTO ${collection.databaseId}.${collection.id()} ${properties} VALUES ${values}`; - const deferred = Q.defer(); - this.queryDocuments(collection, query) - .then( - (data: any) => { - entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; - entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); - NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`); - deferred.resolve(entity); - }, - error => { - handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); - deferred.reject(error); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - return deferred.promise; - } - - public async updateDocument( - collection: ViewModels.Collection, - originalDocument: any, - newEntity: Entities.ITableEntity - ): Promise { - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`); - - try { - let whereSegment = " WHERE"; - let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( - collection.cassandraKeys.clusteringKeys - ); - for (let keyIndex in keys) { - const key = keys[keyIndex].property; - const keyType = keys[keyIndex].type; - whereSegment += this.isStringType(keyType) - ? ` ${key} = '${newEntity[key]._}' AND` - : ` ${key} = ${newEntity[key]._} AND`; - } - whereSegment = whereSegment.slice(0, whereSegment.length - 4); - - let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`; - let isPropertyUpdated = false; - for (let property in newEntity) { - if ( - !originalDocument[property] || - newEntity[property]._.toString() !== originalDocument[property]._.toString() - ) { - updateQuery += this.isStringType(newEntity[property].$) - ? ` SET ${property} = '${newEntity[property]._}',` - : ` SET ${property} = ${newEntity[property]._},`; - isPropertyUpdated = true; - } - } - - if (isPropertyUpdated) { - updateQuery = updateQuery.slice(0, updateQuery.length - 1); - updateQuery += whereSegment; - await this.queryDocuments(collection, updateQuery); - } - - let deleteQuery = `DELETE `; - let isPropertyDeleted = false; - for (let property in originalDocument) { - if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) { - deleteQuery += ` ${property},`; - isPropertyDeleted = true; - } - } - - if (isPropertyDeleted) { - deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1); - deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`; - await this.queryDocuments(collection, deleteQuery); - } - - newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey]; - NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`); - return newEntity; - } catch (error) { - handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}"); - throw error; - } finally { - clearMessage(); - } - } - - public async queryDocuments( - collection: ViewModels.Collection, - query: string, - shouldNotify?: boolean, - paginationToken?: string - ): Promise { - const clearMessage = - shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); - try { - const authType = window.authType; - const apiEndpoint: string = - authType === AuthType.EncryptedToken - ? Constants.CassandraBackend.guestQueryApi - : Constants.CassandraBackend.queryApi; - const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { - type: "POST", - data: { - accountName: - collection && collection.container.databaseAccount && collection.container.databaseAccount().name, - cassandraEndpoint: this.trimCassandraEndpoint( - collection.container.databaseAccount().properties.cassandraEndpoint - ), - resourceId: collection.container.databaseAccount().id, - keyspaceId: collection.databaseId, - tableId: collection.id(), - query, - paginationToken - }, - beforeSend: this.setAuthorizationHeader, - error: this.handleAjaxError, - cache: false - }); - shouldNotify && - NotificationConsoleUtils.logConsoleInfo( - `Successfully fetched ${data.result.length} rows for table ${collection.id()}` - ); - return { - Results: data.result, - ContinuationToken: data.paginationToken - }; - } catch (error) { - shouldNotify && - handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); - throw error; - } finally { - clearMessage?.(); - } - } - - public async deleteDocuments( - collection: ViewModels.Collection, - entitiesToDelete: Entities.ITableEntity[] - ): Promise { - const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; - const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); - - await Promise.all( - entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => { - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`); - const partitionKeyValue = currEntityToDelete[partitionKeyProperty]; - const currQuery = - query + - (this.isStringType(partitionKeyValue.$) - ? `${partitionKeyProperty} = '${partitionKeyValue._}'` - : `${partitionKeyProperty} = ${partitionKeyValue._}`); - - try { - await this.queryDocuments(collection, currQuery); - NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`); - } catch (error) { - handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); - throw error; - } finally { - clearMessage(); - } - }) - ); - } - - public createKeyspace( - cassandraEndpoint: string, - resourceId: string, - explorer: Explorer, - createKeyspaceQuery: string - ): Q.Promise { - if (!createKeyspaceQuery) { - return Q.reject("No query specified"); - } - - const deferred: Q.Deferred = Q.defer(); - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Creating a new keyspace with query ${createKeyspaceQuery}` - ); - this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery, explorer) - .then( - (data: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully created a keyspace with query ${createKeyspaceQuery}` - ); - deferred.resolve(); - }, - error => { - handleError( - error, - "CreateKeyspaceCassandra", - `Error while creating a keyspace with query ${createKeyspaceQuery}` - ); - deferred.reject(error); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - - return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); - } - - public createTableAndKeyspace( - cassandraEndpoint: string, - resourceId: string, - explorer: Explorer, - createTableQuery: string, - createKeyspaceQuery?: string - ): Q.Promise { - let createKeyspacePromise: Q.Promise; - if (createKeyspaceQuery) { - createKeyspacePromise = this.createKeyspace(cassandraEndpoint, resourceId, explorer, createKeyspaceQuery); - } else { - createKeyspacePromise = Q.resolve(null); - } - - const deferred = Q.defer(); - createKeyspacePromise.then( - () => { - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Creating a new table with query ${createTableQuery}` - ); - this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery, explorer) - .then( - (data: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully created a table with query ${createTableQuery}` - ); - deferred.resolve(); - }, - error => { - handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); - deferred.reject(error); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - }, - reason => { - deferred.reject(reason); - } - ); - return deferred.promise; - } - - public deleteTableOrKeyspace( - cassandraEndpoint: string, - resourceId: string, - deleteQuery: string, - explorer: Explorer - ): Q.Promise { - const deferred = Q.defer(); - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Deleting resource with query ${deleteQuery}` - ); - this.createOrDeleteQuery(cassandraEndpoint, resourceId, deleteQuery, explorer) - .then( - () => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully deleted resource with query ${deleteQuery}` - ); - deferred.resolve(); - }, - error => { - handleError( - error, - "DeleteKeyspaceOrTableCassandra", - `Error while deleting resource with query ${deleteQuery}` - ); - deferred.reject(error); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - return deferred.promise; - } - - public getTableKeys(collection: ViewModels.Collection): Q.Promise { - if (!!collection.cassandraKeys) { - return Q.resolve(collection.cassandraKeys); - } - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Fetching keys for table ${collection.id()}` - ); - const authType = window.authType; - const apiEndpoint: string = - authType === AuthType.EncryptedToken - ? Constants.CassandraBackend.guestKeysApi - : Constants.CassandraBackend.keysApi; - let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`; - const deferred = Q.defer(); - $.ajax(endpoint, { - type: "POST", - data: { - accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, - cassandraEndpoint: this.trimCassandraEndpoint( - collection.container.databaseAccount().properties.cassandraEndpoint - ), - resourceId: collection.container.databaseAccount().id, - keyspaceId: collection.databaseId, - tableId: collection.id() - }, - beforeSend: this.setAuthorizationHeader, - error: this.handleAjaxError, - cache: false - }) - .then( - (data: CassandraTableKeys) => { - collection.cassandraKeys = data; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully fetched keys for table ${collection.id()}` - ); - deferred.resolve(data); - }, - (error: any) => { - handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); - deferred.reject(error); - } - ) - .done(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - return deferred.promise; - } - - public getTableSchema(collection: ViewModels.Collection): Q.Promise { - if (!!collection.cassandraSchema) { - return Q.resolve(collection.cassandraSchema); - } - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Fetching schema for table ${collection.id()}` - ); - const authType = window.authType; - const apiEndpoint: string = - authType === AuthType.EncryptedToken - ? Constants.CassandraBackend.guestSchemaApi - : Constants.CassandraBackend.schemaApi; - let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`; - const deferred = Q.defer(); - $.ajax(endpoint, { - type: "POST", - data: { - accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, - cassandraEndpoint: this.trimCassandraEndpoint( - collection.container.databaseAccount().properties.cassandraEndpoint - ), - resourceId: collection.container.databaseAccount().id, - keyspaceId: collection.databaseId, - tableId: collection.id() - }, - beforeSend: this.setAuthorizationHeader, - error: this.handleAjaxError, - cache: false - }) - .then( - (data: any) => { - collection.cassandraSchema = data.columns; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully fetched schema for table ${collection.id()}` - ); - deferred.resolve(data.columns); - }, - (error: any) => { - handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); - deferred.reject(error); - } - ) - .done(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }); - return deferred.promise; - } - - private createOrDeleteQuery( - cassandraEndpoint: string, - resourceId: string, - query: string, - explorer: Explorer - ): Q.Promise { - const deferred = Q.defer(); - const authType = window.authType; - const apiEndpoint: string = - authType === AuthType.EncryptedToken - ? Constants.CassandraBackend.guestCreateOrDeleteApi - : Constants.CassandraBackend.createOrDeleteApi; - $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { - type: "POST", - data: { - accountName: explorer.databaseAccount() && explorer.databaseAccount().name, - cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint), - resourceId: resourceId, - query: query - }, - beforeSend: this.setAuthorizationHeader, - error: this.handleAjaxError, - cache: false - }).then( - (data: any) => { - deferred.resolve(); - }, - reason => { - deferred.reject(reason); - } - ); - return deferred.promise; - } - - private trimCassandraEndpoint(cassandraEndpoint: string): string { - if (!cassandraEndpoint) { - return cassandraEndpoint; - } - - if (cassandraEndpoint.indexOf("https://") === 0) { - cassandraEndpoint = cassandraEndpoint.slice(8, cassandraEndpoint.length); - } - - if (cassandraEndpoint.indexOf(":443/", cassandraEndpoint.length - 5) !== -1) { - cassandraEndpoint = cassandraEndpoint.slice(0, cassandraEndpoint.length - 5); - } - - return cassandraEndpoint; - } - - private setAuthorizationHeader: (xhr: XMLHttpRequest) => boolean = (xhr: XMLHttpRequest): boolean => { - const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); - xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token); - - return true; - }; - - private isStringType(dataType: string): boolean { - // TODO figure out rest of types that are considered strings by Cassandra (if any have been missed) - return ( - dataType === TableConstants.CassandraType.Text || - dataType === TableConstants.CassandraType.Inet || - dataType === TableConstants.CassandraType.Ascii || - dataType === TableConstants.CassandraType.Varchar - ); - } - - private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string { - return collection.cassandraKeys.partitionKeys[0].property; - } - - private handleAjaxError = (xhrObj: XMLHttpRequest, textStatus: string, errorThrown: string): void => { - if (!xhrObj) { - return; - } - - displayTokenRenewalPromptForStatus(xhrObj.status); - }; -} +import * as ko from "knockout"; +import Q from "q"; + +import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; +import { AuthType } from "../../AuthType"; +import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { FeedOptions } from "@azure/cosmos"; +import * as Constants from "../../Common/Constants"; +import * as Entities from "./Entities"; +import * as HeadersUtility from "../../Common/HeadersUtility"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import * as TableConstants from "./Constants"; +import * as TableEntityProcessor from "./TableEntityProcessor"; +import * as ViewModels from "../../Contracts/ViewModels"; +import Explorer from "../Explorer"; +import { configContext } from "../../ConfigContext"; +import { handleError } from "../../Common/ErrorHandlingUtils"; +import { createDocument } from "../../Common/dataAccess/createDocument"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; + +export interface CassandraTableKeys { + partitionKeys: CassandraTableKey[]; + clusteringKeys: CassandraTableKey[]; +} + +export interface CassandraTableKey { + property: string; + type: string; +} + +export abstract class TableDataClient { + constructor() {} + + public abstract createDocument( + collection: ViewModels.Collection, + entity: Entities.ITableEntity + ): Q.Promise; + + public abstract updateDocument( + collection: ViewModels.Collection, + originalDocument: any, + newEntity: Entities.ITableEntity + ): Promise; + + public abstract queryDocuments( + collection: ViewModels.Collection, + query: string, + shouldNotify?: boolean, + paginationToken?: string + ): Promise; + + public abstract deleteDocuments( + collection: ViewModels.Collection, + entitiesToDelete: Entities.ITableEntity[] + ): Promise; +} + +export class TablesAPIDataClient extends TableDataClient { + public createDocument( + collection: ViewModels.Collection, + entity: Entities.ITableEntity + ): Q.Promise { + const deferred = Q.defer(); + createDocument( + collection, + TableEntityProcessor.convertEntityToNewDocument(entity) + ).then( + (newDocument: any) => { + const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; + deferred.resolve(newEntity); + }, + (reason) => { + deferred.reject(reason); + } + ); + return deferred.promise; + } + + public async updateDocument( + collection: ViewModels.Collection, + originalDocument: any, + entity: Entities.ITableEntity + ): Promise { + try { + const newDocument = await updateDocument( + collection, + originalDocument, + TableEntityProcessor.convertEntityToNewDocument(entity) + ); + return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; + } catch (error) { + handleError(error, "TablesAPIDataClient/updateDocument"); + throw error; + } + } + + public async queryDocuments( + collection: ViewModels.Collection, + query: string + ): Promise { + try { + const options = { + enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey(), + } as FeedOptions; + const iterator = queryDocuments(collection.databaseId, collection.id(), query, options); + const response = await iterator.fetchNext(); + const documents = response?.resources; + const entities = TableEntityProcessor.convertDocumentsToEntities(documents); + + return { + Results: entities, + ContinuationToken: iterator.hasMoreResults(), + iterator: iterator, + }; + } catch (error) { + handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed"); + throw error; + } + } + + public async deleteDocuments( + collection: ViewModels.Collection, + entitiesToDelete: Entities.ITableEntity[] + ): Promise { + const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( + entitiesToDelete, + collection + ); + + await Promise.all( + documentsToDelete?.map(async (document) => { + document.id = ko.observable(document.id); + await deleteDocument(collection, document); + }) + ); + } +} + +export class CassandraAPIDataClient extends TableDataClient { + public createDocument( + collection: ViewModels.Collection, + entity: Entities.ITableEntity + ): Q.Promise { + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Adding new row to table ${collection.id()}` + ); + let properties = "("; + let values = "("; + for (let property in entity) { + if (entity[property]._ === null) { + continue; + } + properties = properties.concat(`${property}, `); + const propertyType = entity[property].$; + if (this.isStringType(propertyType)) { + values = values.concat(`'${entity[property]._}', `); + } else { + values = values.concat(`${entity[property]._}, `); + } + } + properties = properties.slice(0, properties.length - 2) + ")"; + values = values.slice(0, values.length - 2) + ")"; + const query = `INSERT INTO ${collection.databaseId}.${collection.id()} ${properties} VALUES ${values}`; + const deferred = Q.defer(); + this.queryDocuments(collection, query) + .then( + (data: any) => { + entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; + entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); + NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`); + deferred.resolve(entity); + }, + (error) => { + handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); + deferred.reject(error); + } + ) + .finally(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }); + return deferred.promise; + } + + public async updateDocument( + collection: ViewModels.Collection, + originalDocument: any, + newEntity: Entities.ITableEntity + ): Promise { + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`); + + try { + let whereSegment = " WHERE"; + let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( + collection.cassandraKeys.clusteringKeys + ); + for (let keyIndex in keys) { + const key = keys[keyIndex].property; + const keyType = keys[keyIndex].type; + whereSegment += this.isStringType(keyType) + ? ` ${key} = '${newEntity[key]._}' AND` + : ` ${key} = ${newEntity[key]._} AND`; + } + whereSegment = whereSegment.slice(0, whereSegment.length - 4); + + let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`; + let isPropertyUpdated = false; + for (let property in newEntity) { + if ( + !originalDocument[property] || + newEntity[property]._.toString() !== originalDocument[property]._.toString() + ) { + updateQuery += this.isStringType(newEntity[property].$) + ? ` SET ${property} = '${newEntity[property]._}',` + : ` SET ${property} = ${newEntity[property]._},`; + isPropertyUpdated = true; + } + } + + if (isPropertyUpdated) { + updateQuery = updateQuery.slice(0, updateQuery.length - 1); + updateQuery += whereSegment; + await this.queryDocuments(collection, updateQuery); + } + + let deleteQuery = `DELETE `; + let isPropertyDeleted = false; + for (let property in originalDocument) { + if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) { + deleteQuery += ` ${property},`; + isPropertyDeleted = true; + } + } + + if (isPropertyDeleted) { + deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1); + deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`; + await this.queryDocuments(collection, deleteQuery); + } + + newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey]; + NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`); + return newEntity; + } catch (error) { + handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}"); + throw error; + } finally { + clearMessage(); + } + } + + public async queryDocuments( + collection: ViewModels.Collection, + query: string, + shouldNotify?: boolean, + paginationToken?: string + ): Promise { + const clearMessage = + shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); + try { + const authType = window.authType; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? Constants.CassandraBackend.guestQueryApi + : Constants.CassandraBackend.queryApi; + const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + data: { + accountName: + collection && collection.container.databaseAccount && collection.container.databaseAccount().name, + cassandraEndpoint: this.trimCassandraEndpoint( + collection.container.databaseAccount().properties.cassandraEndpoint + ), + resourceId: collection.container.databaseAccount().id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + query, + paginationToken, + }, + beforeSend: this.setAuthorizationHeader, + error: this.handleAjaxError, + cache: false, + }); + shouldNotify && + NotificationConsoleUtils.logConsoleInfo( + `Successfully fetched ${data.result.length} rows for table ${collection.id()}` + ); + return { + Results: data.result, + ContinuationToken: data.paginationToken, + }; + } catch (error) { + shouldNotify && + handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); + throw error; + } finally { + clearMessage?.(); + } + } + + public async deleteDocuments( + collection: ViewModels.Collection, + entitiesToDelete: Entities.ITableEntity[] + ): Promise { + const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; + const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); + + await Promise.all( + entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => { + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`); + const partitionKeyValue = currEntityToDelete[partitionKeyProperty]; + const currQuery = + query + + (this.isStringType(partitionKeyValue.$) + ? `${partitionKeyProperty} = '${partitionKeyValue._}'` + : `${partitionKeyProperty} = ${partitionKeyValue._}`); + + try { + await this.queryDocuments(collection, currQuery); + NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`); + } catch (error) { + handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); + throw error; + } finally { + clearMessage(); + } + }) + ); + } + + public createKeyspace( + cassandraEndpoint: string, + resourceId: string, + explorer: Explorer, + createKeyspaceQuery: string + ): Q.Promise { + if (!createKeyspaceQuery) { + return Q.reject("No query specified"); + } + + const deferred: Q.Deferred = Q.defer(); + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Creating a new keyspace with query ${createKeyspaceQuery}` + ); + this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery, explorer) + .then( + (data: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + `Successfully created a keyspace with query ${createKeyspaceQuery}` + ); + deferred.resolve(); + }, + (error) => { + handleError( + error, + "CreateKeyspaceCassandra", + `Error while creating a keyspace with query ${createKeyspaceQuery}` + ); + deferred.reject(error); + } + ) + .finally(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }); + + return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); + } + + public createTableAndKeyspace( + cassandraEndpoint: string, + resourceId: string, + explorer: Explorer, + createTableQuery: string, + createKeyspaceQuery?: string + ): Q.Promise { + let createKeyspacePromise: Q.Promise; + if (createKeyspaceQuery) { + createKeyspacePromise = this.createKeyspace(cassandraEndpoint, resourceId, explorer, createKeyspaceQuery); + } else { + createKeyspacePromise = Q.resolve(null); + } + + const deferred = Q.defer(); + createKeyspacePromise.then( + () => { + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Creating a new table with query ${createTableQuery}` + ); + this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery, explorer) + .then( + (data: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + `Successfully created a table with query ${createTableQuery}` + ); + deferred.resolve(); + }, + (error) => { + handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); + deferred.reject(error); + } + ) + .finally(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }); + }, + (reason) => { + deferred.reject(reason); + } + ); + return deferred.promise; + } + + public deleteTableOrKeyspace( + cassandraEndpoint: string, + resourceId: string, + deleteQuery: string, + explorer: Explorer + ): Q.Promise { + const deferred = Q.defer(); + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Deleting resource with query ${deleteQuery}` + ); + this.createOrDeleteQuery(cassandraEndpoint, resourceId, deleteQuery, explorer) + .then( + () => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + `Successfully deleted resource with query ${deleteQuery}` + ); + deferred.resolve(); + }, + (error) => { + handleError( + error, + "DeleteKeyspaceOrTableCassandra", + `Error while deleting resource with query ${deleteQuery}` + ); + deferred.reject(error); + } + ) + .finally(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }); + return deferred.promise; + } + + public getTableKeys(collection: ViewModels.Collection): Q.Promise { + if (!!collection.cassandraKeys) { + return Q.resolve(collection.cassandraKeys); + } + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Fetching keys for table ${collection.id()}` + ); + const authType = window.authType; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? Constants.CassandraBackend.guestKeysApi + : Constants.CassandraBackend.keysApi; + let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`; + const deferred = Q.defer(); + $.ajax(endpoint, { + type: "POST", + data: { + accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, + cassandraEndpoint: this.trimCassandraEndpoint( + collection.container.databaseAccount().properties.cassandraEndpoint + ), + resourceId: collection.container.databaseAccount().id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + }, + beforeSend: this.setAuthorizationHeader, + error: this.handleAjaxError, + cache: false, + }) + .then( + (data: CassandraTableKeys) => { + collection.cassandraKeys = data; + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + `Successfully fetched keys for table ${collection.id()}` + ); + deferred.resolve(data); + }, + (error: any) => { + handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(error); + } + ) + .done(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }); + return deferred.promise; + } + + public getTableSchema(collection: ViewModels.Collection): Q.Promise { + if (!!collection.cassandraSchema) { + return Q.resolve(collection.cassandraSchema); + } + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Fetching schema for table ${collection.id()}` + ); + const authType = window.authType; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? Constants.CassandraBackend.guestSchemaApi + : Constants.CassandraBackend.schemaApi; + let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`; + const deferred = Q.defer(); + $.ajax(endpoint, { + type: "POST", + data: { + accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, + cassandraEndpoint: this.trimCassandraEndpoint( + collection.container.databaseAccount().properties.cassandraEndpoint + ), + resourceId: collection.container.databaseAccount().id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + }, + beforeSend: this.setAuthorizationHeader, + error: this.handleAjaxError, + cache: false, + }) + .then( + (data: any) => { + collection.cassandraSchema = data.columns; + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + `Successfully fetched schema for table ${collection.id()}` + ); + deferred.resolve(data.columns); + }, + (error: any) => { + handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(error); + } + ) + .done(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }); + return deferred.promise; + } + + private createOrDeleteQuery( + cassandraEndpoint: string, + resourceId: string, + query: string, + explorer: Explorer + ): Q.Promise { + const deferred = Q.defer(); + const authType = window.authType; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? Constants.CassandraBackend.guestCreateOrDeleteApi + : Constants.CassandraBackend.createOrDeleteApi; + $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + data: { + accountName: explorer.databaseAccount() && explorer.databaseAccount().name, + cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint), + resourceId: resourceId, + query: query, + }, + beforeSend: this.setAuthorizationHeader, + error: this.handleAjaxError, + cache: false, + }).then( + (data: any) => { + deferred.resolve(); + }, + (reason) => { + deferred.reject(reason); + } + ); + return deferred.promise; + } + + private trimCassandraEndpoint(cassandraEndpoint: string): string { + if (!cassandraEndpoint) { + return cassandraEndpoint; + } + + if (cassandraEndpoint.indexOf("https://") === 0) { + cassandraEndpoint = cassandraEndpoint.slice(8, cassandraEndpoint.length); + } + + if (cassandraEndpoint.indexOf(":443/", cassandraEndpoint.length - 5) !== -1) { + cassandraEndpoint = cassandraEndpoint.slice(0, cassandraEndpoint.length - 5); + } + + return cassandraEndpoint; + } + + private setAuthorizationHeader: (xhr: XMLHttpRequest) => boolean = (xhr: XMLHttpRequest): boolean => { + const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); + xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token); + + return true; + }; + + private isStringType(dataType: string): boolean { + // TODO figure out rest of types that are considered strings by Cassandra (if any have been missed) + return ( + dataType === TableConstants.CassandraType.Text || + dataType === TableConstants.CassandraType.Inet || + dataType === TableConstants.CassandraType.Ascii || + dataType === TableConstants.CassandraType.Varchar + ); + } + + private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string { + return collection.cassandraKeys.partitionKeys[0].property; + } + + private handleAjaxError = (xhrObj: XMLHttpRequest, textStatus: string, errorThrown: string): void => { + if (!xhrObj) { + return; + } + + displayTokenRenewalPromptForStatus(xhrObj.status); + }; +} diff --git a/src/Explorer/Tables/TableEntityProcessor.ts b/src/Explorer/Tables/TableEntityProcessor.ts index e63f0cfd8..f412fb12c 100644 --- a/src/Explorer/Tables/TableEntityProcessor.ts +++ b/src/Explorer/Tables/TableEntityProcessor.ts @@ -1,194 +1,194 @@ -import * as ViewModels from "../../Contracts/ViewModels"; -import * as Entities from "./Entities"; -import * as Constants from "./Constants"; -import * as DateTimeUtilities from "./QueryBuilder/DateTimeUtilities"; - -// For use exclusively with Tables API. - -enum DataTypes { - Guid = 0, - Double = 1, - String = 2, - Binary = 5, - Boolean = 8, - DateTime = 9, - Int32 = 16, - Int64 = 18 -} - -var tablesIndexers = { - Value: "$v", - Type: "$t" -}; - -export var keyProperties = { - PartitionKey: "$pk", - Id: "id", - Id2: "$id", // This should always be the same value as Id - Timestamp: "_ts", - resourceId: "_rid", - self: "_self", - etag: "_etag", - attachments: "_attachments" -}; - -export function convertDocumentsToEntities(documents: any[]): Entities.ITableEntityForTablesAPI[] { - let results: Entities.ITableEntityForTablesAPI[] = []; - documents && - documents.forEach(document => { - if (!document.hasOwnProperty(keyProperties.PartitionKey) || !document.hasOwnProperty(keyProperties.Id2)) { - //Document does not match the current required format for Tables, so we ignore it - return; // The rest of the key properties should be guaranteed as DocumentDB properties - } - let entity: Entities.ITableEntityForTablesAPI = { - PartitionKey: { - _: document[keyProperties.PartitionKey], - $: Constants.TableType.String - }, - RowKey: { - _: document[keyProperties.Id], - $: Constants.TableType.String - }, - Timestamp: { - // DocumentDB Timestamp is unix time so we convert to Javascript date here - _: DateTimeUtilities.convertUnixToJSDate(document[keyProperties.Timestamp]).toUTCString(), - $: Constants.TableType.DateTime - }, - _rid: { - _: document[keyProperties.resourceId], - $: Constants.TableType.String - }, - _self: { - _: document[keyProperties.self], - $: Constants.TableType.String - }, - _etag: { - _: document[keyProperties.etag], - $: Constants.TableType.String - }, - _attachments: { - _: document[keyProperties.attachments], - $: Constants.TableType.String - } - }; - for (var property in document) { - if (document.hasOwnProperty(property)) { - if ( - property !== keyProperties.PartitionKey && - property !== keyProperties.Id && - property !== keyProperties.Timestamp && - property !== keyProperties.resourceId && - property !== keyProperties.self && - property !== keyProperties.etag && - property !== keyProperties.attachments && - property !== keyProperties.Id2 - ) { - if (!document[property].hasOwnProperty("$v") || !document[property].hasOwnProperty("$t")) { - return; //Document property does not match the current required format for Tables, so we ignore it - } - if (DataTypes[document[property][tablesIndexers.Type]] === DataTypes[DataTypes.DateTime]) { - // Convert Ticks datetime to javascript date for better visualization in table - entity[property] = { - _: DateTimeUtilities.convertTicksToJSDate(document[property][tablesIndexers.Value]).toUTCString(), - $: DataTypes[document[property][tablesIndexers.Type]] - }; - } else { - entity[property] = { - _: document[property][tablesIndexers.Value], - $: DataTypes[document[property][tablesIndexers.Type]] - }; - } - } - } - } - results.push(entity); - }); - return results; -} - -// Do not use this to create a document to send to the server, only for delete and for giving rid/self/collection to the utility methods. -export function convertEntitiesToDocuments( - entities: Entities.ITableEntityForTablesAPI[], - collection: ViewModels.Collection -): any[] { - let results: any[] = []; - entities && - entities.forEach(entity => { - let document: any = { - $id: entity.RowKey._, - id: entity.RowKey._, - ts: DateTimeUtilities.convertJSDateToUnix(entity.Timestamp._), // Convert back to unix time - rid: entity._rid._, - self: entity._self._, - etag: entity._etag._, - attachments: entity._attachments._, - collection: collection - }; - if (collection.partitionKey) { - document["partitionKey"] = collection.partitionKey; - document[collection.partitionKeyProperty] = entity.PartitionKey._; - document["partitionKeyValue"] = entity.PartitionKey._; - } - for (var property in entity) { - if ( - property !== Constants.EntityKeyNames.PartitionKey && - property !== Constants.EntityKeyNames.RowKey && - property !== Constants.EntityKeyNames.Timestamp && - property !== keyProperties.resourceId && - property !== keyProperties.self && - property !== keyProperties.etag && - property !== keyProperties.attachments && - property !== keyProperties.Id2 - ) { - if (entity[property].$ === Constants.TableType.DateTime) { - // Convert javascript date back to ticks with 20 zeros padding - document[property] = { - $t: (DataTypes)[entity[property].$], - $v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._) - }; - } else { - document[property] = { - $t: (DataTypes)[entity[property].$], - $v: entity[property]._ - }; - } - } - } - results.push(document); - }); - return results; -} - -export function convertEntityToNewDocument(entity: Entities.ITableEntityForTablesAPI): any { - let document: any = { - $pk: entity.PartitionKey._, - $id: entity.RowKey._, - id: entity.RowKey._ - }; - for (var property in entity) { - if ( - property !== Constants.EntityKeyNames.PartitionKey && - property !== Constants.EntityKeyNames.RowKey && - property !== Constants.EntityKeyNames.Timestamp && - property !== keyProperties.resourceId && - property !== keyProperties.self && - property !== keyProperties.etag && - property !== keyProperties.attachments && - property !== keyProperties.Id2 - ) { - if (entity[property].$ === Constants.TableType.DateTime) { - // Convert javascript date back to ticks with 20 zeros padding - document[property] = { - $t: (DataTypes)[entity[property].$], - $v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._) - }; - } else { - document[property] = { - $t: (DataTypes)[entity[property].$], - $v: entity[property]._ - }; - } - } - } - return document; -} +import * as ViewModels from "../../Contracts/ViewModels"; +import * as Entities from "./Entities"; +import * as Constants from "./Constants"; +import * as DateTimeUtilities from "./QueryBuilder/DateTimeUtilities"; + +// For use exclusively with Tables API. + +enum DataTypes { + Guid = 0, + Double = 1, + String = 2, + Binary = 5, + Boolean = 8, + DateTime = 9, + Int32 = 16, + Int64 = 18, +} + +var tablesIndexers = { + Value: "$v", + Type: "$t", +}; + +export var keyProperties = { + PartitionKey: "$pk", + Id: "id", + Id2: "$id", // This should always be the same value as Id + Timestamp: "_ts", + resourceId: "_rid", + self: "_self", + etag: "_etag", + attachments: "_attachments", +}; + +export function convertDocumentsToEntities(documents: any[]): Entities.ITableEntityForTablesAPI[] { + let results: Entities.ITableEntityForTablesAPI[] = []; + documents && + documents.forEach((document) => { + if (!document.hasOwnProperty(keyProperties.PartitionKey) || !document.hasOwnProperty(keyProperties.Id2)) { + //Document does not match the current required format for Tables, so we ignore it + return; // The rest of the key properties should be guaranteed as DocumentDB properties + } + let entity: Entities.ITableEntityForTablesAPI = { + PartitionKey: { + _: document[keyProperties.PartitionKey], + $: Constants.TableType.String, + }, + RowKey: { + _: document[keyProperties.Id], + $: Constants.TableType.String, + }, + Timestamp: { + // DocumentDB Timestamp is unix time so we convert to Javascript date here + _: DateTimeUtilities.convertUnixToJSDate(document[keyProperties.Timestamp]).toUTCString(), + $: Constants.TableType.DateTime, + }, + _rid: { + _: document[keyProperties.resourceId], + $: Constants.TableType.String, + }, + _self: { + _: document[keyProperties.self], + $: Constants.TableType.String, + }, + _etag: { + _: document[keyProperties.etag], + $: Constants.TableType.String, + }, + _attachments: { + _: document[keyProperties.attachments], + $: Constants.TableType.String, + }, + }; + for (var property in document) { + if (document.hasOwnProperty(property)) { + if ( + property !== keyProperties.PartitionKey && + property !== keyProperties.Id && + property !== keyProperties.Timestamp && + property !== keyProperties.resourceId && + property !== keyProperties.self && + property !== keyProperties.etag && + property !== keyProperties.attachments && + property !== keyProperties.Id2 + ) { + if (!document[property].hasOwnProperty("$v") || !document[property].hasOwnProperty("$t")) { + return; //Document property does not match the current required format for Tables, so we ignore it + } + if (DataTypes[document[property][tablesIndexers.Type]] === DataTypes[DataTypes.DateTime]) { + // Convert Ticks datetime to javascript date for better visualization in table + entity[property] = { + _: DateTimeUtilities.convertTicksToJSDate(document[property][tablesIndexers.Value]).toUTCString(), + $: DataTypes[document[property][tablesIndexers.Type]], + }; + } else { + entity[property] = { + _: document[property][tablesIndexers.Value], + $: DataTypes[document[property][tablesIndexers.Type]], + }; + } + } + } + } + results.push(entity); + }); + return results; +} + +// Do not use this to create a document to send to the server, only for delete and for giving rid/self/collection to the utility methods. +export function convertEntitiesToDocuments( + entities: Entities.ITableEntityForTablesAPI[], + collection: ViewModels.Collection +): any[] { + let results: any[] = []; + entities && + entities.forEach((entity) => { + let document: any = { + $id: entity.RowKey._, + id: entity.RowKey._, + ts: DateTimeUtilities.convertJSDateToUnix(entity.Timestamp._), // Convert back to unix time + rid: entity._rid._, + self: entity._self._, + etag: entity._etag._, + attachments: entity._attachments._, + collection: collection, + }; + if (collection.partitionKey) { + document["partitionKey"] = collection.partitionKey; + document[collection.partitionKeyProperty] = entity.PartitionKey._; + document["partitionKeyValue"] = entity.PartitionKey._; + } + for (var property in entity) { + if ( + property !== Constants.EntityKeyNames.PartitionKey && + property !== Constants.EntityKeyNames.RowKey && + property !== Constants.EntityKeyNames.Timestamp && + property !== keyProperties.resourceId && + property !== keyProperties.self && + property !== keyProperties.etag && + property !== keyProperties.attachments && + property !== keyProperties.Id2 + ) { + if (entity[property].$ === Constants.TableType.DateTime) { + // Convert javascript date back to ticks with 20 zeros padding + document[property] = { + $t: (DataTypes)[entity[property].$], + $v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._), + }; + } else { + document[property] = { + $t: (DataTypes)[entity[property].$], + $v: entity[property]._, + }; + } + } + } + results.push(document); + }); + return results; +} + +export function convertEntityToNewDocument(entity: Entities.ITableEntityForTablesAPI): any { + let document: any = { + $pk: entity.PartitionKey._, + $id: entity.RowKey._, + id: entity.RowKey._, + }; + for (var property in entity) { + if ( + property !== Constants.EntityKeyNames.PartitionKey && + property !== Constants.EntityKeyNames.RowKey && + property !== Constants.EntityKeyNames.Timestamp && + property !== keyProperties.resourceId && + property !== keyProperties.self && + property !== keyProperties.etag && + property !== keyProperties.attachments && + property !== keyProperties.Id2 + ) { + if (entity[property].$ === Constants.TableType.DateTime) { + // Convert javascript date back to ticks with 20 zeros padding + document[property] = { + $t: (DataTypes)[entity[property].$], + $v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._), + }; + } else { + document[property] = { + $t: (DataTypes)[entity[property].$], + $v: entity[property]._, + }; + } + } + } + return document; +} diff --git a/src/Explorer/Tables/Utilities.ts b/src/Explorer/Tables/Utilities.ts index 3a851dfc8..83e29bd85 100644 --- a/src/Explorer/Tables/Utilities.ts +++ b/src/Explorer/Tables/Utilities.ts @@ -1,279 +1,279 @@ -import * as _ from "underscore"; -import Q from "q"; -import * as Entities from "./Entities"; -import { CassandraTableKey } from "./TableDataClient"; -import * as Constants from "./Constants"; - -/** - * Generates a pseudo-random GUID. - */ -export function guid() { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); -} - -/** - * Returns a promise that resolves in the specified number of milliseconds. - */ -export function delay(milliseconds: number): Q.Promise { - return Q.delay(milliseconds); -} - -/** - * Given a value and minimum and maximum limits, returns the value if it is within the limits - * (inclusive); or the maximum or minimum limit, if the value is greater or lesser than the - * respective limit. - */ -export function ensureBetweenBounds(value: number, minimum: number, maximum: number): number { - return Math.max(Math.min(value, maximum), minimum); -} - -/** - * Retrieves an appropriate error message for an error. - * @param error The actual error - * @param simpleMessage A simpler message to use instead of the actual error. - * If supplied, the original error will be added as "details". - */ -export function getErrorMessage(error: any, simpleMessage?: string): string { - var detailsMessage: string; - if (typeof error === "string" || error instanceof String) { - detailsMessage = error.toString(); - } else { - detailsMessage = error.message || error.error || error.name; - } - - if (simpleMessage && detailsMessage) { - return simpleMessage + getEnvironmentNewLine() + getEnvironmentNewLine() + "Details: " + detailsMessage; - } else if (simpleMessage) { - return simpleMessage; - } else { - return detailsMessage || "An unexpected error has occurred."; - } -} - -/** - * Get the environment's new line characters - */ -export function getEnvironmentNewLine(): string { - var platform = navigator.platform.toUpperCase(); - - if (platform.indexOf("WIN") >= 0) { - return "\r\n"; - } else { - // Mac OS X and *nix - return "\n"; - } -} - -/** - * Tests whether two arrays have same elements in the same sequence. - */ -export function isEqual(a: T[], b: T[]): boolean { - var isEqual: boolean = false; - if (!!a && !!b && a.length === b.length) { - isEqual = _.every(a, (value: T, index: number) => value === b[index]); - } - return isEqual; -} - -/** - * Escape meta-characters for jquery selector - */ -export function jQuerySelectorEscape(value: string): string { - value = value || ""; - return value.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, "\\$&"); -} - -export function copyTableQuery(query: Entities.ITableQuery): Entities.ITableQuery { - if (!query) { - return null; - } - - return { - filter: query.filter, - select: query.select && query.select.slice(), - top: query.top - }; -} - -/** - * Html encode - */ -export function htmlEncode(value: string): string { - var _divElem: JQuery = $("
"); - return _divElem.text(value).html(); -} - -/** - * Executes an action on a keyboard event. - * Modifiers: ctrlKey - control/command key, shiftKey - shift key, altKey - alt/option key; - * pass on 'null' to ignore the modifier (default). - */ -export function onKey( - event: any, - eventKeyCode: number, - action: ($sourceElement: JQuery) => void, - metaKey: boolean = null, - shiftKey: boolean = null, - altKey: boolean = null -): boolean { - var source: any = event.target || event.srcElement, - keyCode: number = event.keyCode, - $sourceElement = $(source), - handled: boolean = false; - - if ( - $sourceElement.length && - keyCode === eventKeyCode && - $.isFunction(action) && - (metaKey === null || metaKey === event.metaKey) && - (shiftKey === null || shiftKey === event.shiftKey) && - (altKey === null || altKey === event.altKey) - ) { - action($sourceElement); - handled = true; - } - - return handled; -} - -/** - * Executes an action on an 'enter' keyboard event. - */ -export function onEnter( - event: any, - action: ($sourceElement: JQuery) => void, - metaKey: boolean = null, - shiftKey: boolean = null, - altKey: boolean = null -): boolean { - return onKey(event, Constants.keyCodes.Enter, action, metaKey, shiftKey, altKey); -} - -/** - * Executes an action on a 'tab' keyboard event. - */ -export function onTab( - event: any, - action: ($sourceElement: JQuery) => void, - metaKey: boolean = null, - shiftKey: boolean = null, - altKey: boolean = null -): boolean { - return onKey(event, Constants.keyCodes.Tab, action, metaKey, shiftKey, altKey); -} - -/** - * Executes an action on an 'Esc' keyboard event. - */ -export function onEsc( - event: any, - action: ($sourceElement: JQuery) => void, - metaKey: boolean = null, - shiftKey: boolean = null, - altKey: boolean = null -): boolean { - return onKey(event, Constants.keyCodes.Esc, action, metaKey, shiftKey, altKey); -} - -/** - * Is the environment 'ctrl' key press. This key is used for multi selection, like select one more item, select all. - * For Windows and Linux, it's ctrl. For Mac, it's command. - */ -export function isEnvironmentCtrlPressed(event: JQueryEventObject): boolean { - return isMac() ? event.metaKey : event.ctrlKey; -} - -export function isEnvironmentShiftPressed(event: JQueryEventObject): boolean { - return event.shiftKey; -} - -export function isEnvironmentAltPressed(event: JQueryEventObject): boolean { - return event.altKey; -} - -/** - * Returns whether the current platform is MacOS. - */ -export function isMac(): boolean { - var platform = navigator.platform.toUpperCase(); - return platform.indexOf("MAC") >= 0; -} - -// MAX_SAFE_INTEGER and MIN_SAFE_INTEGER will be provided by ECMAScript 6's Number -export var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; -export var MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER; - -/** - * Tests whether a value a safe integer. - * A safe integer is an integer that can be exactly represented as an IEEE-754 double precision number (all integers from (2^53 - 1) to -(2^53 - 1)) - * Note: Function and constants will be provided by ECMAScript 6's Number. - */ -export function isSafeInteger(value: any): boolean { - var n: number = typeof value !== "number" ? Number(value) : value; - - return Math.round(n) === n && MIN_SAFE_INTEGER <= n && n <= MAX_SAFE_INTEGER; -} - -export function getInputTypeFromDisplayedName(displayedName: string): string { - switch (displayedName) { - case Constants.TableType.DateTime: - return Constants.InputType.DateTime; - case Constants.TableType.Int32: - case Constants.TableType.Int64: - case Constants.TableType.Double: - case Constants.CassandraType.Bigint: - case Constants.CassandraType.Decimal: - case Constants.CassandraType.Double: - case Constants.CassandraType.Float: - case Constants.CassandraType.Int: - case Constants.CassandraType.Smallint: - case Constants.CassandraType.Tinyint: - return Constants.InputType.Number; - default: - return Constants.InputType.Text; - } -} - -export function padLongWithZeros(value: string): string { - var s = "0000000000000000000" + value; - return s.substr(s.length - 20); -} - -/** - * Set a data type for each header. The data type is inferred from entities. - * Notice: Not every header will have a data type since some headers don't even exist in entities. - */ -export function getDataTypesFromEntities(headers: string[], entities: Entities.ITableEntity[]): any { - var currentHeaders: string[] = _.clone(headers); - var dataTypes: any = {}; - entities = entities || []; - entities.forEach((entity: Entities.ITableEntity, index: number) => { - if (currentHeaders.length) { - var keys: string[] = _.keys(entity); - var headersToProcess: string[] = _.intersection(currentHeaders, keys); - headersToProcess && - headersToProcess.forEach((propertyName: string) => { - dataTypes[propertyName] = entity[propertyName].$ || Constants.TableType.String; - }); - currentHeaders = _.difference(currentHeaders, headersToProcess); - } - }); - return dataTypes; -} - -/** - * Set a data type for each header. The data type is inferred from Cassandra Schema. - */ -export function getDataTypesFromCassandraSchema(schema: CassandraTableKey[]): any { - var dataTypes: any = {}; - schema && - schema.forEach((schemaItem: CassandraTableKey, index: number) => { - dataTypes[schemaItem.property] = schemaItem.type; - }); - return dataTypes; -} +import * as _ from "underscore"; +import Q from "q"; +import * as Entities from "./Entities"; +import { CassandraTableKey } from "./TableDataClient"; +import * as Constants from "./Constants"; + +/** + * Generates a pseudo-random GUID. + */ +export function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); +} + +/** + * Returns a promise that resolves in the specified number of milliseconds. + */ +export function delay(milliseconds: number): Q.Promise { + return Q.delay(milliseconds); +} + +/** + * Given a value and minimum and maximum limits, returns the value if it is within the limits + * (inclusive); or the maximum or minimum limit, if the value is greater or lesser than the + * respective limit. + */ +export function ensureBetweenBounds(value: number, minimum: number, maximum: number): number { + return Math.max(Math.min(value, maximum), minimum); +} + +/** + * Retrieves an appropriate error message for an error. + * @param error The actual error + * @param simpleMessage A simpler message to use instead of the actual error. + * If supplied, the original error will be added as "details". + */ +export function getErrorMessage(error: any, simpleMessage?: string): string { + var detailsMessage: string; + if (typeof error === "string" || error instanceof String) { + detailsMessage = error.toString(); + } else { + detailsMessage = error.message || error.error || error.name; + } + + if (simpleMessage && detailsMessage) { + return simpleMessage + getEnvironmentNewLine() + getEnvironmentNewLine() + "Details: " + detailsMessage; + } else if (simpleMessage) { + return simpleMessage; + } else { + return detailsMessage || "An unexpected error has occurred."; + } +} + +/** + * Get the environment's new line characters + */ +export function getEnvironmentNewLine(): string { + var platform = navigator.platform.toUpperCase(); + + if (platform.indexOf("WIN") >= 0) { + return "\r\n"; + } else { + // Mac OS X and *nix + return "\n"; + } +} + +/** + * Tests whether two arrays have same elements in the same sequence. + */ +export function isEqual(a: T[], b: T[]): boolean { + var isEqual: boolean = false; + if (!!a && !!b && a.length === b.length) { + isEqual = _.every(a, (value: T, index: number) => value === b[index]); + } + return isEqual; +} + +/** + * Escape meta-characters for jquery selector + */ +export function jQuerySelectorEscape(value: string): string { + value = value || ""; + return value.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, "\\$&"); +} + +export function copyTableQuery(query: Entities.ITableQuery): Entities.ITableQuery { + if (!query) { + return null; + } + + return { + filter: query.filter, + select: query.select && query.select.slice(), + top: query.top, + }; +} + +/** + * Html encode + */ +export function htmlEncode(value: string): string { + var _divElem: JQuery = $("
"); + return _divElem.text(value).html(); +} + +/** + * Executes an action on a keyboard event. + * Modifiers: ctrlKey - control/command key, shiftKey - shift key, altKey - alt/option key; + * pass on 'null' to ignore the modifier (default). + */ +export function onKey( + event: any, + eventKeyCode: number, + action: ($sourceElement: JQuery) => void, + metaKey: boolean = null, + shiftKey: boolean = null, + altKey: boolean = null +): boolean { + var source: any = event.target || event.srcElement, + keyCode: number = event.keyCode, + $sourceElement = $(source), + handled: boolean = false; + + if ( + $sourceElement.length && + keyCode === eventKeyCode && + $.isFunction(action) && + (metaKey === null || metaKey === event.metaKey) && + (shiftKey === null || shiftKey === event.shiftKey) && + (altKey === null || altKey === event.altKey) + ) { + action($sourceElement); + handled = true; + } + + return handled; +} + +/** + * Executes an action on an 'enter' keyboard event. + */ +export function onEnter( + event: any, + action: ($sourceElement: JQuery) => void, + metaKey: boolean = null, + shiftKey: boolean = null, + altKey: boolean = null +): boolean { + return onKey(event, Constants.keyCodes.Enter, action, metaKey, shiftKey, altKey); +} + +/** + * Executes an action on a 'tab' keyboard event. + */ +export function onTab( + event: any, + action: ($sourceElement: JQuery) => void, + metaKey: boolean = null, + shiftKey: boolean = null, + altKey: boolean = null +): boolean { + return onKey(event, Constants.keyCodes.Tab, action, metaKey, shiftKey, altKey); +} + +/** + * Executes an action on an 'Esc' keyboard event. + */ +export function onEsc( + event: any, + action: ($sourceElement: JQuery) => void, + metaKey: boolean = null, + shiftKey: boolean = null, + altKey: boolean = null +): boolean { + return onKey(event, Constants.keyCodes.Esc, action, metaKey, shiftKey, altKey); +} + +/** + * Is the environment 'ctrl' key press. This key is used for multi selection, like select one more item, select all. + * For Windows and Linux, it's ctrl. For Mac, it's command. + */ +export function isEnvironmentCtrlPressed(event: JQueryEventObject): boolean { + return isMac() ? event.metaKey : event.ctrlKey; +} + +export function isEnvironmentShiftPressed(event: JQueryEventObject): boolean { + return event.shiftKey; +} + +export function isEnvironmentAltPressed(event: JQueryEventObject): boolean { + return event.altKey; +} + +/** + * Returns whether the current platform is MacOS. + */ +export function isMac(): boolean { + var platform = navigator.platform.toUpperCase(); + return platform.indexOf("MAC") >= 0; +} + +// MAX_SAFE_INTEGER and MIN_SAFE_INTEGER will be provided by ECMAScript 6's Number +export var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; +export var MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER; + +/** + * Tests whether a value a safe integer. + * A safe integer is an integer that can be exactly represented as an IEEE-754 double precision number (all integers from (2^53 - 1) to -(2^53 - 1)) + * Note: Function and constants will be provided by ECMAScript 6's Number. + */ +export function isSafeInteger(value: any): boolean { + var n: number = typeof value !== "number" ? Number(value) : value; + + return Math.round(n) === n && MIN_SAFE_INTEGER <= n && n <= MAX_SAFE_INTEGER; +} + +export function getInputTypeFromDisplayedName(displayedName: string): string { + switch (displayedName) { + case Constants.TableType.DateTime: + return Constants.InputType.DateTime; + case Constants.TableType.Int32: + case Constants.TableType.Int64: + case Constants.TableType.Double: + case Constants.CassandraType.Bigint: + case Constants.CassandraType.Decimal: + case Constants.CassandraType.Double: + case Constants.CassandraType.Float: + case Constants.CassandraType.Int: + case Constants.CassandraType.Smallint: + case Constants.CassandraType.Tinyint: + return Constants.InputType.Number; + default: + return Constants.InputType.Text; + } +} + +export function padLongWithZeros(value: string): string { + var s = "0000000000000000000" + value; + return s.substr(s.length - 20); +} + +/** + * Set a data type for each header. The data type is inferred from entities. + * Notice: Not every header will have a data type since some headers don't even exist in entities. + */ +export function getDataTypesFromEntities(headers: string[], entities: Entities.ITableEntity[]): any { + var currentHeaders: string[] = _.clone(headers); + var dataTypes: any = {}; + entities = entities || []; + entities.forEach((entity: Entities.ITableEntity, index: number) => { + if (currentHeaders.length) { + var keys: string[] = _.keys(entity); + var headersToProcess: string[] = _.intersection(currentHeaders, keys); + headersToProcess && + headersToProcess.forEach((propertyName: string) => { + dataTypes[propertyName] = entity[propertyName].$ || Constants.TableType.String; + }); + currentHeaders = _.difference(currentHeaders, headersToProcess); + } + }); + return dataTypes; +} + +/** + * Set a data type for each header. The data type is inferred from Cassandra Schema. + */ +export function getDataTypesFromCassandraSchema(schema: CassandraTableKey[]): any { + var dataTypes: any = {}; + schema && + schema.forEach((schemaItem: CassandraTableKey, index: number) => { + dataTypes[schemaItem.property] = schemaItem.type; + }); + return dataTypes; +} diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index abad6ea4e..ff77107d8 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -74,10 +74,7 @@ export default class ConflictsTab extends TabsBase { this.partitionKeyPropertyHeader = (this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader(); this.partitionKeyProperty = !!this.partitionKeyPropertyHeader - ? this.partitionKeyPropertyHeader - .replace(/[/]+/g, ".") - .substr(1) - .replace(/[']+/g, "") + ? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "") : null; this.dataContentsGridScrollHeight = ko.observable(null); @@ -88,13 +85,13 @@ export default class ConflictsTab extends TabsBase { const tabContainer: HTMLElement = document.getElementById("content"); const splitterBounds: SplitterBounds = { min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth, - max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth + max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth, }; this.splitter = new Splitter({ splitterId: "h_splitter2", leftId: this.documentContentsContainerId, bounds: splitterBounds, - direction: SplitterDirection.Vertical + direction: SplitterDirection.Vertical, }); } }); @@ -150,7 +147,7 @@ export default class ConflictsTab extends TabsBase { visible: ko.computed(() => { return this.conflictOperation() !== Constants.ConflictOperationType.Delete || !!this.selectedConflictContent(); - }) + }), }; this.discardButton = { @@ -166,7 +163,7 @@ export default class ConflictsTab extends TabsBase { visible: ko.computed(() => { return this.conflictOperation() !== Constants.ConflictOperationType.Delete || !!this.selectedConflictContent(); - }) + }), }; this.deleteButton = { @@ -182,7 +179,7 @@ export default class ConflictsTab extends TabsBase { visible: ko.computed(() => { return true; - }) + }), }; this.buildCommandBarOptions(); @@ -270,7 +267,7 @@ export default class ConflictsTab extends TabsBase { tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId + conflictResourceId: selectedConflict.resourceId, }); try { @@ -317,7 +314,7 @@ export default class ConflictsTab extends TabsBase { tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId + conflictResourceId: selectedConflict.resourceId, }, startKey ); @@ -336,7 +333,7 @@ export default class ConflictsTab extends TabsBase { conflictOperationType: selectedConflict.operationType, conflictResourceId: selectedConflict.resourceId, error: errorMessage, - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, startKey ); @@ -358,7 +355,7 @@ export default class ConflictsTab extends TabsBase { tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId + conflictResourceId: selectedConflict.resourceId, }); try { @@ -377,7 +374,7 @@ export default class ConflictsTab extends TabsBase { tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId + conflictResourceId: selectedConflict.resourceId, }, startKey ); @@ -396,7 +393,7 @@ export default class ConflictsTab extends TabsBase { conflictOperationType: selectedConflict.operationType, conflictResourceId: selectedConflict.resourceId, error: errorMessage, - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, startKey ); @@ -453,7 +450,7 @@ export default class ConflictsTab extends TabsBase { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: getErrorMessage(error), - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, this.onLoadStartKey ); @@ -467,7 +464,7 @@ export default class ConflictsTab extends TabsBase { // TODO: Conflict Feed does not allow filtering atm const query: string = undefined; const options = { - enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() + enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey(), }; return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions); } @@ -479,7 +476,7 @@ export default class ConflictsTab extends TabsBase { .then( (conflictIdsResponse: DataModels.ConflictId[]) => { const currentConflicts = this.conflictIds(); - const currentDocumentsRids = currentConflicts.map(currentConflict => currentConflict.rid); + const currentDocumentsRids = currentConflicts.map((currentConflict) => currentConflict.rid); const nextConflictIds = conflictIdsResponse // filter documents already loaded in observable .filter((d: any) => { @@ -501,14 +498,14 @@ export default class ConflictsTab extends TabsBase { collectionName: this.collection.id(), defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, this.onLoadStartKey ); this.onLoadStartKey = null; } }, - error => { + (error) => { this.isExecutionError(true); if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { TelemetryProcessor.traceFailure( @@ -521,7 +518,7 @@ export default class ConflictsTab extends TabsBase { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: getErrorMessage(error), - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, this.onLoadStartKey ); @@ -541,7 +538,7 @@ export default class ConflictsTab extends TabsBase { }; protected _loadNextPageInternal(): Q.Promise { - return Q(this._documentsIterator.fetchNext().then(response => response.resources)); + return Q(this._documentsIterator.fetchNext().then((response) => response.resources)); } protected _onEditorContentChange(newContent: string) { @@ -615,7 +612,7 @@ export default class ConflictsTab extends TabsBase { commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.acceptChangesButton.enabled() + disabled: !this.acceptChangesButton.enabled(), }); } @@ -628,7 +625,7 @@ export default class ConflictsTab extends TabsBase { commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.discardButton.enabled() + disabled: !this.discardButton.enabled(), }); } @@ -641,7 +638,7 @@ export default class ConflictsTab extends TabsBase { commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.deleteButton.enabled() + disabled: !this.deleteButton.enabled(), }); } return buttons; @@ -656,7 +653,7 @@ export default class ConflictsTab extends TabsBase { this.discardButton.visible, this.discardButton.enabled, this.deleteButton.visible, - this.deleteButton.enabled + this.deleteButton.enabled, ]) ).subscribe(() => this.updateNavbarWithTabsButtons()); this.updateNavbarWithTabsButtons(); diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.html b/src/Explorer/Tabs/DatabaseSettingsTab.html index e18080bbb..620ea968c 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.html +++ b/src/Explorer/Tabs/DatabaseSettingsTab.html @@ -10,11 +10,11 @@
- Info + Info
- Warning + Warning
@@ -24,7 +24,7 @@ Scale
- Info + Info With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. To keep your account free, keep the total RU/s across all resources in the account to 400 RU/s. diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.ts b/src/Explorer/Tabs/DatabaseSettingsTab.ts index 492eba648..f995bac2f 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.ts +++ b/src/Explorer/Tabs/DatabaseSettingsTab.ts @@ -336,7 +336,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. visible: ko.computed(() => { return true; - }) + }), }; this.discardSettingsChangesButton = { @@ -356,7 +356,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. visible: ko.computed(() => { return true; - }) + }), }; this.isTemplateReady = ko.observable(false); @@ -384,7 +384,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }); const headerOptions: RequestOptions = { initialHeaders: {} }; @@ -394,7 +394,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. databaseId: this.database.id(), currentOffer: this.database.offer(), autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined, - manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput() + manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput(), }; if (this._hasProvisioningTypeChanged()) { @@ -425,7 +425,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: errorMessage, - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, startKey ); @@ -467,7 +467,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.saveSettingsButton.enabled() + disabled: !this.saveSettingsButton.enabled(), }); } @@ -480,7 +480,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.discardSettingsChangesButton.enabled() + disabled: !this.discardSettingsChangesButton.enabled(), }); } return buttons; @@ -492,7 +492,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. this.saveSettingsButton.visible, this.saveSettingsButton.enabled, this.discardSettingsChangesButton.visible, - this.discardSettingsChangesButton.enabled + this.discardSettingsChangesButton.enabled, ]) ).subscribe(() => this.updateNavbarWithTabsButtons()); this.updateNavbarWithTabsButtons(); diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html index ffd2e05db..725c9fe2b 100644 --- a/src/Explorer/Tabs/DocumentsTab.html +++ b/src/Explorer/Tabs/DocumentsTab.html @@ -1,226 +1,221 @@ -
- - -
-
-

Title

-
Text
-
-
- - -
-
-
-
- - - - -
- -
- SELECT * FROM c - - -
-
- Filter : - - No filter applied - - -
- - - -
-
-
- - SELECT * FROM c - - - - - - - - - - - - Hide filter - -
-
-
- -
- - - -
-
-
- -
- - - - - - - - - - - - - - - - - - - - -
- Refresh documents -
- -
-
-
- Load more -
- - -
-
-
-
-

Document WaterMark

-

Create new or work with existing document(s).

-
- - - -
- -
+
+ + +
+
+

Title

+
Text
+
+
+ + +
+
+
+
+ + + + +
+ +
+ SELECT * FROM c + + +
+
+ Filter : + + No filter applied + + +
+ + + +
+
+
+ SELECT * FROM c + + + + + + + + + + + Hide filter + +
+
+
+ +
+ + + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ Refresh documents +
+ +
+
+
+ Load more +
+ + +
+
+
+
+

Document WaterMark

+

Create new or work with existing document(s).

+
+ + + +
+ +
diff --git a/src/Explorer/Tabs/DocumentsTab.test.ts b/src/Explorer/Tabs/DocumentsTab.test.ts index 77b5746b3..df46d171a 100644 --- a/src/Explorer/Tabs/DocumentsTab.test.ts +++ b/src/Explorer/Tabs/DocumentsTab.test.ts @@ -1,168 +1,168 @@ -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import * as Constants from "../../Common/Constants"; -import DocumentsTab from "./DocumentsTab"; -import Explorer from "../Explorer"; -import DocumentId from "../Tree/DocumentId"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; - -describe("Documents tab", () => { - describe("buildQuery", () => { - it("should generate the right select query for SQL API", () => { - const documentsTab = new DocumentsTab({ - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - hashLocation: "", - isActive: ko.observable(false), - - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {} - }); - - expect(documentsTab.buildQuery("")).toContain("select"); - }); - }); - - describe("showPartitionKey", () => { - const explorer = new Explorer(); - - const mongoExplorer = new Explorer(); - mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB); - - const collectionWithoutPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo") - }, - container: explorer - }); - - const collectionWithSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo") - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: true - }, - container: explorer - }); - - const collectionWithNonSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo") - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: false - }, - container: explorer - }); - - const mongoCollectionWithSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo") - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: true - }, - container: mongoExplorer - }); - - it("should be false for null or undefined collection", () => { - const documentsTab = new DocumentsTab({ - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - hashLocation: "", - isActive: ko.observable(false), - - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {} - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be false for null or undefined partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithoutPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - hashLocation: "", - isActive: ko.observable(false), - - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {} - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be true for non-Mongo accounts with system partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - hashLocation: "", - isActive: ko.observable(false), - - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {} - }); - - expect(documentsTab.showPartitionKey).toBe(true); - }); - - it("should be false for Mongo accounts with system partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: mongoCollectionWithSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - hashLocation: "", - isActive: ko.observable(false), - - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {} - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be true for non-system partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithNonSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - hashLocation: "", - isActive: ko.observable(false), - - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {} - }); - - expect(documentsTab.showPartitionKey).toBe(true); - }); - }); -}); +import * as ko from "knockout"; +import * as ViewModels from "../../Contracts/ViewModels"; +import * as Constants from "../../Common/Constants"; +import DocumentsTab from "./DocumentsTab"; +import Explorer from "../Explorer"; +import DocumentId from "../Tree/DocumentId"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; + +describe("Documents tab", () => { + describe("buildQuery", () => { + it("should generate the right select query for SQL API", () => { + const documentsTab = new DocumentsTab({ + partitionKey: null, + documentIds: ko.observableArray(), + tabKind: ViewModels.CollectionTabKind.Documents, + title: "", + tabPath: "", + hashLocation: "", + isActive: ko.observable(false), + + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, + }); + + expect(documentsTab.buildQuery("")).toContain("select"); + }); + }); + + describe("showPartitionKey", () => { + const explorer = new Explorer(); + + const mongoExplorer = new Explorer(); + mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB); + + const collectionWithoutPartitionKey = ({ + id: ko.observable("foo"), + database: { + id: ko.observable("foo"), + }, + container: explorer, + }); + + const collectionWithSystemPartitionKey = ({ + id: ko.observable("foo"), + database: { + id: ko.observable("foo"), + }, + partitionKey: { + paths: ["/foo"], + kind: "Hash", + version: 2, + systemKey: true, + }, + container: explorer, + }); + + const collectionWithNonSystemPartitionKey = ({ + id: ko.observable("foo"), + database: { + id: ko.observable("foo"), + }, + partitionKey: { + paths: ["/foo"], + kind: "Hash", + version: 2, + systemKey: false, + }, + container: explorer, + }); + + const mongoCollectionWithSystemPartitionKey = ({ + id: ko.observable("foo"), + database: { + id: ko.observable("foo"), + }, + partitionKey: { + paths: ["/foo"], + kind: "Hash", + version: 2, + systemKey: true, + }, + container: mongoExplorer, + }); + + it("should be false for null or undefined collection", () => { + const documentsTab = new DocumentsTab({ + partitionKey: null, + documentIds: ko.observableArray(), + tabKind: ViewModels.CollectionTabKind.Documents, + title: "", + tabPath: "", + hashLocation: "", + isActive: ko.observable(false), + + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, + }); + + expect(documentsTab.showPartitionKey).toBe(false); + }); + + it("should be false for null or undefined partitionKey", () => { + const documentsTab = new DocumentsTab({ + collection: collectionWithoutPartitionKey, + partitionKey: null, + documentIds: ko.observableArray(), + tabKind: ViewModels.CollectionTabKind.Documents, + title: "", + tabPath: "", + hashLocation: "", + isActive: ko.observable(false), + + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, + }); + + expect(documentsTab.showPartitionKey).toBe(false); + }); + + it("should be true for non-Mongo accounts with system partitionKey", () => { + const documentsTab = new DocumentsTab({ + collection: collectionWithSystemPartitionKey, + partitionKey: null, + documentIds: ko.observableArray(), + tabKind: ViewModels.CollectionTabKind.Documents, + title: "", + tabPath: "", + hashLocation: "", + isActive: ko.observable(false), + + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, + }); + + expect(documentsTab.showPartitionKey).toBe(true); + }); + + it("should be false for Mongo accounts with system partitionKey", () => { + const documentsTab = new DocumentsTab({ + collection: mongoCollectionWithSystemPartitionKey, + partitionKey: null, + documentIds: ko.observableArray(), + tabKind: ViewModels.CollectionTabKind.Documents, + title: "", + tabPath: "", + hashLocation: "", + isActive: ko.observable(false), + + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, + }); + + expect(documentsTab.showPartitionKey).toBe(false); + }); + + it("should be true for non-system partitionKey", () => { + const documentsTab = new DocumentsTab({ + collection: collectionWithNonSystemPartitionKey, + partitionKey: null, + documentIds: ko.observableArray(), + tabKind: ViewModels.CollectionTabKind.Documents, + title: "", + tabPath: "", + hashLocation: "", + isActive: ko.observable(false), + + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, + }); + + expect(documentsTab.showPartitionKey).toBe(true); + }); + }); +}); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index feec01aa2..186f6a813 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -1,962 +1,959 @@ -import * as ko from "knockout"; -import Q from "q"; -import * as Constants from "../../Common/Constants"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; -import { KeyCodes } from "../../Common/Constants"; -import DocumentId from "../Tree/DocumentId"; -import editable from "../../Common/EditableUtility"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -import TabsBase from "./TabsBase"; -import { DocumentsGridMetrics } from "../../Common/Constants"; -import { QueryUtils } from "../../Utils/QueryUtils"; -import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import NewDocumentIcon from "../../../images/NewDocument.svg"; -import SaveIcon from "../../../images/save-cosmos.svg"; -import DiscardIcon from "../../../images/discard.svg"; -import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; -import UploadIcon from "../../../images/Upload_16x16.svg"; -import { - extractPartitionKey, - PartitionKeyDefinition, - QueryIterator, - ItemDefinition, - Resource, - Item -} from "@azure/cosmos"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import Explorer from "../Explorer"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; -import { readDocument } from "../../Common/dataAccess/readDocument"; -import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; -import { updateDocument } from "../../Common/dataAccess/updateDocument"; -import { createDocument } from "../../Common/dataAccess/createDocument"; - -export default class DocumentsTab extends TabsBase { - public selectedDocumentId: ko.Observable; - public selectedDocumentContent: ViewModels.Editable; - public initialDocumentContent: ko.Observable; - public documentContentsGridId: string; - public documentContentsContainerId: string; - public filterContent: ko.Observable; - public appliedFilter: ko.Observable; - public lastFilterContents: ko.ObservableArray; - public isFilterExpanded: ko.Observable; - public isFilterCreated: ko.Observable; - public applyFilterButton: ViewModels.Button; - public isEditorDirty: ko.Computed; - public editorState: ko.Observable; - public newDocumentButton: ViewModels.Button; - public saveNewDocumentButton: ViewModels.Button; - public saveExisitingDocumentButton: ViewModels.Button; - public discardNewDocumentChangesButton: ViewModels.Button; - public discardExisitingDocumentChangesButton: ViewModels.Button; - public deleteExisitingDocumentButton: ViewModels.Button; - public displayedError: ko.Observable; - public accessibleDocumentList: AccessibleVerticalList; - public dataContentsGridScrollHeight: ko.Observable; - public isPreferredApiMongoDB: boolean; - public shouldShowEditor: ko.Computed; - public splitter: Splitter; - public showPartitionKey: boolean; - public idHeader: string; - - // TODO need to refactor - public partitionKey: DataModels.PartitionKey; - public partitionKeyPropertyHeader: string; - public partitionKeyProperty: string; - public documentIds: ko.ObservableArray; - - private _documentsIterator: QueryIterator; - private _resourceTokenPartitionKey: string; - - constructor(options: ViewModels.DocumentsTabOptions) { - super(options); - this.isPreferredApiMongoDB = !!this.collection - ? this.collection.container.isPreferredApiMongoDB() - : options.isPreferredApiMongoDB; - - this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; - - this.documentContentsGridId = `documentContentsGrid${this.tabId}`; - this.documentContentsContainerId = `documentContentsContainer${this.tabId}`; - this.editorState = ko.observable( - ViewModels.DocumentExplorerState.noDocumentSelected - ); - this.selectedDocumentId = ko.observable(); - this.selectedDocumentContent = editable.observable(""); - this.initialDocumentContent = ko.observable(""); - this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey); - this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; - this.documentIds = options.documentIds; - - this.partitionKeyPropertyHeader = - (this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader(); - this.partitionKeyProperty = !!this.partitionKeyPropertyHeader - ? this.partitionKeyPropertyHeader - .replace(/[/]+/g, ".") - .substr(1) - .replace(/[']+/g, "") - : null; - - this.isFilterExpanded = ko.observable(false); - this.isFilterCreated = ko.observable(true); - this.filterContent = ko.observable(""); - this.appliedFilter = ko.observable(""); - this.displayedError = ko.observable(""); - this.lastFilterContents = ko.observableArray([ - 'WHERE c.id = "foo"', - "ORDER BY c._ts DESC", - 'WHERE c.id = "foo" ORDER BY c._ts DESC' - ]); - - this.dataContentsGridScrollHeight = ko.observable(null); - - // initialize splitter only after template has been loaded so dom elements are accessible - super.onTemplateReady((isTemplateReady: boolean) => { - if (isTemplateReady) { - const tabContainer: HTMLElement = document.getElementById("content"); - const splitterBounds: SplitterBounds = { - min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth, - max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth - }; - this.splitter = new Splitter({ - splitterId: "h_splitter2", - leftId: this.documentContentsContainerId, - bounds: splitterBounds, - direction: SplitterDirection.Vertical - }); - } - }); - - this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds()); - this.accessibleDocumentList.setOnSelect( - (selectedDocument: DocumentId) => selectedDocument && selectedDocument.click() - ); - this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) => - this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId) - ); - this.documentIds.subscribe((newDocuments: DocumentId[]) => { - this.accessibleDocumentList.updateItemList(newDocuments); - if (newDocuments.length > 0) { - this.dataContentsGridScrollHeight( - newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px" - ); - } else { - this.dataContentsGridScrollHeight( - DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px" - ); - } - }); - - this.isEditorDirty = ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - return false; - - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - return true; - - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return ( - this.selectedDocumentContent.getEditableOriginalValue() !== - this.selectedDocumentContent.getEditableCurrentValue() - ); - - default: - return false; - } - }); - - this.newDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - return true; - }) - }; - - this.saveNewDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }) - }; - - this.discardNewDocumentChangesButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }) - }; - - this.saveExisitingDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }) - }; - - this.discardExisitingDocumentChangesButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }) - }; - - this.deleteExisitingDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }) - }; - - this.applyFilterButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }) - }; - this.buildCommandBarOptions(); - this.shouldShowEditor = ko.computed(() => { - const documentHasContent: boolean = - this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0; - const isNewDocument: boolean = - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid; - - return documentHasContent || isNewDocument; - }); - this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent)); - - this.showPartitionKey = this._shouldShowPartitionKey(); - } - - private _shouldShowPartitionKey(): boolean { - if (!this.collection) { - return false; - } - - if (!this.collection.partitionKey) { - return false; - } - - if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) { - return false; - } - - return true; - } - - public onShowFilterClick(): Q.Promise { - this.isFilterCreated(true); - this.isFilterExpanded(true); - - $(".filterDocExpanded").addClass("active"); - $("#content").addClass("active"); - $(".querydropdown").focus(); - - return Q(); - } - - public onHideFilterClick(): Q.Promise { - this.isFilterExpanded(false); - - $(".filterDocExpanded").removeClass("active"); - $("#content").removeClass("active"); - $(".queryButton").focus(); - return Q(); - } - - public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.onHideFilterClick(); - event.stopPropagation(); - return false; - } - return true; - }; - - public async refreshDocumentsGrid(): Promise { - // clear documents grid - this.documentIds([]); - - try { - // reset iterator - this._documentsIterator = this.createIterator(); - // load documents - await this.loadNextPage(); - // collapse filter - this.appliedFilter(this.filterContent()); - this.isFilterExpanded(false); - document.getElementById("errorStatusIcon")?.focus(); - } catch (error) { - window.alert(getErrorMessage(error)); - } - } - - public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.refreshDocumentsGrid(); - event.stopPropagation(); - return false; - } - return true; - }; - - public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise { - if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) { - return Q(); - } - - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - - return Q(); - } - - public onNewDocumentClick = (): Q.Promise => { - if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return Q(); - } - this.selectedDocumentId(null); - - const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); - this.initialDocumentContent(defaultDocument); - this.selectedDocumentContent.setBaseline(defaultDocument); - this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - - return Q(); - }; - - public onSaveNewDocumentClick = (): Promise => { - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }); - const document = JSON.parse(this.selectedDocumentContent()); - this.isExecuting(true); - return createDocument(this.collection, document) - .then( - (savedDocument: any) => { - const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - this.initialDocumentContent(value); - const partitionKeyValueArray = extractPartitionKey( - savedDocument, - this.partitionKey as PartitionKeyDefinition - ); - const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0]; - let id = new DocumentId(this, savedDocument, partitionKeyValue); - let ids = this.documentIds(); - ids.push(id); - - this.selectedDocumentId(id); - this.documentIds(ids); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.CreateDocument, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - }, - error => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - window.alert(errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - }; - - public onRevertNewDocumentClick = (): Q.Promise => { - this.initialDocumentContent(""); - this.selectedDocumentContent(""); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - - return Q(); - }; - - public onSaveExisitingDocumentClick = (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const documentContent = JSON.parse(this.selectedDocumentContent()); - - const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition); - const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0]; - - selectedDocumentId.partitionKeyValue = partitionKeyValue; - - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }); - this.isExecuting(true); - return updateDocument(this.collection, selectedDocumentId, documentContent) - .then( - (updatedDocument: any) => { - const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - this.initialDocumentContent(value); - this.documentIds().forEach((documentId: DocumentId) => { - if (documentId.rid === updatedDocument._rid) { - documentId.id(updatedDocument.id); - } - }); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.UpdateDocument, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - }, - error => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - window.alert(errorMessage); - TelemetryProcessor.traceFailure( - Action.UpdateDocument, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - }; - - public onRevertExisitingDocumentClick = (): Q.Promise => { - this.selectedDocumentContent.setBaseline(this.initialDocumentContent()); - this.initialDocumentContent.valueHasMutated(); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - - return Q(); - }; - - public onDeleteExisitingDocumentClick = async (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const msg = !this.isPreferredApiMongoDB - ? "Are you sure you want to delete the selected item ?" - : "Are you sure you want to delete the selected document ?"; - - if (window.confirm(msg)) { - await this._deleteDocument(selectedDocumentId); - } - }; - - public onValidDocumentEdit(): Q.Promise { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid - ) { - this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - return Q(); - } - - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); - return Q(); - } - - public onInvalidDocumentEdit(): Q.Promise { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid - ) { - this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid); - return Q(); - } - - if ( - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid - ) { - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); - return Q(); - } - - return Q(); - } - - public onTabClick(): void { - super.onTabClick(); - this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); - } - - public async onActivate(): Promise { - super.onActivate(); - - if (!this._documentsIterator) { - try { - this._documentsIterator = this.createIterator(); - await this.loadNextPage(); - } catch (error) { - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseAccountName: this.collection.container.databaseAccount().name, - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - defaultExperience: this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - this.onLoadStartKey - ); - this.onLoadStartKey = null; - } - } - } - } - - private _isIgnoreDirtyEditor = (): boolean => { - var msg: string = "Changes will be lost. Do you want to continue?"; - return window.confirm(msg); - }; - - protected __deleteDocument(documentId: DocumentId): Promise { - return deleteDocument(this.collection, documentId); - } - - private _deleteDocument(selectedDocumentId: DocumentId): Promise { - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }); - this.isExecuting(true); - return this.__deleteDocument(selectedDocumentId) - .then( - () => { - this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); - this.selectedDocumentContent(""); - this.selectedDocumentId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - TelemetryProcessor.traceSuccess( - Action.DeleteDocument, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - }, - error => { - this.isExecutionError(true); - console.error(error); - TelemetryProcessor.traceFailure( - Action.DeleteDocument, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - } - - public createIterator(): QueryIterator { - let filters = this.lastFilterContents(); - const filter: string = this.filterContent().trim(); - const query: string = this.buildQuery(filter); - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - - if (this._resourceTokenPartitionKey) { - options.partitionKey = this._resourceTokenPartitionKey; - } - - return queryDocuments(this.collection.databaseId, this.collection.id(), query, options); - } - - public async selectDocument(documentId: DocumentId): Promise { - this.selectedDocumentId(documentId); - const content = await readDocument(this.collection, documentId); - this.initDocumentEditor(documentId, content); - } - - public loadNextPage(): Q.Promise { - this.isExecuting(true); - this.isExecutionError(false); - return this._loadNextPageInternal() - .then( - (documentsIdsResponse = []) => { - const currentDocuments = this.documentIds(); - const currentDocumentsRids = currentDocuments.map(currentDocument => currentDocument.rid); - const nextDocumentIds = documentsIdsResponse - // filter documents already loaded in observable - .filter((d: any) => { - return currentDocumentsRids.indexOf(d._rid) < 0; - }) - // map raw response to view model - .map((rawDocument: any) => { - const partitionKeyValue = rawDocument._partitionKeyValue; - return new DocumentId(this, rawDocument, partitionKeyValue); - }); - - const merged = currentDocuments.concat(nextDocumentIds); - this.documentIds(merged); - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseAccountName: this.collection.container.databaseAccount().name, - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - defaultExperience: this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - this.onLoadStartKey - ); - this.onLoadStartKey = null; - } - }, - error => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseAccountName: this.collection.container.databaseAccount().name, - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - defaultExperience: this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - this.onLoadStartKey - ); - this.onLoadStartKey = null; - } - } - ) - .finally(() => this.isExecuting(false)); - } - - public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => { - if (event.key === " " || event.key === "Enter") { - const focusElement = document.getElementById(this.documentContentsGridId); - this.loadNextPage(); - focusElement && focusElement.focus(); - event.stopPropagation(); - event.preventDefault(); - } - }; - - protected _loadNextPageInternal(): Q.Promise { - return Q(this._documentsIterator.fetchNext().then(response => response.resources)); - } - - protected _onEditorContentChange(newContent: string) { - try { - let parsed: any = JSON.parse(newContent); - this.onValidDocumentEdit(); - } catch (e) { - this.onInvalidDocumentEdit(); - } - } - - public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise { - if (documentId) { - const content: string = this.renderObjectForEditor(documentContent, null, 4); - this.selectedDocumentContent.setBaseline(content); - this.initialDocumentContent(content); - const newState = documentId - ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits - : ViewModels.DocumentExplorerState.newDocumentValid; - this.editorState(newState); - } - - return Q(); - } - - public buildQuery(filter: string): string { - return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey); - } - - protected getTabsButtons(): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = []; - const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; - if (this.newDocumentButton.visible()) { - buttons.push({ - iconSrc: NewDocumentIcon, - iconAlt: label, - onCommandClick: this.onNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.newDocumentButton.enabled() - }); - } - - if (this.saveNewDocumentButton.visible()) { - const label = "Save"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - onCommandClick: this.onSaveNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.saveNewDocumentButton.enabled() - }); - } - - if (this.discardNewDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - onCommandClick: this.onRevertNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.discardNewDocumentChangesButton.enabled() - }); - } - - if (this.saveExisitingDocumentButton.visible()) { - const label = "Update"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - onCommandClick: this.onSaveExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.saveExisitingDocumentButton.enabled() - }); - } - - if (this.discardExisitingDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - onCommandClick: this.onRevertExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.discardExisitingDocumentChangesButton.enabled() - }); - } - - if (this.deleteExisitingDocumentButton.visible()) { - const label = "Delete"; - buttons.push({ - iconSrc: DeleteDocumentIcon, - iconAlt: label, - onCommandClick: this.onDeleteExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.deleteExisitingDocumentButton.enabled() - }); - } - - if (!this.isPreferredApiMongoDB) { - buttons.push(DocumentsTab._createUploadButton(this.collection.container)); - } - - const features = this.collection.container.features() || {}; - - return buttons; - } - - protected buildCommandBarOptions(): void { - ko.computed(() => - ko.toJSON([ - this.newDocumentButton.visible, - this.newDocumentButton.enabled, - this.saveNewDocumentButton.visible, - this.saveNewDocumentButton.enabled, - this.discardNewDocumentChangesButton.visible, - this.discardNewDocumentChangesButton.enabled, - this.saveExisitingDocumentButton.visible, - this.saveExisitingDocumentButton.enabled, - this.discardExisitingDocumentChangesButton.visible, - this.discardExisitingDocumentChangesButton.enabled, - this.deleteExisitingDocumentButton.visible, - this.deleteExisitingDocumentButton.enabled - ]) - ).subscribe(() => this.updateNavbarWithTabsButtons()); - this.updateNavbarWithTabsButtons(); - } - - private _getPartitionKeyPropertyHeader(): string { - return ( - (this.partitionKey && - this.partitionKey.paths && - this.partitionKey.paths.length > 0 && - this.partitionKey.paths[0]) || - null - ); - } - - public static _createUploadButton(container: Explorer): CommandButtonComponentProps { - const label = "Upload Item"; - return { - iconSrc: UploadIcon, - iconAlt: label, - onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - const focusElement = document.getElementById("itemImportLink"); - const uploadItemsPane = container.isRightPanelV2Enabled() - ? container.uploadItemsPaneAdapter - : container.uploadItemsPane; - selectedCollection && uploadItemsPane.open(); - focusElement && focusElement.focus(); - }, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected() - }; - } -} +import * as ko from "knockout"; +import Q from "q"; +import * as Constants from "../../Common/Constants"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; +import { KeyCodes } from "../../Common/Constants"; +import DocumentId from "../Tree/DocumentId"; +import editable from "../../Common/EditableUtility"; +import * as HeadersUtility from "../../Common/HeadersUtility"; +import TabsBase from "./TabsBase"; +import { DocumentsGridMetrics } from "../../Common/Constants"; +import { QueryUtils } from "../../Utils/QueryUtils"; +import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import NewDocumentIcon from "../../../images/NewDocument.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import DiscardIcon from "../../../images/discard.svg"; +import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; +import UploadIcon from "../../../images/Upload_16x16.svg"; +import { + extractPartitionKey, + PartitionKeyDefinition, + QueryIterator, + ItemDefinition, + Resource, + Item, +} from "@azure/cosmos"; +import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import Explorer from "../Explorer"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; +import { readDocument } from "../../Common/dataAccess/readDocument"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; +import { createDocument } from "../../Common/dataAccess/createDocument"; + +export default class DocumentsTab extends TabsBase { + public selectedDocumentId: ko.Observable; + public selectedDocumentContent: ViewModels.Editable; + public initialDocumentContent: ko.Observable; + public documentContentsGridId: string; + public documentContentsContainerId: string; + public filterContent: ko.Observable; + public appliedFilter: ko.Observable; + public lastFilterContents: ko.ObservableArray; + public isFilterExpanded: ko.Observable; + public isFilterCreated: ko.Observable; + public applyFilterButton: ViewModels.Button; + public isEditorDirty: ko.Computed; + public editorState: ko.Observable; + public newDocumentButton: ViewModels.Button; + public saveNewDocumentButton: ViewModels.Button; + public saveExisitingDocumentButton: ViewModels.Button; + public discardNewDocumentChangesButton: ViewModels.Button; + public discardExisitingDocumentChangesButton: ViewModels.Button; + public deleteExisitingDocumentButton: ViewModels.Button; + public displayedError: ko.Observable; + public accessibleDocumentList: AccessibleVerticalList; + public dataContentsGridScrollHeight: ko.Observable; + public isPreferredApiMongoDB: boolean; + public shouldShowEditor: ko.Computed; + public splitter: Splitter; + public showPartitionKey: boolean; + public idHeader: string; + + // TODO need to refactor + public partitionKey: DataModels.PartitionKey; + public partitionKeyPropertyHeader: string; + public partitionKeyProperty: string; + public documentIds: ko.ObservableArray; + + private _documentsIterator: QueryIterator; + private _resourceTokenPartitionKey: string; + + constructor(options: ViewModels.DocumentsTabOptions) { + super(options); + this.isPreferredApiMongoDB = !!this.collection + ? this.collection.container.isPreferredApiMongoDB() + : options.isPreferredApiMongoDB; + + this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; + + this.documentContentsGridId = `documentContentsGrid${this.tabId}`; + this.documentContentsContainerId = `documentContentsContainer${this.tabId}`; + this.editorState = ko.observable( + ViewModels.DocumentExplorerState.noDocumentSelected + ); + this.selectedDocumentId = ko.observable(); + this.selectedDocumentContent = editable.observable(""); + this.initialDocumentContent = ko.observable(""); + this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey); + this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; + this.documentIds = options.documentIds; + + this.partitionKeyPropertyHeader = + (this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader(); + this.partitionKeyProperty = !!this.partitionKeyPropertyHeader + ? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "") + : null; + + this.isFilterExpanded = ko.observable(false); + this.isFilterCreated = ko.observable(true); + this.filterContent = ko.observable(""); + this.appliedFilter = ko.observable(""); + this.displayedError = ko.observable(""); + this.lastFilterContents = ko.observableArray([ + 'WHERE c.id = "foo"', + "ORDER BY c._ts DESC", + 'WHERE c.id = "foo" ORDER BY c._ts DESC', + ]); + + this.dataContentsGridScrollHeight = ko.observable(null); + + // initialize splitter only after template has been loaded so dom elements are accessible + super.onTemplateReady((isTemplateReady: boolean) => { + if (isTemplateReady) { + const tabContainer: HTMLElement = document.getElementById("content"); + const splitterBounds: SplitterBounds = { + min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth, + max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth, + }; + this.splitter = new Splitter({ + splitterId: "h_splitter2", + leftId: this.documentContentsContainerId, + bounds: splitterBounds, + direction: SplitterDirection.Vertical, + }); + } + }); + + this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds()); + this.accessibleDocumentList.setOnSelect( + (selectedDocument: DocumentId) => selectedDocument && selectedDocument.click() + ); + this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) => + this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId) + ); + this.documentIds.subscribe((newDocuments: DocumentId[]) => { + this.accessibleDocumentList.updateItemList(newDocuments); + if (newDocuments.length > 0) { + this.dataContentsGridScrollHeight( + newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px" + ); + } else { + this.dataContentsGridScrollHeight( + DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px" + ); + } + }); + + this.isEditorDirty = ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.noDocumentSelected: + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + return false; + + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + return true; + + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return ( + this.selectedDocumentContent.getEditableOriginalValue() !== + this.selectedDocumentContent.getEditableCurrentValue() + ); + + default: + return false; + } + }); + + this.newDocumentButton = { + enabled: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.noDocumentSelected: + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + return true; + } + + return false; + }), + + visible: ko.computed(() => { + return true; + }), + }; + + this.saveNewDocumentButton = { + enabled: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.newDocumentValid: + return true; + } + + return false; + }), + + visible: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + return true; + } + + return false; + }), + }; + + this.discardNewDocumentChangesButton = { + enabled: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + return true; + } + + return false; + }), + + visible: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + return true; + } + + return false; + }), + }; + + this.saveExisitingDocumentButton = { + enabled: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + } + + return false; + }), + + visible: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + } + + return false; + }), + }; + + this.discardExisitingDocumentChangesButton = { + enabled: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + } + + return false; + }), + + visible: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + } + + return false; + }), + }; + + this.deleteExisitingDocumentButton = { + enabled: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + } + + return false; + }), + + visible: ko.computed(() => { + switch (this.editorState()) { + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + } + + return false; + }), + }; + + this.applyFilterButton = { + enabled: ko.computed(() => { + return true; + }), + + visible: ko.computed(() => { + return true; + }), + }; + this.buildCommandBarOptions(); + this.shouldShowEditor = ko.computed(() => { + const documentHasContent: boolean = + this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0; + const isNewDocument: boolean = + this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid || + this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid; + + return documentHasContent || isNewDocument; + }); + this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent)); + + this.showPartitionKey = this._shouldShowPartitionKey(); + } + + private _shouldShowPartitionKey(): boolean { + if (!this.collection) { + return false; + } + + if (!this.collection.partitionKey) { + return false; + } + + if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) { + return false; + } + + return true; + } + + public onShowFilterClick(): Q.Promise { + this.isFilterCreated(true); + this.isFilterExpanded(true); + + $(".filterDocExpanded").addClass("active"); + $("#content").addClass("active"); + $(".querydropdown").focus(); + + return Q(); + } + + public onHideFilterClick(): Q.Promise { + this.isFilterExpanded(false); + + $(".filterDocExpanded").removeClass("active"); + $("#content").removeClass("active"); + $(".queryButton").focus(); + return Q(); + } + + public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.onHideFilterClick(); + event.stopPropagation(); + return false; + } + return true; + }; + + public async refreshDocumentsGrid(): Promise { + // clear documents grid + this.documentIds([]); + + try { + // reset iterator + this._documentsIterator = this.createIterator(); + // load documents + await this.loadNextPage(); + // collapse filter + this.appliedFilter(this.filterContent()); + this.isFilterExpanded(false); + document.getElementById("errorStatusIcon")?.focus(); + } catch (error) { + window.alert(getErrorMessage(error)); + } + } + + public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + this.refreshDocumentsGrid(); + event.stopPropagation(); + return false; + } + return true; + }; + + public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise { + if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) { + return Q(); + } + + this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + + return Q(); + } + + public onNewDocumentClick = (): Q.Promise => { + if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { + return Q(); + } + this.selectedDocumentId(null); + + const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); + this.initialDocumentContent(defaultDocument); + this.selectedDocumentContent.setBaseline(defaultDocument); + this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); + + return Q(); + }; + + public onSaveNewDocumentClick = (): Promise => { + this.isExecutionError(false); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }); + const document = JSON.parse(this.selectedDocumentContent()); + this.isExecuting(true); + return createDocument(this.collection, document) + .then( + (savedDocument: any) => { + const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4); + this.selectedDocumentContent.setBaseline(value); + this.initialDocumentContent(value); + const partitionKeyValueArray = extractPartitionKey( + savedDocument, + this.partitionKey as PartitionKeyDefinition + ); + const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0]; + let id = new DocumentId(this, savedDocument, partitionKeyValue); + let ids = this.documentIds(); + ids.push(id); + + this.selectedDocumentId(id); + this.documentIds(ids); + this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + TelemetryProcessor.traceSuccess( + Action.CreateDocument, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }, + startKey + ); + }, + (error) => { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateDocument, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + } + ) + .finally(() => this.isExecuting(false)); + }; + + public onRevertNewDocumentClick = (): Q.Promise => { + this.initialDocumentContent(""); + this.selectedDocumentContent(""); + this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); + + return Q(); + }; + + public onSaveExisitingDocumentClick = (): Promise => { + const selectedDocumentId = this.selectedDocumentId(); + const documentContent = JSON.parse(this.selectedDocumentContent()); + + const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition); + const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0]; + + selectedDocumentId.partitionKeyValue = partitionKeyValue; + + this.isExecutionError(false); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }); + this.isExecuting(true); + return updateDocument(this.collection, selectedDocumentId, documentContent) + .then( + (updatedDocument: any) => { + const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); + this.selectedDocumentContent.setBaseline(value); + this.initialDocumentContent(value); + this.documentIds().forEach((documentId: DocumentId) => { + if (documentId.rid === updatedDocument._rid) { + documentId.id(updatedDocument.id); + } + }); + this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + TelemetryProcessor.traceSuccess( + Action.UpdateDocument, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }, + startKey + ); + }, + (error) => { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.UpdateDocument, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + } + ) + .finally(() => this.isExecuting(false)); + }; + + public onRevertExisitingDocumentClick = (): Q.Promise => { + this.selectedDocumentContent.setBaseline(this.initialDocumentContent()); + this.initialDocumentContent.valueHasMutated(); + this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + + return Q(); + }; + + public onDeleteExisitingDocumentClick = async (): Promise => { + const selectedDocumentId = this.selectedDocumentId(); + const msg = !this.isPreferredApiMongoDB + ? "Are you sure you want to delete the selected item ?" + : "Are you sure you want to delete the selected document ?"; + + if (window.confirm(msg)) { + await this._deleteDocument(selectedDocumentId); + } + }; + + public onValidDocumentEdit(): Q.Promise { + if ( + this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || + this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid + ) { + this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); + return Q(); + } + + this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); + return Q(); + } + + public onInvalidDocumentEdit(): Q.Promise { + if ( + this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || + this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid + ) { + this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid); + return Q(); + } + + if ( + this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || + this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid + ) { + this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); + return Q(); + } + + return Q(); + } + + public onTabClick(): void { + super.onTabClick(); + this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); + } + + public async onActivate(): Promise { + super.onActivate(); + + if (!this._documentsIterator) { + try { + this._documentsIterator = this.createIterator(); + await this.loadNextPage(); + } catch (error) { + if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseAccountName: this.collection.container.databaseAccount().name, + databaseName: this.collection.databaseId, + collectionName: this.collection.id(), + defaultExperience: this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + this.onLoadStartKey + ); + this.onLoadStartKey = null; + } + } + } + } + + private _isIgnoreDirtyEditor = (): boolean => { + var msg: string = "Changes will be lost. Do you want to continue?"; + return window.confirm(msg); + }; + + protected __deleteDocument(documentId: DocumentId): Promise { + return deleteDocument(this.collection, documentId); + } + + private _deleteDocument(selectedDocumentId: DocumentId): Promise { + this.isExecutionError(false); + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }); + this.isExecuting(true); + return this.__deleteDocument(selectedDocumentId) + .then( + () => { + this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); + this.selectedDocumentContent(""); + this.selectedDocumentId(null); + this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); + TelemetryProcessor.traceSuccess( + Action.DeleteDocument, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }, + startKey + ); + }, + (error) => { + this.isExecutionError(true); + console.error(error); + TelemetryProcessor.traceFailure( + Action.DeleteDocument, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + ) + .finally(() => this.isExecuting(false)); + } + + public createIterator(): QueryIterator { + let filters = this.lastFilterContents(); + const filter: string = this.filterContent().trim(); + const query: string = this.buildQuery(filter); + let options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + + if (this._resourceTokenPartitionKey) { + options.partitionKey = this._resourceTokenPartitionKey; + } + + return queryDocuments(this.collection.databaseId, this.collection.id(), query, options); + } + + public async selectDocument(documentId: DocumentId): Promise { + this.selectedDocumentId(documentId); + const content = await readDocument(this.collection, documentId); + this.initDocumentEditor(documentId, content); + } + + public loadNextPage(): Q.Promise { + this.isExecuting(true); + this.isExecutionError(false); + return this._loadNextPageInternal() + .then( + (documentsIdsResponse = []) => { + const currentDocuments = this.documentIds(); + const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); + const nextDocumentIds = documentsIdsResponse + // filter documents already loaded in observable + .filter((d: any) => { + return currentDocumentsRids.indexOf(d._rid) < 0; + }) + // map raw response to view model + .map((rawDocument: any) => { + const partitionKeyValue = rawDocument._partitionKeyValue; + return new DocumentId(this, rawDocument, partitionKeyValue); + }); + + const merged = currentDocuments.concat(nextDocumentIds); + this.documentIds(merged); + if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { + TelemetryProcessor.traceSuccess( + Action.Tab, + { + databaseAccountName: this.collection.container.databaseAccount().name, + databaseName: this.collection.databaseId, + collectionName: this.collection.id(), + defaultExperience: this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }, + this.onLoadStartKey + ); + this.onLoadStartKey = null; + } + }, + (error) => { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); + if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseAccountName: this.collection.container.databaseAccount().name, + databaseName: this.collection.databaseId, + collectionName: this.collection.id(), + defaultExperience: this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + this.onLoadStartKey + ); + this.onLoadStartKey = null; + } + } + ) + .finally(() => this.isExecuting(false)); + } + + public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => { + if (event.key === " " || event.key === "Enter") { + const focusElement = document.getElementById(this.documentContentsGridId); + this.loadNextPage(); + focusElement && focusElement.focus(); + event.stopPropagation(); + event.preventDefault(); + } + }; + + protected _loadNextPageInternal(): Q.Promise { + return Q(this._documentsIterator.fetchNext().then((response) => response.resources)); + } + + protected _onEditorContentChange(newContent: string) { + try { + let parsed: any = JSON.parse(newContent); + this.onValidDocumentEdit(); + } catch (e) { + this.onInvalidDocumentEdit(); + } + } + + public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise { + if (documentId) { + const content: string = this.renderObjectForEditor(documentContent, null, 4); + this.selectedDocumentContent.setBaseline(content); + this.initialDocumentContent(content); + const newState = documentId + ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits + : ViewModels.DocumentExplorerState.newDocumentValid; + this.editorState(newState); + } + + return Q(); + } + + public buildQuery(filter: string): string { + return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey); + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; + if (this.newDocumentButton.visible()) { + buttons.push({ + iconSrc: NewDocumentIcon, + iconAlt: label, + onCommandClick: this.onNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.newDocumentButton.enabled(), + }); + } + + if (this.saveNewDocumentButton.visible()) { + const label = "Save"; + buttons.push({ + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveNewDocumentButton.enabled(), + }); + } + + if (this.discardNewDocumentChangesButton.visible()) { + const label = "Discard"; + buttons.push({ + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onRevertNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardNewDocumentChangesButton.enabled(), + }); + } + + if (this.saveExisitingDocumentButton.visible()) { + const label = "Update"; + buttons.push({ + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveExisitingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveExisitingDocumentButton.enabled(), + }); + } + + if (this.discardExisitingDocumentChangesButton.visible()) { + const label = "Discard"; + buttons.push({ + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onRevertExisitingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardExisitingDocumentChangesButton.enabled(), + }); + } + + if (this.deleteExisitingDocumentButton.visible()) { + const label = "Delete"; + buttons.push({ + iconSrc: DeleteDocumentIcon, + iconAlt: label, + onCommandClick: this.onDeleteExisitingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.deleteExisitingDocumentButton.enabled(), + }); + } + + if (!this.isPreferredApiMongoDB) { + buttons.push(DocumentsTab._createUploadButton(this.collection.container)); + } + + const features = this.collection.container.features() || {}; + + return buttons; + } + + protected buildCommandBarOptions(): void { + ko.computed(() => + ko.toJSON([ + this.newDocumentButton.visible, + this.newDocumentButton.enabled, + this.saveNewDocumentButton.visible, + this.saveNewDocumentButton.enabled, + this.discardNewDocumentChangesButton.visible, + this.discardNewDocumentChangesButton.enabled, + this.saveExisitingDocumentButton.visible, + this.saveExisitingDocumentButton.enabled, + this.discardExisitingDocumentChangesButton.visible, + this.discardExisitingDocumentChangesButton.enabled, + this.deleteExisitingDocumentButton.visible, + this.deleteExisitingDocumentButton.enabled, + ]) + ).subscribe(() => this.updateNavbarWithTabsButtons()); + this.updateNavbarWithTabsButtons(); + } + + private _getPartitionKeyPropertyHeader(): string { + return ( + (this.partitionKey && + this.partitionKey.paths && + this.partitionKey.paths.length > 0 && + this.partitionKey.paths[0]) || + null + ); + } + + public static _createUploadButton(container: Explorer): CommandButtonComponentProps { + const label = "Upload Item"; + return { + iconSrc: UploadIcon, + iconAlt: label, + onCommandClick: () => { + const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const focusElement = document.getElementById("itemImportLink"); + const uploadItemsPane = container.isRightPanelV2Enabled() + ? container.uploadItemsPaneAdapter + : container.uploadItemsPane; + selectedCollection && uploadItemsPane.open(); + focusElement && focusElement.focus(); + }, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: container.isDatabaseNodeOrNoneSelected(), + }; + } +} diff --git a/src/Explorer/Tabs/GalleryTab.tsx b/src/Explorer/Tabs/GalleryTab.tsx index 2d9768021..2adea0c12 100644 --- a/src/Explorer/Tabs/GalleryTab.tsx +++ b/src/Explorer/Tabs/GalleryTab.tsx @@ -35,7 +35,7 @@ export default class GalleryTab extends TabsBase { isFavorite: options.isFavorite, selectedTab: GalleryViewerTab.OfficialSamples, sortBy: SortBy.MostViewed, - searchText: undefined + searchText: undefined, }; this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(props); diff --git a/src/Explorer/Tabs/GraphTab.ts b/src/Explorer/Tabs/GraphTab.ts index 35136a63e..ba1e2f5ad 100644 --- a/src/Explorer/Tabs/GraphTab.ts +++ b/src/Explorer/Tabs/GraphTab.ts @@ -1,241 +1,241 @@ -import * as ko from "knockout"; -import * as Q from "q"; -import * as ViewModels from "../../Contracts/ViewModels"; -import TabsBase from "./TabsBase"; -import { GraphExplorerAdapter } from "../Graph/GraphExplorerComponent/GraphExplorerAdapter"; -import { GraphAccessor, GraphExplorerError } from "../Graph/GraphExplorerComponent/GraphExplorer"; -import NewVertexIcon from "../../../images/NewVertex.svg"; -import StyleIcon from "../../../images/Style.svg"; -import GraphStylingPane from "../Panes/GraphStylingPane"; -import NewVertexPane from "../Panes/NewVertexPane"; -import { DatabaseAccount } from "../../Contracts/DataModels"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; - -export interface GraphIconMap { - [key: string]: { data: string; format: string }; -} - -export interface GraphConfig { - nodeColor: ko.Observable; - nodeColorKey: ko.Observable; // map property to node color. Takes precedence over nodeColor unless null - linkColor: ko.Observable; - showNeighborType: ko.Observable; - nodeCaption: ko.Observable; - nodeSize: ko.Observable; - linkWidth: ko.Observable; - nodeIconKey: ko.Observable; - iconsMap: ko.Observable; -} - -interface GraphTabOptions extends ViewModels.TabOptions { - account: DatabaseAccount; - masterKey: string; - collectionId: string; - databaseId: string; - collectionPartitionKeyProperty: string; -} - -export default class GraphTab extends TabsBase { - // Graph default configuration - public static readonly DEFAULT_NODE_CAPTION = "id"; - private static readonly LINK_COLOR = "#aaa"; - private static readonly NODE_SIZE = 10; - private static readonly NODE_COLOR = "orange"; - private static readonly LINK_WIDTH = 1; - private graphExplorerAdapter: GraphExplorerAdapter; - private isNewVertexDisabled: ko.Observable; - private isPropertyEditing: ko.Observable; - private isGraphDisplayed: ko.Observable; - private graphAccessor: GraphAccessor; - private graphConfig: GraphConfig; - private graphConfigUiData: ViewModels.GraphConfigUiData; - private isFilterQueryLoading: ko.Observable; - private isValidQuery: ko.Observable; - private newVertexPane: NewVertexPane; - private graphStylingPane: GraphStylingPane; - private collectionPartitionKeyProperty: string; - - constructor(options: GraphTabOptions) { - super(options); - - this.newVertexPane = options.collection && options.collection.container.newVertexPane; - this.graphStylingPane = options.collection && options.collection.container.graphStylingPane; - this.collectionPartitionKeyProperty = options.collectionPartitionKeyProperty; - - this.isNewVertexDisabled = ko.observable(false); - this.isPropertyEditing = ko.observable(false); - this.isGraphDisplayed = ko.observable(false); - this.graphAccessor = null; - this.graphConfig = GraphTab.createGraphConfig(); - // TODO Merge this with this.graphConfig - this.graphConfigUiData = GraphTab.createGraphConfigUiData(this.graphConfig); - this.graphExplorerAdapter = new GraphExplorerAdapter({ - onGraphAccessorCreated: (instance: GraphAccessor): void => { - this.graphAccessor = instance; - }, - onIsNewVertexDisabledChange: (isDisabled: boolean): void => { - this.isNewVertexDisabled(isDisabled); - this.updateNavbarWithTabsButtons(); - }, - onIsPropertyEditing: (isEditing: boolean) => { - this.isPropertyEditing(isEditing); - this.updateNavbarWithTabsButtons(); - }, - onIsGraphDisplayed: (isDisplayed: boolean) => { - this.isGraphDisplayed(isDisplayed); - this.updateNavbarWithTabsButtons(); - }, - onResetDefaultGraphConfigValues: () => this.setDefaultGraphConfigValues(), - graphConfig: this.graphConfig, - graphConfigUiData: this.graphConfigUiData, - onIsFilterQueryLoading: (isFilterQueryLoading: boolean): void => this.isFilterQueryLoading(isFilterQueryLoading), - onIsValidQuery: (isValidQuery: boolean): void => this.isValidQuery(isValidQuery), - collectionPartitionKeyProperty: options.collectionPartitionKeyProperty, - graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account), - databaseId: options.databaseId, - collectionId: options.collectionId, - masterKey: options.masterKey, - onLoadStartKey: options.onLoadStartKey, - onLoadStartKeyChange: (onLoadStartKey: number): void => { - if (onLoadStartKey == null) { - this.onLoadStartKey = null; - } - }, - resourceId: options.account.id - }); - - this.isFilterQueryLoading = ko.observable(false); - this.isValidQuery = ko.observable(true); - } - - public static getGremlinEndpoint(account: DatabaseAccount): string { - return account.properties.gremlinEndpoint - ? GraphTab.sanitizeHost(account.properties.gremlinEndpoint) - : `${account.name}.graphs.azure.com:443/`; - } - - public onTabClick(): void { - super.onTabClick(); - this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph); - } - - /** - * Removing leading http|https and remove trailing / - * @param url - * @return - */ - private static sanitizeHost(url: string): string { - if (!url) { - return url; - } - return url.replace(/^(http|https):\/\//, "").replace(/\/$/, ""); - } - - /* Command bar */ - private showNewVertexEditor(): void { - this.newVertexPane.open(); - this.newVertexPane.setPartitionKeyProperty(this.collectionPartitionKeyProperty); - // TODO Must update GraphExplorer properties - this.newVertexPane.subscribeOnSubmitCreate((result: ViewModels.NewVertexData) => { - this.newVertexPane.formErrors(null); - this.newVertexPane.formErrorsDetails(null); - this.graphAccessor.addVertex(result).then( - () => { - this.newVertexPane.cancel(); - }, - (error: GraphExplorerError) => { - this.newVertexPane.formErrors(error.title); - if (!!error.details) { - this.newVertexPane.formErrorsDetails(error.details); - } - } - ); - }); - } - public openStyling() { - this.setDefaultGraphConfigValues(); - // Update the styling pane with this instance - this.graphStylingPane.setData(this.graphConfigUiData); - this.graphStylingPane.open(); - } - - public static createGraphConfig(): GraphConfig { - return { - nodeColor: ko.observable(GraphTab.NODE_COLOR), - nodeColorKey: ko.observable(null), - linkColor: ko.observable(GraphTab.LINK_COLOR), - showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY), - nodeCaption: ko.observable(GraphTab.DEFAULT_NODE_CAPTION), - nodeSize: ko.observable(GraphTab.NODE_SIZE), - linkWidth: ko.observable(GraphTab.LINK_WIDTH), - nodeIconKey: ko.observable(null), - iconsMap: ko.observable({}) - }; - } - - public static createGraphConfigUiData(graphConfig: GraphConfig): ViewModels.GraphConfigUiData { - return { - showNeighborType: ko.observable(graphConfig.showNeighborType()), - nodeProperties: ko.observableArray([]), - nodePropertiesWithNone: ko.observableArray([]), - nodeCaptionChoice: ko.observable(graphConfig.nodeCaption()), - nodeColorKeyChoice: ko.observable(graphConfig.nodeColorKey()), - nodeIconChoice: ko.observable(graphConfig.nodeIconKey()), - nodeIconSet: ko.observable(null) - }; - } - - /** - * Make sure graph config values are not null - */ - private setDefaultGraphConfigValues() { - // Assign default values if null - if (this.graphConfigUiData.nodeCaptionChoice() === null && this.graphConfigUiData.nodeProperties().length > 1) { - this.graphConfigUiData.nodeCaptionChoice(this.graphConfigUiData.nodeProperties()[0]); - } - if ( - this.graphConfigUiData.nodeColorKeyChoice() === null && - this.graphConfigUiData.nodePropertiesWithNone().length > 1 - ) { - this.graphConfigUiData.nodeColorKeyChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); - } - if ( - this.graphConfigUiData.nodeIconChoice() === null && - this.graphConfigUiData.nodePropertiesWithNone().length > 1 - ) { - this.graphConfigUiData.nodeIconChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); - } - } - protected getTabsButtons(): CommandButtonComponentProps[] { - const label = "New Vertex"; - const buttons: CommandButtonComponentProps[] = [ - { - iconSrc: NewVertexIcon, - iconAlt: label, - onCommandClick: () => this.showNewVertexEditor(), - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: this.isNewVertexDisabled() - } - ]; - buttons.push(); - if (this.isGraphDisplayed()) { - const label = "Style"; - buttons.push({ - iconSrc: StyleIcon, - iconAlt: label, - onCommandClick: () => this.openStyling(), - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: this.isPropertyEditing() - }); - } - return buttons; - } - protected buildCommandBarOptions(): void { - ko.computed(() => ko.toJSON([this.isNewVertexDisabled])).subscribe(() => this.updateNavbarWithTabsButtons()); - this.updateNavbarWithTabsButtons(); - } -} +import * as ko from "knockout"; +import * as Q from "q"; +import * as ViewModels from "../../Contracts/ViewModels"; +import TabsBase from "./TabsBase"; +import { GraphExplorerAdapter } from "../Graph/GraphExplorerComponent/GraphExplorerAdapter"; +import { GraphAccessor, GraphExplorerError } from "../Graph/GraphExplorerComponent/GraphExplorer"; +import NewVertexIcon from "../../../images/NewVertex.svg"; +import StyleIcon from "../../../images/Style.svg"; +import GraphStylingPane from "../Panes/GraphStylingPane"; +import NewVertexPane from "../Panes/NewVertexPane"; +import { DatabaseAccount } from "../../Contracts/DataModels"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; + +export interface GraphIconMap { + [key: string]: { data: string; format: string }; +} + +export interface GraphConfig { + nodeColor: ko.Observable; + nodeColorKey: ko.Observable; // map property to node color. Takes precedence over nodeColor unless null + linkColor: ko.Observable; + showNeighborType: ko.Observable; + nodeCaption: ko.Observable; + nodeSize: ko.Observable; + linkWidth: ko.Observable; + nodeIconKey: ko.Observable; + iconsMap: ko.Observable; +} + +interface GraphTabOptions extends ViewModels.TabOptions { + account: DatabaseAccount; + masterKey: string; + collectionId: string; + databaseId: string; + collectionPartitionKeyProperty: string; +} + +export default class GraphTab extends TabsBase { + // Graph default configuration + public static readonly DEFAULT_NODE_CAPTION = "id"; + private static readonly LINK_COLOR = "#aaa"; + private static readonly NODE_SIZE = 10; + private static readonly NODE_COLOR = "orange"; + private static readonly LINK_WIDTH = 1; + private graphExplorerAdapter: GraphExplorerAdapter; + private isNewVertexDisabled: ko.Observable; + private isPropertyEditing: ko.Observable; + private isGraphDisplayed: ko.Observable; + private graphAccessor: GraphAccessor; + private graphConfig: GraphConfig; + private graphConfigUiData: ViewModels.GraphConfigUiData; + private isFilterQueryLoading: ko.Observable; + private isValidQuery: ko.Observable; + private newVertexPane: NewVertexPane; + private graphStylingPane: GraphStylingPane; + private collectionPartitionKeyProperty: string; + + constructor(options: GraphTabOptions) { + super(options); + + this.newVertexPane = options.collection && options.collection.container.newVertexPane; + this.graphStylingPane = options.collection && options.collection.container.graphStylingPane; + this.collectionPartitionKeyProperty = options.collectionPartitionKeyProperty; + + this.isNewVertexDisabled = ko.observable(false); + this.isPropertyEditing = ko.observable(false); + this.isGraphDisplayed = ko.observable(false); + this.graphAccessor = null; + this.graphConfig = GraphTab.createGraphConfig(); + // TODO Merge this with this.graphConfig + this.graphConfigUiData = GraphTab.createGraphConfigUiData(this.graphConfig); + this.graphExplorerAdapter = new GraphExplorerAdapter({ + onGraphAccessorCreated: (instance: GraphAccessor): void => { + this.graphAccessor = instance; + }, + onIsNewVertexDisabledChange: (isDisabled: boolean): void => { + this.isNewVertexDisabled(isDisabled); + this.updateNavbarWithTabsButtons(); + }, + onIsPropertyEditing: (isEditing: boolean) => { + this.isPropertyEditing(isEditing); + this.updateNavbarWithTabsButtons(); + }, + onIsGraphDisplayed: (isDisplayed: boolean) => { + this.isGraphDisplayed(isDisplayed); + this.updateNavbarWithTabsButtons(); + }, + onResetDefaultGraphConfigValues: () => this.setDefaultGraphConfigValues(), + graphConfig: this.graphConfig, + graphConfigUiData: this.graphConfigUiData, + onIsFilterQueryLoading: (isFilterQueryLoading: boolean): void => this.isFilterQueryLoading(isFilterQueryLoading), + onIsValidQuery: (isValidQuery: boolean): void => this.isValidQuery(isValidQuery), + collectionPartitionKeyProperty: options.collectionPartitionKeyProperty, + graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account), + databaseId: options.databaseId, + collectionId: options.collectionId, + masterKey: options.masterKey, + onLoadStartKey: options.onLoadStartKey, + onLoadStartKeyChange: (onLoadStartKey: number): void => { + if (onLoadStartKey == null) { + this.onLoadStartKey = null; + } + }, + resourceId: options.account.id, + }); + + this.isFilterQueryLoading = ko.observable(false); + this.isValidQuery = ko.observable(true); + } + + public static getGremlinEndpoint(account: DatabaseAccount): string { + return account.properties.gremlinEndpoint + ? GraphTab.sanitizeHost(account.properties.gremlinEndpoint) + : `${account.name}.graphs.azure.com:443/`; + } + + public onTabClick(): void { + super.onTabClick(); + this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph); + } + + /** + * Removing leading http|https and remove trailing / + * @param url + * @return + */ + private static sanitizeHost(url: string): string { + if (!url) { + return url; + } + return url.replace(/^(http|https):\/\//, "").replace(/\/$/, ""); + } + + /* Command bar */ + private showNewVertexEditor(): void { + this.newVertexPane.open(); + this.newVertexPane.setPartitionKeyProperty(this.collectionPartitionKeyProperty); + // TODO Must update GraphExplorer properties + this.newVertexPane.subscribeOnSubmitCreate((result: ViewModels.NewVertexData) => { + this.newVertexPane.formErrors(null); + this.newVertexPane.formErrorsDetails(null); + this.graphAccessor.addVertex(result).then( + () => { + this.newVertexPane.cancel(); + }, + (error: GraphExplorerError) => { + this.newVertexPane.formErrors(error.title); + if (!!error.details) { + this.newVertexPane.formErrorsDetails(error.details); + } + } + ); + }); + } + public openStyling() { + this.setDefaultGraphConfigValues(); + // Update the styling pane with this instance + this.graphStylingPane.setData(this.graphConfigUiData); + this.graphStylingPane.open(); + } + + public static createGraphConfig(): GraphConfig { + return { + nodeColor: ko.observable(GraphTab.NODE_COLOR), + nodeColorKey: ko.observable(null), + linkColor: ko.observable(GraphTab.LINK_COLOR), + showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY), + nodeCaption: ko.observable(GraphTab.DEFAULT_NODE_CAPTION), + nodeSize: ko.observable(GraphTab.NODE_SIZE), + linkWidth: ko.observable(GraphTab.LINK_WIDTH), + nodeIconKey: ko.observable(null), + iconsMap: ko.observable({}), + }; + } + + public static createGraphConfigUiData(graphConfig: GraphConfig): ViewModels.GraphConfigUiData { + return { + showNeighborType: ko.observable(graphConfig.showNeighborType()), + nodeProperties: ko.observableArray([]), + nodePropertiesWithNone: ko.observableArray([]), + nodeCaptionChoice: ko.observable(graphConfig.nodeCaption()), + nodeColorKeyChoice: ko.observable(graphConfig.nodeColorKey()), + nodeIconChoice: ko.observable(graphConfig.nodeIconKey()), + nodeIconSet: ko.observable(null), + }; + } + + /** + * Make sure graph config values are not null + */ + private setDefaultGraphConfigValues() { + // Assign default values if null + if (this.graphConfigUiData.nodeCaptionChoice() === null && this.graphConfigUiData.nodeProperties().length > 1) { + this.graphConfigUiData.nodeCaptionChoice(this.graphConfigUiData.nodeProperties()[0]); + } + if ( + this.graphConfigUiData.nodeColorKeyChoice() === null && + this.graphConfigUiData.nodePropertiesWithNone().length > 1 + ) { + this.graphConfigUiData.nodeColorKeyChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); + } + if ( + this.graphConfigUiData.nodeIconChoice() === null && + this.graphConfigUiData.nodePropertiesWithNone().length > 1 + ) { + this.graphConfigUiData.nodeIconChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); + } + } + protected getTabsButtons(): CommandButtonComponentProps[] { + const label = "New Vertex"; + const buttons: CommandButtonComponentProps[] = [ + { + iconSrc: NewVertexIcon, + iconAlt: label, + onCommandClick: () => this.showNewVertexEditor(), + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: this.isNewVertexDisabled(), + }, + ]; + buttons.push(); + if (this.isGraphDisplayed()) { + const label = "Style"; + buttons.push({ + iconSrc: StyleIcon, + iconAlt: label, + onCommandClick: () => this.openStyling(), + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: this.isPropertyEditing(), + }); + } + return buttons; + } + protected buildCommandBarOptions(): void { + ko.computed(() => ko.toJSON([this.isNewVertexDisabled])).subscribe(() => this.updateNavbarWithTabsButtons()); + this.updateNavbarWithTabsButtons(); + } +} diff --git a/src/Explorer/Tabs/MongoDocumentsTab.html b/src/Explorer/Tabs/MongoDocumentsTab.html index 499483c6a..00e2a6685 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.html +++ b/src/Explorer/Tabs/MongoDocumentsTab.html @@ -1,417 +1,426 @@ -
- -
-
- - - New Document - - - New Document - - - - - - New SQL Query - - - New Query - - - - - - Save - - - Save - - - - - - Discard - - - Discard - - - - - - Update - - - Update - - - - - - Discard - - - Discard - - - - - - Delete - - - Delete - - -
-
- - - - -
-
-

Title

-
Text
-
-
- - -
-
-
-
- - - - -
- -
- SELECT * FROM c - - -
-
- Filter : - - No filter applied - - -
- - - -
-
-
- - SELECT * FROM c - - - - - - - - - - - - - - -
-
-
- -
- - - -
-
-
- - - - - - - - - - - - - -
_idid - -
- - - - - - - - - - - -
_id - -
- -
- -
- - - - - - - - - - - -
- -
- -
- Load more -
-
- - - -
-
-
- -
-
- -
+
+ +
+
+ + + New Document + + + New Document + + + + + + New SQL Query + + + New Query + + + + + + Save + + + Save + + + + + + Discard + + + Discard + + + + + + Update + + + Update + + + + + + Discard + + + Discard + + + + + + Delete + + + Delete + + +
+
+ + + + +
+
+

Title

+
Text
+
+
+ + +
+
+
+
+ + + + +
+ +
+ SELECT * FROM c + + +
+
+ Filter : + + No filter applied + + +
+ + + +
+
+
+ + SELECT * FROM c + + + + + + + + + + + + + + +
+
+
+ +
+ + + +
+
+
+ + + + + + + + + + + + + +
_idid + +
+ + + + + + + + + + + +
_id + +
+ +
+ +
+ + + + + + + + + + + +
+ +
+ +
+ Load more +
+
+ + + +
+
+
+ +
+
+ +
diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index 61d500988..921f2d677 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -14,7 +14,7 @@ import { deleteDocument, queryDocuments, readDocument, - updateDocument + updateDocument, } from "../../Common/MongoProxyClient"; import { extractPartitionKey } from "@azure/cosmos"; import * as Logger from "../../Common/Logger"; @@ -51,7 +51,7 @@ export default class MongoDocumentsTab extends DocumentsTab { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }); if ( @@ -73,7 +73,7 @@ export default class MongoDocumentsTab extends DocumentsTab { defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: message + error: message, }, startKey ); @@ -110,12 +110,12 @@ export default class MongoDocumentsTab extends DocumentsTab { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, startKey ); }, - error => { + (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); window.alert(errorMessage); @@ -127,7 +127,7 @@ export default class MongoDocumentsTab extends DocumentsTab { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: errorMessage, - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, startKey ); @@ -145,7 +145,7 @@ export default class MongoDocumentsTab extends DocumentsTab { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }); return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent) @@ -174,12 +174,12 @@ export default class MongoDocumentsTab extends DocumentsTab { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, startKey ); }, - error => { + (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); window.alert(errorMessage); @@ -191,7 +191,7 @@ export default class MongoDocumentsTab extends DocumentsTab { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: errorMessage, - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, startKey ); @@ -221,7 +221,7 @@ export default class MongoDocumentsTab extends DocumentsTab { ({ continuationToken, documents }) => { this.continuationToken = continuationToken; let currentDocuments = this.documentIds(); - const currentDocumentsRids = currentDocuments.map(currentDocument => currentDocument.rid); + const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); const nextDocumentIds = documents .filter((d: any) => { return currentDocumentsRids.indexOf(d._rid) < 0; @@ -251,7 +251,7 @@ export default class MongoDocumentsTab extends DocumentsTab { collectionName: this.collection.id(), defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, this.onLoadStartKey ); @@ -270,7 +270,7 @@ export default class MongoDocumentsTab extends DocumentsTab { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: getErrorMessage(error), - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, this.onLoadStartKey ); @@ -320,7 +320,7 @@ export default class MongoDocumentsTab extends DocumentsTab { partitionKey = { kind: partitionKey.kind, paths: ["/" + this.partitionKeyProperty.replace(/\./g, "/")], - version: partitionKey.version + version: partitionKey.version, }; } diff --git a/src/Explorer/Tabs/MongoQueryTab.html b/src/Explorer/Tabs/MongoQueryTab.html index 681a81aa1..4302c986f 100644 --- a/src/Explorer/Tabs/MongoQueryTab.html +++ b/src/Explorer/Tabs/MongoQueryTab.html @@ -1,120 +1,120 @@ -
- -
-
- - - Run - - - Run - - -
-
- - -
-
- Start by writing a Mongo query, for example: {'id':'foo'} or { } to get all the - documents. -
- -
- Errors -
- - - -
- -
-
- Results: - | Request Charge: - | - - - Next - - - - Next - -
-
-
- - - -
- -
- : -
- -
- -
- -
+
+ +
+
+ + + Run + + + Run + + +
+
+ + +
+
+ Start by writing a Mongo query, for example: {'id':'foo'} or { } to get all the + documents. +
+ +
+ Errors +
+ + + +
+ +
+
+ Results: + | Request Charge: + | + + + Next + + + + Next + +
+
+
+ + + +
+ +
+ : +
+ +
+ +
+ +
diff --git a/src/Explorer/Tabs/MongoShellTab.html b/src/Explorer/Tabs/MongoShellTab.html index 84775b99d..33b6dae29 100644 --- a/src/Explorer/Tabs/MongoShellTab.html +++ b/src/Explorer/Tabs/MongoShellTab.html @@ -1,15 +1,15 @@ - + diff --git a/src/Explorer/Tabs/MongoShellTab.ts b/src/Explorer/Tabs/MongoShellTab.ts index 182bddce3..8ec2ec006 100644 --- a/src/Explorer/Tabs/MongoShellTab.ts +++ b/src/Explorer/Tabs/MongoShellTab.ts @@ -1,215 +1,215 @@ -import * as Constants from "../../Common/Constants"; -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import AuthHeadersUtil from "../../Platform/Hosted/Authorization"; -import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; -import Q from "q"; -import TabsBase from "./TabsBase"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import { HashMap } from "../../Common/HashMap"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import Explorer from "../Explorer"; -import { userContext } from "../../UserContext"; -import { configContext, Platform } from "../../ConfigContext"; - -export default class MongoShellTab extends TabsBase { - public url: ko.Computed; - private _container: Explorer; - private _runtimeEndpoint: string; - private _logTraces: HashMap; - - constructor(options: ViewModels.TabOptions) { - super(options); - this._logTraces = new HashMap(); - this._container = options.collection.container; - this.url = ko.computed(() => { - const account = userContext.databaseAccount; - const resourceId = account && account.id; - const accountName = account && account.name; - const mongoEndpoint = account && (account.properties.mongoEndpoint || account.properties.documentEndpoint); - - this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : ""; - const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || ""; - let baseUrl = "/content/mongoshell/dist/"; - if (this._container.serverId() === "localhost") { - baseUrl = "/content/mongoshell/"; - } - - return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; - }); - - window.addEventListener("message", this.handleMessage.bind(this), false); - } - - public setContentFocus(event: any): any { - // TODO: Work around cross origin security issue in Hosted Data Explorer by using Shell <-> Data Explorer messaging (253527) - // if(event.type === "load" && window.dataExplorerPlatform != PlatformType.Hosted) { - // let activeShell = event.target.contentWindow && event.target.contentWindow.mongo && event.target.contentWindow.mongo.shells && event.target.contentWindow.mongo.shells[0]; - // activeShell && setTimeout(function(){ - // activeShell.focus(); - // },2000); - // } - } - - public onTabClick(): void { - super.onTabClick(); - this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); - } - - public handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - const shellIframe: HTMLIFrameElement = document.getElementById(this.tabId); - - if (!shellIframe) { - return; - } - if (typeof event.data !== "object" || event.data["signature"] !== "mongoshell") { - return; - } - if (!("data" in event.data) || !("eventType" in event.data)) { - return; - } - - if (event.data.eventType == MessageType.IframeReady) { - this.handleReadyMessage(event, shellIframe); - } else if (event.data.eventType == MessageType.Notification) { - this.handleNotificationMessage(event, shellIframe); - } else { - this.handleLogMessage(event, shellIframe); - } - } - - private handleReadyMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { - if (typeof event.data["data"] !== "string") { - return; - } - if (event.data.data !== "ready") { - return; - } - - const authorization: string = userContext.authorizationToken || ""; - const resourceId = this._container.databaseAccount().id; - const accountName = this._container.databaseAccount().name; - const documentEndpoint = - this._container.databaseAccount().properties.mongoEndpoint || - this._container.databaseAccount().properties.documentEndpoint; - const mongoEndpoint = - documentEndpoint.substr( - Constants.MongoDBAccounts.protocol.length + 3, - documentEndpoint.length - - (Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length) - ) + Constants.MongoDBAccounts.defaultPort.toString(); - const databaseId = this.collection.databaseId; - const collectionId = this.collection.id(); - const apiEndpoint = configContext.BACKEND_ENDPOINT; - const encryptedAuthToken: string = userContext.accessToken; - - shellIframe.contentWindow.postMessage( - { - signature: "dataexplorer", - data: { - resourceId: resourceId, - accountName: accountName, - mongoEndpoint: mongoEndpoint, - authorization: authorization, - databaseId: databaseId, - collectionId: collectionId, - encryptedAuthToken: encryptedAuthToken, - apiEndpoint: apiEndpoint - } - }, - configContext.BACKEND_ENDPOINT - ); - } - - private handleLogMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { - if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") { - return; - } - if (!("logData" in event.data.data)) { - return; - } - - const dataToLog = { message: event.data.data.logData }; - const logType: string = event.data.data.logType; - const shellTraceId: string = event.data.data.traceId || "none"; - - switch (logType) { - case LogType.Information: - TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Success, dataToLog); - break; - case LogType.Warning: - TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Failed, dataToLog); - break; - case LogType.Verbose: - TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Mark, dataToLog); - break; - case LogType.StartTrace: - const telemetryTraceId: number = TelemetryProcessor.traceStart(Action.MongoShell, dataToLog); - this._logTraces.set(shellTraceId, telemetryTraceId); - break; - case LogType.SuccessTrace: - if (this._logTraces.has(shellTraceId)) { - const originalTelemetryTraceId: number = this._logTraces.get(shellTraceId); - TelemetryProcessor.traceSuccess(Action.MongoShell, dataToLog, originalTelemetryTraceId); - this._logTraces.delete(shellTraceId); - } else { - TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Success, dataToLog); - } - break; - case LogType.FailureTrace: - if (this._logTraces.has(shellTraceId)) { - const originalTelemetryTraceId: number = this._logTraces.get(shellTraceId); - TelemetryProcessor.traceFailure(Action.MongoShell, dataToLog, originalTelemetryTraceId); - this._logTraces.delete(shellTraceId); - } else { - TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Failed, dataToLog); - } - break; - } - } - - private handleNotificationMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { - if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") { - return; - } - if (!("logData" in event.data.data)) { - return; - } - - const dataToLog: string = event.data.data.logData; - const logType: string = event.data.data.logType; - - switch (logType) { - case LogType.Information: - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, dataToLog); - break; - case LogType.Warning: - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, dataToLog); - break; - case LogType.InProgress: - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, dataToLog); - } - } -} - -class MessageType { - static IframeReady: string = "iframeready"; - static Notification: string = "notification"; - static Log: string = "log"; -} - -class LogType { - static Information: string = "information"; - static Warning: string = "warning"; - static Verbose: string = "verbose"; - static InProgress: string = "inprogress"; - static StartTrace: string = "start"; - static SuccessTrace: string = "success"; - static FailureTrace: string = "failure"; -} +import * as Constants from "../../Common/Constants"; +import * as ko from "knockout"; +import * as ViewModels from "../../Contracts/ViewModels"; +import AuthHeadersUtil from "../../Platform/Hosted/Authorization"; +import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; +import Q from "q"; +import TabsBase from "./TabsBase"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; +import { HashMap } from "../../Common/HashMap"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import Explorer from "../Explorer"; +import { userContext } from "../../UserContext"; +import { configContext, Platform } from "../../ConfigContext"; + +export default class MongoShellTab extends TabsBase { + public url: ko.Computed; + private _container: Explorer; + private _runtimeEndpoint: string; + private _logTraces: HashMap; + + constructor(options: ViewModels.TabOptions) { + super(options); + this._logTraces = new HashMap(); + this._container = options.collection.container; + this.url = ko.computed(() => { + const account = userContext.databaseAccount; + const resourceId = account && account.id; + const accountName = account && account.name; + const mongoEndpoint = account && (account.properties.mongoEndpoint || account.properties.documentEndpoint); + + this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : ""; + const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || ""; + let baseUrl = "/content/mongoshell/dist/"; + if (this._container.serverId() === "localhost") { + baseUrl = "/content/mongoshell/"; + } + + return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; + }); + + window.addEventListener("message", this.handleMessage.bind(this), false); + } + + public setContentFocus(event: any): any { + // TODO: Work around cross origin security issue in Hosted Data Explorer by using Shell <-> Data Explorer messaging (253527) + // if(event.type === "load" && window.dataExplorerPlatform != PlatformType.Hosted) { + // let activeShell = event.target.contentWindow && event.target.contentWindow.mongo && event.target.contentWindow.mongo.shells && event.target.contentWindow.mongo.shells[0]; + // activeShell && setTimeout(function(){ + // activeShell.focus(); + // },2000); + // } + } + + public onTabClick(): void { + super.onTabClick(); + this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); + } + + public handleMessage(event: MessageEvent) { + if (isInvalidParentFrameOrigin(event)) { + return; + } + + const shellIframe: HTMLIFrameElement = document.getElementById(this.tabId); + + if (!shellIframe) { + return; + } + if (typeof event.data !== "object" || event.data["signature"] !== "mongoshell") { + return; + } + if (!("data" in event.data) || !("eventType" in event.data)) { + return; + } + + if (event.data.eventType == MessageType.IframeReady) { + this.handleReadyMessage(event, shellIframe); + } else if (event.data.eventType == MessageType.Notification) { + this.handleNotificationMessage(event, shellIframe); + } else { + this.handleLogMessage(event, shellIframe); + } + } + + private handleReadyMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { + if (typeof event.data["data"] !== "string") { + return; + } + if (event.data.data !== "ready") { + return; + } + + const authorization: string = userContext.authorizationToken || ""; + const resourceId = this._container.databaseAccount().id; + const accountName = this._container.databaseAccount().name; + const documentEndpoint = + this._container.databaseAccount().properties.mongoEndpoint || + this._container.databaseAccount().properties.documentEndpoint; + const mongoEndpoint = + documentEndpoint.substr( + Constants.MongoDBAccounts.protocol.length + 3, + documentEndpoint.length - + (Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length) + ) + Constants.MongoDBAccounts.defaultPort.toString(); + const databaseId = this.collection.databaseId; + const collectionId = this.collection.id(); + const apiEndpoint = configContext.BACKEND_ENDPOINT; + const encryptedAuthToken: string = userContext.accessToken; + + shellIframe.contentWindow.postMessage( + { + signature: "dataexplorer", + data: { + resourceId: resourceId, + accountName: accountName, + mongoEndpoint: mongoEndpoint, + authorization: authorization, + databaseId: databaseId, + collectionId: collectionId, + encryptedAuthToken: encryptedAuthToken, + apiEndpoint: apiEndpoint, + }, + }, + configContext.BACKEND_ENDPOINT + ); + } + + private handleLogMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { + if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") { + return; + } + if (!("logData" in event.data.data)) { + return; + } + + const dataToLog = { message: event.data.data.logData }; + const logType: string = event.data.data.logType; + const shellTraceId: string = event.data.data.traceId || "none"; + + switch (logType) { + case LogType.Information: + TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Success, dataToLog); + break; + case LogType.Warning: + TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Failed, dataToLog); + break; + case LogType.Verbose: + TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Mark, dataToLog); + break; + case LogType.StartTrace: + const telemetryTraceId: number = TelemetryProcessor.traceStart(Action.MongoShell, dataToLog); + this._logTraces.set(shellTraceId, telemetryTraceId); + break; + case LogType.SuccessTrace: + if (this._logTraces.has(shellTraceId)) { + const originalTelemetryTraceId: number = this._logTraces.get(shellTraceId); + TelemetryProcessor.traceSuccess(Action.MongoShell, dataToLog, originalTelemetryTraceId); + this._logTraces.delete(shellTraceId); + } else { + TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Success, dataToLog); + } + break; + case LogType.FailureTrace: + if (this._logTraces.has(shellTraceId)) { + const originalTelemetryTraceId: number = this._logTraces.get(shellTraceId); + TelemetryProcessor.traceFailure(Action.MongoShell, dataToLog, originalTelemetryTraceId); + this._logTraces.delete(shellTraceId); + } else { + TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Failed, dataToLog); + } + break; + } + } + + private handleNotificationMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { + if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") { + return; + } + if (!("logData" in event.data.data)) { + return; + } + + const dataToLog: string = event.data.data.logData; + const logType: string = event.data.data.logType; + + switch (logType) { + case LogType.Information: + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, dataToLog); + break; + case LogType.Warning: + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, dataToLog); + break; + case LogType.InProgress: + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, dataToLog); + } + } +} + +class MessageType { + static IframeReady: string = "iframeready"; + static Notification: string = "notification"; + static Log: string = "log"; +} + +class LogType { + static Information: string = "information"; + static Warning: string = "warning"; + static Verbose: string = "verbose"; + static InProgress: string = "inprogress"; + static StartTrace: string = "start"; + static SuccessTrace: string = "success"; + static FailureTrace: string = "failure"; +} diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 88a692ec9..0bc14dde8 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -56,7 +56,7 @@ export default class NotebookTabV2 extends TabsBase { connectionInfo: this.container.notebookServerInfo(), databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), - contentProvider: this.container.notebookManager?.notebookContentProvider + contentProvider: this.container.notebookManager?.notebookContentProvider, }); } @@ -70,7 +70,7 @@ export default class NotebookTabV2 extends TabsBase { contentItem: options.notebookContentItem, notebooksBasePath: this.container.getNotebookBasePath(), notebookClient: NotebookTabV2.clientManager, - onUpdateKernelInfo: this.onKernelUpdate + onUpdateKernelInfo: this.onKernelUpdate, }); this.selectedSparkPool = ko.observable(null); @@ -156,7 +156,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: copyToLabel, hasPopup: false, disabled: false, - ariaLabel: copyToLabel + ariaLabel: copyToLabel, }); } @@ -167,7 +167,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: publishLabel, hasPopup: false, disabled: false, - ariaLabel: publishLabel + ariaLabel: publishLabel, }); } @@ -187,10 +187,10 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: saveLabel, hasPopup: false, disabled: false, - ariaLabel: saveLabel + ariaLabel: saveLabel, }, - ...saveButtonChildren - ] + ...saveButtonChildren, + ], }, { iconSrc: null, @@ -213,10 +213,10 @@ export default class NotebookTabV2 extends TabsBase { dropdownItemKey: kernel.name, hasPopup: false, disabled: false, - ariaLabel: kernel.displayName + ariaLabel: kernel.displayName, } as CommandButtonComponentProps) ), - ariaLabel: kernelLabel + ariaLabel: kernelLabel, }, { iconSrc: RunIcon, @@ -240,7 +240,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: runActiveCellLabel, hasPopup: false, disabled: false, - ariaLabel: runActiveCellLabel + ariaLabel: runActiveCellLabel, }, { iconSrc: RunAllIcon, @@ -252,7 +252,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: runAllLabel, hasPopup: false, disabled: false, - ariaLabel: runAllLabel + ariaLabel: runAllLabel, }, { iconSrc: InterruptKernelIcon, @@ -261,7 +261,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: interruptKernelLabel, hasPopup: false, disabled: false, - ariaLabel: interruptKernelLabel + ariaLabel: interruptKernelLabel, }, { iconSrc: KillKernelIcon, @@ -270,7 +270,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: killKernelLabel, hasPopup: false, disabled: false, - ariaLabel: killKernelLabel + ariaLabel: killKernelLabel, }, { iconSrc: RestartIcon, @@ -279,9 +279,9 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: restartKernelLabel, hasPopup: false, disabled: false, - ariaLabel: restartKernelLabel - } - ] + ariaLabel: restartKernelLabel, + }, + ], }, { iconSrc: ClearAllOutputsIcon, @@ -290,7 +290,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: clearLabel, hasPopup: false, disabled: false, - ariaLabel: clearLabel + ariaLabel: clearLabel, }, { iconSrc: NewCellIcon, @@ -299,7 +299,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: newCellLabel, ariaLabel: newCellLabel, hasPopup: false, - disabled: false + disabled: false, }, CommandBarComponentButtonFactory.createDivider(), { @@ -323,7 +323,7 @@ export default class NotebookTabV2 extends TabsBase { ariaLabel: codeLabel, dropdownItemKey: cellCodeType, hasPopup: false, - disabled: false + disabled: false, }, { iconSrc: null, @@ -333,7 +333,7 @@ export default class NotebookTabV2 extends TabsBase { ariaLabel: markdownLabel, dropdownItemKey: cellMarkdownType, hasPopup: false, - disabled: false + disabled: false, }, { iconSrc: null, @@ -343,9 +343,9 @@ export default class NotebookTabV2 extends TabsBase { ariaLabel: rawLabel, dropdownItemKey: cellRawType, hasPopup: false, - disabled: false - } - ] + disabled: false, + }, + ], }, { iconSrc: CopyIcon, @@ -363,7 +363,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: copyLabel, ariaLabel: copyLabel, hasPopup: false, - disabled: false + disabled: false, }, { iconSrc: CutIcon, @@ -372,7 +372,7 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: cutLabel, ariaLabel: cutLabel, hasPopup: false, - disabled: false + disabled: false, }, { iconSrc: PasteIcon, @@ -381,10 +381,10 @@ export default class NotebookTabV2 extends TabsBase { commandButtonLabel: pasteLabel, ariaLabel: pasteLabel, hasPopup: false, - disabled: false - } - ] - } + disabled: false, + }, + ], + }, // TODO: Uncomment when undo/redo is reimplemented in nteract ]; @@ -408,8 +408,8 @@ export default class NotebookTabV2 extends TabsBase { }, onCreateNewSparkPoolClicked: (workspaceResourceId: string) => { this.container.createSparkPool(workspaceResourceId); - } - } + }, + }, }; buttons.splice(1, 0, arcadiaWorkspaceDropdown); } @@ -429,9 +429,9 @@ export default class NotebookTabV2 extends TabsBase { this.container && this.container.arcadiaWorkspaces && this.container.arcadiaWorkspaces() && - this.container.arcadiaWorkspaces().forEach(async workspace => { + this.container.arcadiaWorkspaces().forEach(async (workspace) => { if (workspace && workspace.name && workspace.sparkPools) { - const selectedPoolIndex = _.findIndex(workspace.sparkPools, pool => pool && pool.name === item.text); + const selectedPoolIndex = _.findIndex(workspace.sparkPools, (pool) => pool && pool.name === item.text); if (selectedPoolIndex >= 0) { const selectedPool = workspace.sparkPools[selectedPoolIndex]; if (selectedPool && selectedPool.name) { @@ -441,9 +441,9 @@ export default class NotebookTabV2 extends TabsBase { endpoints: [ { endpoint: `https://${workspace.name}.${configContext.ARCADIA_LIVY_ENDPOINT_DNS_ZONE}/livyApi/versions/${ArmApiVersions.arcadiaLivy}/sparkPools/${selectedPool.name}/`, - kind: DataModels.SparkClusterEndpointKind.Livy - } - ] + kind: DataModels.SparkClusterEndpointKind.Livy, + }, + ], }); this.selectedSparkPool(item.text); await this.reconfigureServiceEndpoints(); @@ -456,7 +456,7 @@ export default class NotebookTabV2 extends TabsBase { }; private onKernelUpdate = async () => { - await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName()).catch(reason => { + await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName()).catch((reason) => { /* Erroring is ok here */ }); this.updateNavbarWithTabsButtons(); @@ -498,7 +498,7 @@ export default class NotebookTabV2 extends TabsBase { TelemetryProcessor.trace(actionType, ActionModifiers.Mark, { databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience && this.container.defaultExperience(), - dataExplorerArea: Areas.Notebook + dataExplorerArea: Areas.Notebook, }); } } diff --git a/src/Explorer/Tabs/NotebookViewerTab.tsx b/src/Explorer/Tabs/NotebookViewerTab.tsx index 8cbce11e8..165e50460 100644 --- a/src/Explorer/Tabs/NotebookViewerTab.tsx +++ b/src/Explorer/Tabs/NotebookViewerTab.tsx @@ -4,7 +4,7 @@ import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import * as ViewModels from "../../Contracts/ViewModels"; import { NotebookViewerComponent, - NotebookViewerComponentProps + NotebookViewerComponentProps, } from "../Controls/NotebookViewer/NotebookViewerComponent"; import TabsBase from "./TabsBase"; import Explorer from "../Explorer"; @@ -30,7 +30,7 @@ class NotebookViewerComponentAdapter implements ReactAdapter { notebookUrl: this.notebookUrl, backNavigationText: undefined, onBackClick: undefined, - onTagClick: undefined + onTagClick: undefined, }; return this.parameters() ? : <>; diff --git a/src/Explorer/Tabs/QueryTab.html b/src/Explorer/Tabs/QueryTab.html index ff4b40b06..8f3b2d229 100644 --- a/src/Explorer/Tabs/QueryTab.html +++ b/src/Explorer/Tabs/QueryTab.html @@ -1,342 +1,342 @@ -
-
-
- Start by writing a Mongo query, for example: {'id':'foo'} or { } to get all the - documents. -
-
-
- Error - - We have detected you may be using a subquery. Non-correlated subqueries are not currently supported. - Please see Cosmos sub query documentation for further information - -
-
-
- - -
- Splitter -
-
- - - - - - -
- Errors -
- - - -
-
-

Execute Query Watermark

-

Execute a query to see the results

-
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Query Statistics -
METRICVALUE
Request Charge - -
Showing Results - -
- Retrieved document count - - More information - Total number of retrieved documents - -
- Retrieved document size - - More information - Total size of retrieved documents in bytes - - - - bytes -
- Output document count - - More information - Number of output documents - -
- Output document size - - More information - Total size of output documents in bytes - - - - bytes -
- Index hit document count - - More information - Total number of documents matched by the filter - -
- Index lookup time - - More information - Time spent in physical index layer - - - ms -
- Document load time - - More information - Time spent in loading documents - - - ms -
- Query engine execution time - - More information - Time spent by the query engine to execute the query expression (excludes other execution times - like load documents or write results) - - - - ms -
- System function execution time - - More information - Total time spent executing system (built-in) functions - - - - ms -
- User defined function execution time - - More information - Total time spent executing user-defined functions - - - - ms -
- Document write time - - More information - Time spent to write query result set to response buffer - - - ms -
Round Trips
Activity id
- -
- -
-
- - - More details - -
-
- -
-
- -
-
+
+
+
+ Start by writing a Mongo query, for example: {'id':'foo'} or { } to get all the + documents. +
+
+
+ Error + + We have detected you may be using a subquery. Non-correlated subqueries are not currently supported. + Please see Cosmos sub query documentation for further information + +
+
+
+ + +
+ Splitter +
+
+ + + + + + +
+ Errors +
+ + + +
+
+

Execute Query Watermark

+

Execute a query to see the results

+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Query Statistics +
METRICVALUE
Request Charge + +
Showing Results + +
+ Retrieved document count + + More information + Total number of retrieved documents + +
+ Retrieved document size + + More information + Total size of retrieved documents in bytes + + + + bytes +
+ Output document count + + More information + Number of output documents + +
+ Output document size + + More information + Total size of output documents in bytes + + + + bytes +
+ Index hit document count + + More information + Total number of documents matched by the filter + +
+ Index lookup time + + More information + Time spent in physical index layer + + + ms +
+ Document load time + + More information + Time spent in loading documents + + + ms +
+ Query engine execution time + + More information + Time spent by the query engine to execute the query expression (excludes other execution times + like load documents or write results) + + + + ms +
+ System function execution time + + More information + Total time spent executing system (built-in) functions + + + + ms +
+ User defined function execution time + + More information + Total time spent executing user-defined functions + + + + ms +
+ Document write time + + More information + Time spent to write query result set to response buffer + + + ms +
Round Trips
Activity id
+ +
+ +
+
+ + + More details + +
+
+ +
+
+ +
+
diff --git a/src/Explorer/Tabs/QueryTab.test.ts b/src/Explorer/Tabs/QueryTab.test.ts index 100f14227..b9a6cf042 100644 --- a/src/Explorer/Tabs/QueryTab.test.ts +++ b/src/Explorer/Tabs/QueryTab.test.ts @@ -1,86 +1,86 @@ -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import * as ViewModels from "../../Contracts/ViewModels"; -import Explorer from "../Explorer"; -import QueryTab from "./QueryTab"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; - -describe("Query Tab", () => { - function getNewQueryTabForContainer(container: Explorer): QueryTab { - const database = { - container: container, - id: ko.observable("test"), - isDatabaseShared: () => false - } as ViewModels.Database; - const collection = { - container: container, - databaseId: "test", - id: ko.observable("test") - } as ViewModels.Collection; - - return new QueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - collection: collection, - database: database, - title: "", - tabPath: "", - isActive: ko.observable(false), - hashLocation: "", - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {} - }); - } - - describe("shouldSetSystemPartitionKeyContainerPartitionKeyValueUndefined", () => { - const collection = { - id: ko.observable("withoutsystempk"), - partitionKey: { - systemKey: true - } - } as ViewModels.Collection; - - it("no container with system pk, should not set partition key option", () => { - const iteratorOptions = QueryTab.getIteratorOptions(collection); - expect(iteratorOptions.initialHeaders).toBeUndefined(); - }); - }); - - describe("isQueryMetricsEnabled()", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be true for accounts using SQL API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase()); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.isQueryMetricsEnabled()).toBe(true); - }); - - it("should be false for accounts using other APIs", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase()); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.isQueryMetricsEnabled()).toBe(false); - }); - }); - - describe("Save Queries command button", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be visible when using a supported API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.saveQueryButton.visible()).toBe(true); - }); - - it("should not be visible when using an unsupported API", () => { - explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.saveQueryButton.visible()).toBe(false); - }); - }); -}); +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import * as ViewModels from "../../Contracts/ViewModels"; +import Explorer from "../Explorer"; +import QueryTab from "./QueryTab"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; + +describe("Query Tab", () => { + function getNewQueryTabForContainer(container: Explorer): QueryTab { + const database = { + container: container, + id: ko.observable("test"), + isDatabaseShared: () => false, + } as ViewModels.Database; + const collection = { + container: container, + databaseId: "test", + id: ko.observable("test"), + } as ViewModels.Collection; + + return new QueryTab({ + tabKind: ViewModels.CollectionTabKind.Query, + collection: collection, + database: database, + title: "", + tabPath: "", + isActive: ko.observable(false), + hashLocation: "", + onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, + }); + } + + describe("shouldSetSystemPartitionKeyContainerPartitionKeyValueUndefined", () => { + const collection = { + id: ko.observable("withoutsystempk"), + partitionKey: { + systemKey: true, + }, + } as ViewModels.Collection; + + it("no container with system pk, should not set partition key option", () => { + const iteratorOptions = QueryTab.getIteratorOptions(collection); + expect(iteratorOptions.initialHeaders).toBeUndefined(); + }); + }); + + describe("isQueryMetricsEnabled()", () => { + let explorer: Explorer; + + beforeEach(() => { + explorer = new Explorer(); + }); + + it("should be true for accounts using SQL API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase()); + const queryTab = getNewQueryTabForContainer(explorer); + expect(queryTab.isQueryMetricsEnabled()).toBe(true); + }); + + it("should be false for accounts using other APIs", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase()); + const queryTab = getNewQueryTabForContainer(explorer); + expect(queryTab.isQueryMetricsEnabled()).toBe(false); + }); + }); + + describe("Save Queries command button", () => { + let explorer: Explorer; + + beforeEach(() => { + explorer = new Explorer(); + }); + + it("should be visible when using a supported API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB); + const queryTab = getNewQueryTabForContainer(explorer); + expect(queryTab.saveQueryButton.visible()).toBe(true); + }); + + it("should not be visible when using an unsupported API", () => { + explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB); + const queryTab = getNewQueryTabForContainer(explorer); + expect(queryTab.saveQueryButton.visible()).toBe(false); + }); + }); +}); diff --git a/src/Explorer/Tabs/QueryTab.ts b/src/Explorer/Tabs/QueryTab.ts index f8bd4ed7f..9ce56dfaa 100644 --- a/src/Explorer/Tabs/QueryTab.ts +++ b/src/Explorer/Tabs/QueryTab.ts @@ -1,606 +1,606 @@ -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import TabsBase from "./TabsBase"; -import { HashMap } from "../../Common/HashMap"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; -import { QueryUtils } from "../../Utils/QueryUtils"; -import SaveQueryIcon from "../../../images/save-cosmos.svg"; - -import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; -import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage"; - -enum ToggleState { - Result, - QueryMetrics -} - -export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate { - public queryEditorId: string; - public executeQueryButton: ViewModels.Button; - public fetchNextPageButton: ViewModels.Button; - public saveQueryButton: ViewModels.Button; - public initialEditorContent: ko.Observable; - public maybeSubQuery: ko.Computed; - public sqlQueryEditorContent: ko.Observable; - public selectedContent: ko.Observable; - public sqlStatementToExecute: ko.Observable; - public queryResults: ko.Observable; - public error: ko.Observable; - public statusMessge: ko.Observable; - public statusIcon: ko.Observable; - public allResultsMetadata: ko.ObservableArray; - public showingDocumentsDisplayText: ko.Observable; - public requestChargeDisplayText: ko.Observable; - public isTemplateReady: ko.Observable; - public splitterId: string; - public splitter: Splitter; - public isPreferredApiMongoDB: boolean; - - public queryMetrics: ko.Observable>; - public aggregatedQueryMetrics: ko.Observable; - public activityId: ko.Observable; - public roundTrips: ko.Observable; - public toggleState: ko.Observable; - public isQueryMetricsEnabled: ko.Computed; - - protected monacoSettings: ViewModels.MonacoEditorSettings; - private _executeQueryButtonTitle: ko.Observable; - protected _iterator: MinimalQueryIterator; - private _isSaveQueriesEnabled: ko.Computed; - private _resourceTokenPartitionKey: string; - - _partitionKey: DataModels.PartitionKey; - - constructor(options: ViewModels.QueryTabOptions) { - super(options); - this.queryEditorId = `queryeditor${this.tabId}`; - this.showingDocumentsDisplayText = ko.observable(); - this.requestChargeDisplayText = ko.observable(); - const defaultQueryText = options.queryText != void 0 ? options.queryText : "SELECT * FROM c"; - this.initialEditorContent = ko.observable(defaultQueryText); - this.sqlQueryEditorContent = ko.observable(defaultQueryText); - this._executeQueryButtonTitle = ko.observable("Execute Query"); - this.selectedContent = ko.observable(); - this.selectedContent.subscribe((selectedContent: string) => { - if (!selectedContent.trim()) { - this._executeQueryButtonTitle("Execute Query"); - } else { - this._executeQueryButtonTitle("Execute Selection"); - } - }); - this.sqlStatementToExecute = ko.observable(""); - this.queryResults = ko.observable(""); - this.statusMessge = ko.observable(); - this.statusIcon = ko.observable(); - this.allResultsMetadata = ko.observableArray([]); - this.error = ko.observable(); - this._partitionKey = options.partitionKey; - this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; - this.splitterId = this.tabId + "_splitter"; - this.isPreferredApiMongoDB = false; - this.aggregatedQueryMetrics = ko.observable(); - this._resetAggregateQueryMetrics(); - this.queryMetrics = ko.observable>(new HashMap()); - this.queryMetrics.subscribe((metrics: HashMap) => - this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)) - ); - this.isQueryMetricsEnabled = ko.computed(() => { - return ( - (this.collection && this.collection.container && this.collection.container.isPreferredApiDocumentDB()) || false - ); - }); - this.activityId = ko.observable(); - this.roundTrips = ko.observable(); - this.toggleState = ko.observable(ToggleState.Result); - - this.monacoSettings = new ViewModels.MonacoEditorSettings("sql", false); - - this.executeQueryButton = { - enabled: ko.computed(() => { - return !!this.sqlQueryEditorContent() && this.sqlQueryEditorContent().length > 0; - }), - - visible: ko.computed(() => { - return true; - }) - }; - - this._isSaveQueriesEnabled = ko.computed(() => { - const container = this.collection && this.collection.container; - return (container && (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph())) || false; - }); - - this.maybeSubQuery = ko.computed(function() { - const sql = this.sqlQueryEditorContent(); - return sql && /.*\(.*SELECT.*\)/i.test(sql); - }, this); - - this.saveQueryButton = { - enabled: this._isSaveQueriesEnabled, - visible: this._isSaveQueriesEnabled - }; - - super.onTemplateReady((isTemplateReady: boolean) => { - if (isTemplateReady) { - const splitterBounds: SplitterBounds = { - min: Constants.Queries.QueryEditorMinHeightRatio * window.innerHeight, - max: $("#" + this.tabId).height() - Constants.Queries.QueryEditorMaxHeightRatio * window.innerHeight - }; - this.splitter = new Splitter({ - splitterId: this.splitterId, - leftId: this.queryEditorId, - bounds: splitterBounds, - direction: SplitterDirection.Horizontal - }); - } - }); - - this.fetchNextPageButton = { - enabled: ko.computed(() => { - const allResultsMetadata = this.allResultsMetadata() || []; - const numberOfResultsMetadata = allResultsMetadata.length; - - if (numberOfResultsMetadata === 0) { - return false; - } - - if (allResultsMetadata[numberOfResultsMetadata - 1].hasMoreResults) { - return true; - } - - return false; - }), - - visible: ko.computed(() => { - return true; - }) - }; - - this._buildCommandBarOptions(); - } - - public onTabClick(): void { - super.onTabClick(); - this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); - } - - public onExecuteQueryClick = async (): Promise => { - const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent(); - this.sqlStatementToExecute(sqlStatement); - this.allResultsMetadata([]); - this.queryResults(""); - this._iterator = undefined; - - await this._executeQueryDocumentsPage(0); - }; - - public onLoadQueryClick = (): void => { - this.collection && this.collection.container && this.collection.container.loadQueryPane.open(); - }; - - public onSaveQueryClick = (): void => { - this.collection && this.collection.container && this.collection.container.saveQueryPane.open(); - }; - - public onSavedQueriesClick = (): void => { - this.collection && this.collection.container && this.collection.container.browseQueriesPane.open(); - }; - - public async onFetchNextPageClick(): Promise { - const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; - const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; - const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1; - const itemCount: number = (metadata && Number(metadata.itemCount)) || 0; - - await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); - } - - public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { - this.collection && this.collection.container.expandConsole(); - - return false; - }; - - public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.onErrorDetailsClick(src, null); - return false; - } - - return true; - }; - - public toggleResult(): void { - this.toggleState(ToggleState.Result); - this.queryResults.valueHasMutated(); // needed to refresh the json-editor component - } - - public toggleMetrics(): void { - this.toggleState(ToggleState.QueryMetrics); - } - - public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.LeftArrow) { - this.toggleResult(); - event.stopPropagation(); - return false; - } else if (event.keyCode === Constants.KeyCodes.RightArrow) { - this.toggleMetrics(); - event.stopPropagation(); - return false; - } - - return true; - }; - - public togglesOnFocus(): void { - const focusElement = document.getElementById("execute-query-toggles"); - focusElement && focusElement.focus(); - } - - public isResultToggled(): boolean { - return this.toggleState() === ToggleState.Result; - } - - public isMetricsToggled(): boolean { - return this.toggleState() === ToggleState.QueryMetrics; - } - - public onDownloadQueryMetricsCsvClick = (source: any, event: MouseEvent): boolean => { - this._downloadQueryMetricsCsvData(); - return false; - }; - - public onDownloadQueryMetricsCsvKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || Constants.KeyCodes.Enter) { - this._downloadQueryMetricsCsvData(); - return false; - } - - return true; - }; - - private async _executeQueryDocumentsPage(firstItemIndex: number): Promise { - this.error(""); - this.roundTrips(undefined); - if (this._iterator === undefined) { - this._initIterator(); - } - - await this._queryDocumentsPage(firstItemIndex); - } - - // TODO: Position and enable spinner when request is in progress - private async _queryDocumentsPage(firstItemIndex: number): Promise { - this.isExecutionError(false); - this._resetAggregateQueryMetrics(); - const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }); - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - - const queryDocuments = async (firstItemIndex: number) => - await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex); - this.isExecuting(true); - - try { - const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent( - firstItemIndex, - queryDocuments - ); - const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; - const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; - const resultsMetadata: ViewModels.QueryResultsMetadata = { - hasMoreResults: queryResults.hasMoreResults, - itemCount: queryResults.itemCount, - firstItemIndex: queryResults.firstItemIndex, - lastItemIndex: queryResults.lastItemIndex - }; - this.allResultsMetadata.push(resultsMetadata); - this.activityId(queryResults.activityId); - this.roundTrips(queryResults.roundTrips); - - this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); - - if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) { - // we let users query for the next page because the SDK sometimes specifies there are more elements - // even though there aren't any so we should not update the prior query results. - return; - } - - const documents: any[] = queryResults.documents; - const results = this.renderObjectForEditor(documents, null, 4); - - const resultsDisplay: string = - queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; - this.showingDocumentsDisplayText(resultsDisplay); - this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`); - this.queryResults(results); - - TelemetryProcessor.traceSuccess( - Action.ExecuteQuery, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - } catch (error) { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - this.error(errorMessage); - TelemetryProcessor.traceFailure( - Action.ExecuteQuery, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - document.getElementById("error-display").focus(); - } finally { - this.isExecuting(false); - this.togglesOnFocus(); - } - } - - private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { - if (!metricsMap) { - return; - } - - Object.keys(metricsMap).forEach((key: string) => { - this.queryMetrics().set(key, metricsMap[key]); - }); - this.queryMetrics.valueHasMutated(); - } - - private _aggregateQueryMetrics(metricsMap: HashMap): DataModels.QueryMetrics { - if (!metricsMap) { - return null; - } - - const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics(); - metricsMap.forEach((partitionKeyRangeId: string, queryMetrics: DataModels.QueryMetrics) => { - if (queryMetrics) { - aggregatedMetrics.documentLoadTime = - queryMetrics.documentLoadTime && - this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.documentLoadTime); - aggregatedMetrics.documentWriteTime = - queryMetrics.documentWriteTime && - this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.documentWriteTime); - aggregatedMetrics.indexHitDocumentCount = - queryMetrics.indexHitDocumentCount && - this._normalize(queryMetrics.indexHitDocumentCount) + - this._normalize(aggregatedMetrics.indexHitDocumentCount); - aggregatedMetrics.outputDocumentCount = - queryMetrics.outputDocumentCount && - this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount); - aggregatedMetrics.outputDocumentSize = - queryMetrics.outputDocumentSize && - this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize); - aggregatedMetrics.indexLookupTime = - queryMetrics.indexLookupTime && - this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.indexLookupTime); - aggregatedMetrics.retrievedDocumentCount = - queryMetrics.retrievedDocumentCount && - this._normalize(queryMetrics.retrievedDocumentCount) + - this._normalize(aggregatedMetrics.retrievedDocumentCount); - aggregatedMetrics.retrievedDocumentSize = - queryMetrics.retrievedDocumentSize && - this._normalize(queryMetrics.retrievedDocumentSize) + - this._normalize(aggregatedMetrics.retrievedDocumentSize); - aggregatedMetrics.vmExecutionTime = - queryMetrics.vmExecutionTime && - this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.vmExecutionTime); - aggregatedMetrics.totalQueryExecutionTime = - queryMetrics.totalQueryExecutionTime && - this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.totalQueryExecutionTime); - - aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime = - aggregatedMetrics.runtimeExecutionTimes && - this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime); - aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime = - aggregatedMetrics.runtimeExecutionTimes && - this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime); - aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime = - aggregatedMetrics.runtimeExecutionTimes && - this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) + - this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime); - } - }); - - return aggregatedMetrics; - } - - public _downloadQueryMetricsCsvData(): void { - const csvData: string = this._generateQueryMetricsCsvData(); - if (!csvData) { - return; - } - - if (navigator.msSaveBlob) { - // for IE and Edge - navigator.msSaveBlob( - new Blob([csvData], { type: "data:text/csv;charset=utf-8" }), - "PerPartitionQueryMetrics.csv" - ); - } else { - const downloadLink: HTMLAnchorElement = document.createElement("a"); - downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData); - downloadLink.target = "_self"; - downloadLink.download = "QueryMetricsPerPartition.csv"; - - // for some reason, FF displays the download prompt only when - // the link is added to the dom so we add and remove it - document.body.appendChild(downloadLink); - downloadLink.click(); - downloadLink.remove(); - } - } - - protected _initIterator(): void { - const options: any = QueryTab.getIteratorOptions(this.collection); - if (this._resourceTokenPartitionKey) { - options.partitionKey = this._resourceTokenPartitionKey; - } - - this._iterator = queryDocuments( - this.collection.databaseId, - this.collection.id(), - this.sqlStatementToExecute(), - options - ); - } - - public static getIteratorOptions(container: ViewModels.CollectionBase): any { - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - return options; - } - - private _normalize(value: number): number { - if (!value) { - return 0; - } - - return value; - } - - private _resetAggregateQueryMetrics(): void { - this.aggregatedQueryMetrics({ - clientSideMetrics: {}, - documentLoadTime: undefined, - documentWriteTime: undefined, - indexHitDocumentCount: undefined, - outputDocumentCount: undefined, - outputDocumentSize: undefined, - indexLookupTime: undefined, - retrievedDocumentCount: undefined, - retrievedDocumentSize: undefined, - vmExecutionTime: undefined, - queryPreparationTimes: undefined, - runtimeExecutionTimes: { - queryEngineExecutionTime: undefined, - systemFunctionExecutionTime: undefined, - userDefinedFunctionExecutionTime: undefined - }, - totalQueryExecutionTime: undefined - }); - } - - private _generateQueryMetricsCsvData(): string { - if (!this.queryMetrics()) { - return null; - } - - const queryMetrics: HashMap = this.queryMetrics(); - let csvData: string = ""; - const columnHeaders: string = - [ - "Partition key range id", - "Retrieved document count", - "Retrieved document size (in bytes)", - "Output document count", - "Output document size (in bytes)", - "Index hit document count", - "Index lookup time (ms)", - "Document load time (ms)", - "Query engine execution time (ms)", - "System function execution time (ms)", - "User defined function execution time (ms)", - "Document write time (ms)" - ].join(",") + "\n"; - csvData = csvData + columnHeaders; - queryMetrics.forEach((partitionKeyRangeId: string, queryMetric: DataModels.QueryMetrics) => { - const partitionKeyRangeData: string = - [ - partitionKeyRangeId, - queryMetric.retrievedDocumentCount, - queryMetric.retrievedDocumentSize, - queryMetric.outputDocumentCount, - queryMetric.outputDocumentSize, - queryMetric.indexHitDocumentCount, - queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(), - queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(), - queryMetric.runtimeExecutionTimes && - queryMetric.runtimeExecutionTimes.queryEngineExecutionTime && - queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(), - queryMetric.runtimeExecutionTimes && - queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime && - queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(), - queryMetric.runtimeExecutionTimes && - queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime && - queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(), - queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds() - ].join(",") + "\n"; - csvData = csvData + partitionKeyRangeData; - }); - - return csvData; - } - - protected getTabsButtons(): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = []; - if (this.executeQueryButton.visible()) { - const label = this._executeQueryButtonTitle(); - buttons.push({ - iconSrc: ExecuteQueryIcon, - iconAlt: label, - onCommandClick: this.onExecuteQueryClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.executeQueryButton.enabled() - }); - } - - if (this.saveQueryButton.visible()) { - const label = "Save Query"; - buttons.push({ - iconSrc: SaveQueryIcon, - iconAlt: label, - onCommandClick: this.onSaveQueryClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.saveQueryButton.enabled() - }); - } - - return buttons; - } - - private _buildCommandBarOptions(): void { - ko.computed(() => - ko.toJSON([this.executeQueryButton.visible, this.executeQueryButton.enabled, this._executeQueryButtonTitle]) - ).subscribe(() => this.updateNavbarWithTabsButtons()); - this.updateNavbarWithTabsButtons(); - } -} +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import TabsBase from "./TabsBase"; +import { HashMap } from "../../Common/HashMap"; +import * as HeadersUtility from "../../Common/HeadersUtility"; +import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; +import { QueryUtils } from "../../Utils/QueryUtils"; +import SaveQueryIcon from "../../../images/save-cosmos.svg"; + +import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; +import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage"; + +enum ToggleState { + Result, + QueryMetrics, +} + +export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate { + public queryEditorId: string; + public executeQueryButton: ViewModels.Button; + public fetchNextPageButton: ViewModels.Button; + public saveQueryButton: ViewModels.Button; + public initialEditorContent: ko.Observable; + public maybeSubQuery: ko.Computed; + public sqlQueryEditorContent: ko.Observable; + public selectedContent: ko.Observable; + public sqlStatementToExecute: ko.Observable; + public queryResults: ko.Observable; + public error: ko.Observable; + public statusMessge: ko.Observable; + public statusIcon: ko.Observable; + public allResultsMetadata: ko.ObservableArray; + public showingDocumentsDisplayText: ko.Observable; + public requestChargeDisplayText: ko.Observable; + public isTemplateReady: ko.Observable; + public splitterId: string; + public splitter: Splitter; + public isPreferredApiMongoDB: boolean; + + public queryMetrics: ko.Observable>; + public aggregatedQueryMetrics: ko.Observable; + public activityId: ko.Observable; + public roundTrips: ko.Observable; + public toggleState: ko.Observable; + public isQueryMetricsEnabled: ko.Computed; + + protected monacoSettings: ViewModels.MonacoEditorSettings; + private _executeQueryButtonTitle: ko.Observable; + protected _iterator: MinimalQueryIterator; + private _isSaveQueriesEnabled: ko.Computed; + private _resourceTokenPartitionKey: string; + + _partitionKey: DataModels.PartitionKey; + + constructor(options: ViewModels.QueryTabOptions) { + super(options); + this.queryEditorId = `queryeditor${this.tabId}`; + this.showingDocumentsDisplayText = ko.observable(); + this.requestChargeDisplayText = ko.observable(); + const defaultQueryText = options.queryText != void 0 ? options.queryText : "SELECT * FROM c"; + this.initialEditorContent = ko.observable(defaultQueryText); + this.sqlQueryEditorContent = ko.observable(defaultQueryText); + this._executeQueryButtonTitle = ko.observable("Execute Query"); + this.selectedContent = ko.observable(); + this.selectedContent.subscribe((selectedContent: string) => { + if (!selectedContent.trim()) { + this._executeQueryButtonTitle("Execute Query"); + } else { + this._executeQueryButtonTitle("Execute Selection"); + } + }); + this.sqlStatementToExecute = ko.observable(""); + this.queryResults = ko.observable(""); + this.statusMessge = ko.observable(); + this.statusIcon = ko.observable(); + this.allResultsMetadata = ko.observableArray([]); + this.error = ko.observable(); + this._partitionKey = options.partitionKey; + this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; + this.splitterId = this.tabId + "_splitter"; + this.isPreferredApiMongoDB = false; + this.aggregatedQueryMetrics = ko.observable(); + this._resetAggregateQueryMetrics(); + this.queryMetrics = ko.observable>(new HashMap()); + this.queryMetrics.subscribe((metrics: HashMap) => + this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)) + ); + this.isQueryMetricsEnabled = ko.computed(() => { + return ( + (this.collection && this.collection.container && this.collection.container.isPreferredApiDocumentDB()) || false + ); + }); + this.activityId = ko.observable(); + this.roundTrips = ko.observable(); + this.toggleState = ko.observable(ToggleState.Result); + + this.monacoSettings = new ViewModels.MonacoEditorSettings("sql", false); + + this.executeQueryButton = { + enabled: ko.computed(() => { + return !!this.sqlQueryEditorContent() && this.sqlQueryEditorContent().length > 0; + }), + + visible: ko.computed(() => { + return true; + }), + }; + + this._isSaveQueriesEnabled = ko.computed(() => { + const container = this.collection && this.collection.container; + return (container && (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph())) || false; + }); + + this.maybeSubQuery = ko.computed(function () { + const sql = this.sqlQueryEditorContent(); + return sql && /.*\(.*SELECT.*\)/i.test(sql); + }, this); + + this.saveQueryButton = { + enabled: this._isSaveQueriesEnabled, + visible: this._isSaveQueriesEnabled, + }; + + super.onTemplateReady((isTemplateReady: boolean) => { + if (isTemplateReady) { + const splitterBounds: SplitterBounds = { + min: Constants.Queries.QueryEditorMinHeightRatio * window.innerHeight, + max: $("#" + this.tabId).height() - Constants.Queries.QueryEditorMaxHeightRatio * window.innerHeight, + }; + this.splitter = new Splitter({ + splitterId: this.splitterId, + leftId: this.queryEditorId, + bounds: splitterBounds, + direction: SplitterDirection.Horizontal, + }); + } + }); + + this.fetchNextPageButton = { + enabled: ko.computed(() => { + const allResultsMetadata = this.allResultsMetadata() || []; + const numberOfResultsMetadata = allResultsMetadata.length; + + if (numberOfResultsMetadata === 0) { + return false; + } + + if (allResultsMetadata[numberOfResultsMetadata - 1].hasMoreResults) { + return true; + } + + return false; + }), + + visible: ko.computed(() => { + return true; + }), + }; + + this._buildCommandBarOptions(); + } + + public onTabClick(): void { + super.onTabClick(); + this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); + } + + public onExecuteQueryClick = async (): Promise => { + const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent(); + this.sqlStatementToExecute(sqlStatement); + this.allResultsMetadata([]); + this.queryResults(""); + this._iterator = undefined; + + await this._executeQueryDocumentsPage(0); + }; + + public onLoadQueryClick = (): void => { + this.collection && this.collection.container && this.collection.container.loadQueryPane.open(); + }; + + public onSaveQueryClick = (): void => { + this.collection && this.collection.container && this.collection.container.saveQueryPane.open(); + }; + + public onSavedQueriesClick = (): void => { + this.collection && this.collection.container && this.collection.container.browseQueriesPane.open(); + }; + + public async onFetchNextPageClick(): Promise { + const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; + const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; + const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1; + const itemCount: number = (metadata && Number(metadata.itemCount)) || 0; + + await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); + } + + public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { + this.collection && this.collection.container.expandConsole(); + + return false; + }; + + public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.onErrorDetailsClick(src, null); + return false; + } + + return true; + }; + + public toggleResult(): void { + this.toggleState(ToggleState.Result); + this.queryResults.valueHasMutated(); // needed to refresh the json-editor component + } + + public toggleMetrics(): void { + this.toggleState(ToggleState.QueryMetrics); + } + + public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.LeftArrow) { + this.toggleResult(); + event.stopPropagation(); + return false; + } else if (event.keyCode === Constants.KeyCodes.RightArrow) { + this.toggleMetrics(); + event.stopPropagation(); + return false; + } + + return true; + }; + + public togglesOnFocus(): void { + const focusElement = document.getElementById("execute-query-toggles"); + focusElement && focusElement.focus(); + } + + public isResultToggled(): boolean { + return this.toggleState() === ToggleState.Result; + } + + public isMetricsToggled(): boolean { + return this.toggleState() === ToggleState.QueryMetrics; + } + + public onDownloadQueryMetricsCsvClick = (source: any, event: MouseEvent): boolean => { + this._downloadQueryMetricsCsvData(); + return false; + }; + + public onDownloadQueryMetricsCsvKeyPress = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || Constants.KeyCodes.Enter) { + this._downloadQueryMetricsCsvData(); + return false; + } + + return true; + }; + + private async _executeQueryDocumentsPage(firstItemIndex: number): Promise { + this.error(""); + this.roundTrips(undefined); + if (this._iterator === undefined) { + this._initIterator(); + } + + await this._queryDocumentsPage(firstItemIndex); + } + + // TODO: Position and enable spinner when request is in progress + private async _queryDocumentsPage(firstItemIndex: number): Promise { + this.isExecutionError(false); + this._resetAggregateQueryMetrics(); + const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }); + let options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + + const queryDocuments = async (firstItemIndex: number) => + await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex); + this.isExecuting(true); + + try { + const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent( + firstItemIndex, + queryDocuments + ); + const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; + const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; + const resultsMetadata: ViewModels.QueryResultsMetadata = { + hasMoreResults: queryResults.hasMoreResults, + itemCount: queryResults.itemCount, + firstItemIndex: queryResults.firstItemIndex, + lastItemIndex: queryResults.lastItemIndex, + }; + this.allResultsMetadata.push(resultsMetadata); + this.activityId(queryResults.activityId); + this.roundTrips(queryResults.roundTrips); + + this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); + + if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) { + // we let users query for the next page because the SDK sometimes specifies there are more elements + // even though there aren't any so we should not update the prior query results. + return; + } + + const documents: any[] = queryResults.documents; + const results = this.renderObjectForEditor(documents, null, 4); + + const resultsDisplay: string = + queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; + this.showingDocumentsDisplayText(resultsDisplay); + this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`); + this.queryResults(results); + + TelemetryProcessor.traceSuccess( + Action.ExecuteQuery, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }, + startKey + ); + } catch (error) { + this.isExecutionError(true); + const errorMessage = getErrorMessage(error); + this.error(errorMessage); + TelemetryProcessor.traceFailure( + Action.ExecuteQuery, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + document.getElementById("error-display").focus(); + } finally { + this.isExecuting(false); + this.togglesOnFocus(); + } + } + + private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { + if (!metricsMap) { + return; + } + + Object.keys(metricsMap).forEach((key: string) => { + this.queryMetrics().set(key, metricsMap[key]); + }); + this.queryMetrics.valueHasMutated(); + } + + private _aggregateQueryMetrics(metricsMap: HashMap): DataModels.QueryMetrics { + if (!metricsMap) { + return null; + } + + const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics(); + metricsMap.forEach((partitionKeyRangeId: string, queryMetrics: DataModels.QueryMetrics) => { + if (queryMetrics) { + aggregatedMetrics.documentLoadTime = + queryMetrics.documentLoadTime && + this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.documentLoadTime); + aggregatedMetrics.documentWriteTime = + queryMetrics.documentWriteTime && + this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.documentWriteTime); + aggregatedMetrics.indexHitDocumentCount = + queryMetrics.indexHitDocumentCount && + this._normalize(queryMetrics.indexHitDocumentCount) + + this._normalize(aggregatedMetrics.indexHitDocumentCount); + aggregatedMetrics.outputDocumentCount = + queryMetrics.outputDocumentCount && + this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount); + aggregatedMetrics.outputDocumentSize = + queryMetrics.outputDocumentSize && + this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize); + aggregatedMetrics.indexLookupTime = + queryMetrics.indexLookupTime && + this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.indexLookupTime); + aggregatedMetrics.retrievedDocumentCount = + queryMetrics.retrievedDocumentCount && + this._normalize(queryMetrics.retrievedDocumentCount) + + this._normalize(aggregatedMetrics.retrievedDocumentCount); + aggregatedMetrics.retrievedDocumentSize = + queryMetrics.retrievedDocumentSize && + this._normalize(queryMetrics.retrievedDocumentSize) + + this._normalize(aggregatedMetrics.retrievedDocumentSize); + aggregatedMetrics.vmExecutionTime = + queryMetrics.vmExecutionTime && + this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.vmExecutionTime); + aggregatedMetrics.totalQueryExecutionTime = + queryMetrics.totalQueryExecutionTime && + this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.totalQueryExecutionTime); + + aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime = + aggregatedMetrics.runtimeExecutionTimes && + this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime); + aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime = + aggregatedMetrics.runtimeExecutionTimes && + this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime); + aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime = + aggregatedMetrics.runtimeExecutionTimes && + this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime); + } + }); + + return aggregatedMetrics; + } + + public _downloadQueryMetricsCsvData(): void { + const csvData: string = this._generateQueryMetricsCsvData(); + if (!csvData) { + return; + } + + if (navigator.msSaveBlob) { + // for IE and Edge + navigator.msSaveBlob( + new Blob([csvData], { type: "data:text/csv;charset=utf-8" }), + "PerPartitionQueryMetrics.csv" + ); + } else { + const downloadLink: HTMLAnchorElement = document.createElement("a"); + downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData); + downloadLink.target = "_self"; + downloadLink.download = "QueryMetricsPerPartition.csv"; + + // for some reason, FF displays the download prompt only when + // the link is added to the dom so we add and remove it + document.body.appendChild(downloadLink); + downloadLink.click(); + downloadLink.remove(); + } + } + + protected _initIterator(): void { + const options: any = QueryTab.getIteratorOptions(this.collection); + if (this._resourceTokenPartitionKey) { + options.partitionKey = this._resourceTokenPartitionKey; + } + + this._iterator = queryDocuments( + this.collection.databaseId, + this.collection.id(), + this.sqlStatementToExecute(), + options + ); + } + + public static getIteratorOptions(container: ViewModels.CollectionBase): any { + let options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + return options; + } + + private _normalize(value: number): number { + if (!value) { + return 0; + } + + return value; + } + + private _resetAggregateQueryMetrics(): void { + this.aggregatedQueryMetrics({ + clientSideMetrics: {}, + documentLoadTime: undefined, + documentWriteTime: undefined, + indexHitDocumentCount: undefined, + outputDocumentCount: undefined, + outputDocumentSize: undefined, + indexLookupTime: undefined, + retrievedDocumentCount: undefined, + retrievedDocumentSize: undefined, + vmExecutionTime: undefined, + queryPreparationTimes: undefined, + runtimeExecutionTimes: { + queryEngineExecutionTime: undefined, + systemFunctionExecutionTime: undefined, + userDefinedFunctionExecutionTime: undefined, + }, + totalQueryExecutionTime: undefined, + }); + } + + private _generateQueryMetricsCsvData(): string { + if (!this.queryMetrics()) { + return null; + } + + const queryMetrics: HashMap = this.queryMetrics(); + let csvData: string = ""; + const columnHeaders: string = + [ + "Partition key range id", + "Retrieved document count", + "Retrieved document size (in bytes)", + "Output document count", + "Output document size (in bytes)", + "Index hit document count", + "Index lookup time (ms)", + "Document load time (ms)", + "Query engine execution time (ms)", + "System function execution time (ms)", + "User defined function execution time (ms)", + "Document write time (ms)", + ].join(",") + "\n"; + csvData = csvData + columnHeaders; + queryMetrics.forEach((partitionKeyRangeId: string, queryMetric: DataModels.QueryMetrics) => { + const partitionKeyRangeData: string = + [ + partitionKeyRangeId, + queryMetric.retrievedDocumentCount, + queryMetric.retrievedDocumentSize, + queryMetric.outputDocumentCount, + queryMetric.outputDocumentSize, + queryMetric.indexHitDocumentCount, + queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(), + queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(), + queryMetric.runtimeExecutionTimes && + queryMetric.runtimeExecutionTimes.queryEngineExecutionTime && + queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(), + queryMetric.runtimeExecutionTimes && + queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime && + queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(), + queryMetric.runtimeExecutionTimes && + queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime && + queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(), + queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(), + ].join(",") + "\n"; + csvData = csvData + partitionKeyRangeData; + }); + + return csvData; + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + if (this.executeQueryButton.visible()) { + const label = this._executeQueryButtonTitle(); + buttons.push({ + iconSrc: ExecuteQueryIcon, + iconAlt: label, + onCommandClick: this.onExecuteQueryClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.executeQueryButton.enabled(), + }); + } + + if (this.saveQueryButton.visible()) { + const label = "Save Query"; + buttons.push({ + iconSrc: SaveQueryIcon, + iconAlt: label, + onCommandClick: this.onSaveQueryClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveQueryButton.enabled(), + }); + } + + return buttons; + } + + private _buildCommandBarOptions(): void { + ko.computed(() => + ko.toJSON([this.executeQueryButton.visible, this.executeQueryButton.enabled, this._executeQueryButtonTitle]) + ).subscribe(() => this.updateNavbarWithTabsButtons()); + this.updateNavbarWithTabsButtons(); + } +} diff --git a/src/Explorer/Tabs/QueryTablesTab.html b/src/Explorer/Tabs/QueryTablesTab.html index 28f99e440..614828400 100644 --- a/src/Explorer/Tabs/QueryTablesTab.html +++ b/src/Explorer/Tabs/QueryTablesTab.html @@ -1,249 +1,278 @@ -
- -
- -
-
- - -
-
- - -
-
- -
-
- - -
-
-
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - -
-
-
-
- - Add new clause - - -
-
-
-
- - -
-
- - -
- toggle -
- - -
- toggle -
- - Advanced Options -
-
-
-
- Show top results: - - -
-
- Select fields for query: -
- - -
- - Choose Columns... - -
-
-
- -
- -
- -
-
-
- - - - - - +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
+
+
+ + Add new clause + + +
+
+
+
+ + +
+
+ + +
+ toggle +
+ + +
+ toggle +
+ + Advanced Options +
+
+
+
+ Show top results: + + +
+
+ Select fields for query: +
+ + +
+ + Choose Columns... + +
+
+
+ +
+ +
+ +
+
+
+ + + + + + diff --git a/src/Explorer/Tabs/QueryTablesTab.ts b/src/Explorer/Tabs/QueryTablesTab.ts index c68091ab6..ae71b9fc1 100644 --- a/src/Explorer/Tabs/QueryTablesTab.ts +++ b/src/Explorer/Tabs/QueryTablesTab.ts @@ -1,279 +1,277 @@ -import * as ko from "knockout"; -import Q from "q"; -import * as ViewModels from "../../Contracts/ViewModels"; -import TabsBase from "./TabsBase"; -import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel"; -import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel"; -import TableCommands from "../Tables/DataTable/TableCommands"; -import { TableDataClient } from "../Tables/TableDataClient"; - -import QueryBuilderIcon from "../../../images/Query-Builder.svg"; -import QueryTextIcon from "../../../images/Query-Text.svg"; -import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; -import AddEntityIcon from "../../../images/AddEntity.svg"; -import EditEntityIcon from "../../../images/Edit-entity.svg"; -import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg"; -import Explorer from "../Explorer"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; - -// Will act as table explorer class -export default class QueryTablesTab extends TabsBase { - public collection: ViewModels.Collection; - public tableEntityListViewModel = ko.observable(); - public queryViewModel = ko.observable(); - public tableCommands: TableCommands; - public tableDataClient: TableDataClient; - - public queryText = ko.observable("PartitionKey eq 'partitionKey1'"); // Start out with an example they can modify - public selectedQueryText = ko.observable("").extend({ notify: "always" }); - - public executeQueryButton: ViewModels.Button; - public addEntityButton: ViewModels.Button; - public editEntityButton: ViewModels.Button; - public deleteEntityButton: ViewModels.Button; - public queryBuilderButton: ViewModels.Button; - public queryTextButton: ViewModels.Button; - public container: Explorer; - - constructor(options: ViewModels.TabOptions) { - super(options); - - this.container = options.collection && options.collection.container; - this.tableCommands = new TableCommands(this.container); - this.tableDataClient = this.container.tableDataClient; - this.tableEntityListViewModel(new TableEntityListViewModel(this.tableCommands, this)); - this.tableEntityListViewModel().queryTablesTab = this; - this.queryViewModel(new QueryViewModel(this)); - const sampleQuerySubscription = this.tableEntityListViewModel().items.subscribe(() => { - if (this.tableEntityListViewModel().items().length > 0 && this.container.isPreferredApiTable()) { - this.queryViewModel() - .queryBuilderViewModel() - .setExample(); - } - sampleQuerySubscription.dispose(); - }); - - this.executeQueryButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }) - }; - - this.queryBuilderButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }), - - isSelected: ko.computed(() => { - return this.queryViewModel() ? this.queryViewModel().isHelperActive() : false; - }) - }; - - this.queryTextButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }), - - isSelected: ko.computed(() => { - return this.queryViewModel() ? this.queryViewModel().isEditorActive() : false; - }) - }; - - this.addEntityButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }) - }; - - this.editEntityButton = { - enabled: ko.computed(() => { - return this.tableCommands.isEnabled( - TableCommands.editEntityCommand, - this.tableEntityListViewModel().selected() - ); - }), - - visible: ko.computed(() => { - return true; - }) - }; - - this.deleteEntityButton = { - enabled: ko.computed(() => { - return this.tableCommands.isEnabled( - TableCommands.deleteEntitiesCommand, - this.tableEntityListViewModel().selected() - ); - }), - - visible: ko.computed(() => { - return true; - }) - }; - - this.buildCommandBarOptions(); - } - - public onExecuteQueryClick = (): Q.Promise => { - this.queryViewModel().runQuery(); - return null; - }; - - public onQueryBuilderClick = (): Q.Promise => { - this.queryViewModel().selectHelper(); - return null; - }; - - public onQueryTextClick = (): Q.Promise => { - this.queryViewModel().selectEditor(); - return null; - }; - - public onAddEntityClick = (): Q.Promise => { - this.container.addTableEntityPane.tableViewModel = this.tableEntityListViewModel(); - this.container.addTableEntityPane.open(); - return null; - }; - - public onEditEntityClick = (): Q.Promise => { - this.tableCommands.editEntityCommand(this.tableEntityListViewModel()); - return null; - }; - - public onDeleteEntityClick = (): Q.Promise => { - this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel()); - return null; - }; - - public onActivate(): void { - super.onActivate(); - const columns = - !!this.tableEntityListViewModel() && - !!this.tableEntityListViewModel().table && - this.tableEntityListViewModel().table.columns; - if (!!columns) { - columns.adjust(); - $(window).resize(); - } - } - - protected getTabsButtons(): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = []; - if (this.queryBuilderButton.visible()) { - const label = this.container.isPreferredApiCassandra() ? "CQL Query Builder" : "Query Builder"; - buttons.push({ - iconSrc: QueryBuilderIcon, - iconAlt: label, - onCommandClick: this.onQueryBuilderClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.queryBuilderButton.enabled(), - isSelected: this.queryBuilderButton.isSelected() - }); - } - - if (this.queryTextButton.visible()) { - const label = this.container.isPreferredApiCassandra() ? "CQL Query Text" : "Query Text"; - buttons.push({ - iconSrc: QueryTextIcon, - iconAlt: label, - onCommandClick: this.onQueryTextClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.queryTextButton.enabled(), - isSelected: this.queryTextButton.isSelected() - }); - } - - if (this.executeQueryButton.visible()) { - const label = "Run Query"; - buttons.push({ - iconSrc: ExecuteQueryIcon, - iconAlt: label, - onCommandClick: this.onExecuteQueryClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.executeQueryButton.enabled() - }); - } - - if (this.addEntityButton.visible()) { - const label = this.container.isPreferredApiCassandra() ? "Add Row" : "Add Entity"; - buttons.push({ - iconSrc: AddEntityIcon, - iconAlt: label, - onCommandClick: this.onAddEntityClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: true, - disabled: !this.addEntityButton.enabled() - }); - } - - if (this.editEntityButton.visible()) { - const label = this.container.isPreferredApiCassandra() ? "Edit Row" : "Edit Entity"; - buttons.push({ - iconSrc: EditEntityIcon, - iconAlt: label, - onCommandClick: this.onEditEntityClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: true, - disabled: !this.editEntityButton.enabled() - }); - } - - if (this.deleteEntityButton.visible()) { - const label = this.container.isPreferredApiCassandra() ? "Delete Rows" : "Delete Entities"; - buttons.push({ - iconSrc: DeleteEntitiesIcon, - iconAlt: label, - onCommandClick: this.onDeleteEntityClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: true, - disabled: !this.deleteEntityButton.enabled() - }); - } - return buttons; - } - - protected buildCommandBarOptions(): void { - ko.computed(() => - ko.toJSON([ - this.queryBuilderButton.visible, - this.queryBuilderButton.enabled, - this.queryTextButton.visible, - this.queryTextButton.enabled, - this.executeQueryButton.visible, - this.executeQueryButton.enabled, - this.addEntityButton.visible, - this.addEntityButton.enabled, - this.editEntityButton.visible, - this.editEntityButton.enabled, - this.deleteEntityButton.visible, - this.deleteEntityButton.enabled - ]) - ).subscribe(() => this.updateNavbarWithTabsButtons()); - this.updateNavbarWithTabsButtons(); - } -} +import * as ko from "knockout"; +import Q from "q"; +import * as ViewModels from "../../Contracts/ViewModels"; +import TabsBase from "./TabsBase"; +import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel"; +import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel"; +import TableCommands from "../Tables/DataTable/TableCommands"; +import { TableDataClient } from "../Tables/TableDataClient"; + +import QueryBuilderIcon from "../../../images/Query-Builder.svg"; +import QueryTextIcon from "../../../images/Query-Text.svg"; +import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; +import AddEntityIcon from "../../../images/AddEntity.svg"; +import EditEntityIcon from "../../../images/Edit-entity.svg"; +import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg"; +import Explorer from "../Explorer"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; + +// Will act as table explorer class +export default class QueryTablesTab extends TabsBase { + public collection: ViewModels.Collection; + public tableEntityListViewModel = ko.observable(); + public queryViewModel = ko.observable(); + public tableCommands: TableCommands; + public tableDataClient: TableDataClient; + + public queryText = ko.observable("PartitionKey eq 'partitionKey1'"); // Start out with an example they can modify + public selectedQueryText = ko.observable("").extend({ notify: "always" }); + + public executeQueryButton: ViewModels.Button; + public addEntityButton: ViewModels.Button; + public editEntityButton: ViewModels.Button; + public deleteEntityButton: ViewModels.Button; + public queryBuilderButton: ViewModels.Button; + public queryTextButton: ViewModels.Button; + public container: Explorer; + + constructor(options: ViewModels.TabOptions) { + super(options); + + this.container = options.collection && options.collection.container; + this.tableCommands = new TableCommands(this.container); + this.tableDataClient = this.container.tableDataClient; + this.tableEntityListViewModel(new TableEntityListViewModel(this.tableCommands, this)); + this.tableEntityListViewModel().queryTablesTab = this; + this.queryViewModel(new QueryViewModel(this)); + const sampleQuerySubscription = this.tableEntityListViewModel().items.subscribe(() => { + if (this.tableEntityListViewModel().items().length > 0 && this.container.isPreferredApiTable()) { + this.queryViewModel().queryBuilderViewModel().setExample(); + } + sampleQuerySubscription.dispose(); + }); + + this.executeQueryButton = { + enabled: ko.computed(() => { + return true; + }), + + visible: ko.computed(() => { + return true; + }), + }; + + this.queryBuilderButton = { + enabled: ko.computed(() => { + return true; + }), + + visible: ko.computed(() => { + return true; + }), + + isSelected: ko.computed(() => { + return this.queryViewModel() ? this.queryViewModel().isHelperActive() : false; + }), + }; + + this.queryTextButton = { + enabled: ko.computed(() => { + return true; + }), + + visible: ko.computed(() => { + return true; + }), + + isSelected: ko.computed(() => { + return this.queryViewModel() ? this.queryViewModel().isEditorActive() : false; + }), + }; + + this.addEntityButton = { + enabled: ko.computed(() => { + return true; + }), + + visible: ko.computed(() => { + return true; + }), + }; + + this.editEntityButton = { + enabled: ko.computed(() => { + return this.tableCommands.isEnabled( + TableCommands.editEntityCommand, + this.tableEntityListViewModel().selected() + ); + }), + + visible: ko.computed(() => { + return true; + }), + }; + + this.deleteEntityButton = { + enabled: ko.computed(() => { + return this.tableCommands.isEnabled( + TableCommands.deleteEntitiesCommand, + this.tableEntityListViewModel().selected() + ); + }), + + visible: ko.computed(() => { + return true; + }), + }; + + this.buildCommandBarOptions(); + } + + public onExecuteQueryClick = (): Q.Promise => { + this.queryViewModel().runQuery(); + return null; + }; + + public onQueryBuilderClick = (): Q.Promise => { + this.queryViewModel().selectHelper(); + return null; + }; + + public onQueryTextClick = (): Q.Promise => { + this.queryViewModel().selectEditor(); + return null; + }; + + public onAddEntityClick = (): Q.Promise => { + this.container.addTableEntityPane.tableViewModel = this.tableEntityListViewModel(); + this.container.addTableEntityPane.open(); + return null; + }; + + public onEditEntityClick = (): Q.Promise => { + this.tableCommands.editEntityCommand(this.tableEntityListViewModel()); + return null; + }; + + public onDeleteEntityClick = (): Q.Promise => { + this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel()); + return null; + }; + + public onActivate(): void { + super.onActivate(); + const columns = + !!this.tableEntityListViewModel() && + !!this.tableEntityListViewModel().table && + this.tableEntityListViewModel().table.columns; + if (!!columns) { + columns.adjust(); + $(window).resize(); + } + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + if (this.queryBuilderButton.visible()) { + const label = this.container.isPreferredApiCassandra() ? "CQL Query Builder" : "Query Builder"; + buttons.push({ + iconSrc: QueryBuilderIcon, + iconAlt: label, + onCommandClick: this.onQueryBuilderClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.queryBuilderButton.enabled(), + isSelected: this.queryBuilderButton.isSelected(), + }); + } + + if (this.queryTextButton.visible()) { + const label = this.container.isPreferredApiCassandra() ? "CQL Query Text" : "Query Text"; + buttons.push({ + iconSrc: QueryTextIcon, + iconAlt: label, + onCommandClick: this.onQueryTextClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.queryTextButton.enabled(), + isSelected: this.queryTextButton.isSelected(), + }); + } + + if (this.executeQueryButton.visible()) { + const label = "Run Query"; + buttons.push({ + iconSrc: ExecuteQueryIcon, + iconAlt: label, + onCommandClick: this.onExecuteQueryClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.executeQueryButton.enabled(), + }); + } + + if (this.addEntityButton.visible()) { + const label = this.container.isPreferredApiCassandra() ? "Add Row" : "Add Entity"; + buttons.push({ + iconSrc: AddEntityIcon, + iconAlt: label, + onCommandClick: this.onAddEntityClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: !this.addEntityButton.enabled(), + }); + } + + if (this.editEntityButton.visible()) { + const label = this.container.isPreferredApiCassandra() ? "Edit Row" : "Edit Entity"; + buttons.push({ + iconSrc: EditEntityIcon, + iconAlt: label, + onCommandClick: this.onEditEntityClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: !this.editEntityButton.enabled(), + }); + } + + if (this.deleteEntityButton.visible()) { + const label = this.container.isPreferredApiCassandra() ? "Delete Rows" : "Delete Entities"; + buttons.push({ + iconSrc: DeleteEntitiesIcon, + iconAlt: label, + onCommandClick: this.onDeleteEntityClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: !this.deleteEntityButton.enabled(), + }); + } + return buttons; + } + + protected buildCommandBarOptions(): void { + ko.computed(() => + ko.toJSON([ + this.queryBuilderButton.visible, + this.queryBuilderButton.enabled, + this.queryTextButton.visible, + this.queryTextButton.enabled, + this.executeQueryButton.visible, + this.executeQueryButton.enabled, + this.addEntityButton.visible, + this.addEntityButton.enabled, + this.editEntityButton.visible, + this.editEntityButton.enabled, + this.deleteEntityButton.visible, + this.deleteEntityButton.enabled, + ]) + ).subscribe(() => this.updateNavbarWithTabsButtons()); + this.updateNavbarWithTabsButtons(); + } +} diff --git a/src/Explorer/Tabs/ScriptTabBase.ts b/src/Explorer/Tabs/ScriptTabBase.ts index e916f1ebf..a5a994a9b 100644 --- a/src/Explorer/Tabs/ScriptTabBase.ts +++ b/src/Explorer/Tabs/ScriptTabBase.ts @@ -66,7 +66,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode this._setBaselines(); - this.id.editableIsValid.subscribe(isValid => { + this.id.editableIsValid.subscribe((isValid) => { const currentState = this.editorState(); switch (currentState) { case ViewModels.ScriptEditorState.newValid: @@ -94,7 +94,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode this.editor = ko.observable(); this.formIsValid = ko.computed(() => { - const formIsValid: boolean = this.formFields().every(field => { + const formIsValid: boolean = this.formFields().every((field) => { return field.editableIsValid(); }); @@ -102,7 +102,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode }); this.formIsDirty = ko.computed(() => { - const formIsDirty: boolean = this.formFields().some(field => { + const formIsDirty: boolean = this.formFields().some((field) => { return field.editableIsDirty(); }); @@ -124,7 +124,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode visible: ko.computed(() => { return this.isNew(); - }) + }), }; this.updateButton = { @@ -142,7 +142,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode visible: ko.computed(() => { return !this.isNew(); - }) + }), }; this.discardButton = { @@ -152,7 +152,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode visible: ko.computed(() => { return true; - }) + }), }; this.deleteButton = { @@ -162,7 +162,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode visible: ko.computed(() => { return true; - }) + }), }; this.executeButton = { @@ -172,7 +172,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode visible: ko.computed(() => { return true; - }) + }), }; } @@ -226,7 +226,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.saveButton.enabled() + disabled: !this.saveButton.enabled(), }); } @@ -239,7 +239,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.updateButton.enabled() + disabled: !this.updateButton.enabled(), }); } @@ -252,7 +252,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.discardButton.enabled() + disabled: !this.discardButton.enabled(), }); } return buttons; @@ -266,7 +266,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode this.updateButton.visible, this.updateButton.enabled, this.discardButton.visible, - this.discardButton.enabled + this.discardButton.enabled, ]) ).subscribe(() => this.updateNavbarWithTabsButtons()); this.updateNavbarWithTabsButtons(); @@ -324,7 +324,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode const editorPosition: ViewModels.EditorPosition = { line: i + 1, - column: target - cursor + 1 + column: target - cursor + 1, }; return editorPosition; @@ -337,7 +337,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode value: this.editorContent(), language: "javascript", readOnly: false, - ariaLabel: this.ariaLabel() + ariaLabel: this.ariaLabel(), }; container.innerHTML = ""; @@ -355,7 +355,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode } private _setModelMarkers(errors: ViewModels.QueryError[]) { - const markers: monaco.editor.IMarkerData[] = errors.map(e => this._toMarker(e)); + const markers: monaco.editor.IMarkerData[] = errors.map((e) => this._toMarker(e)); const editorModel = this.editor().getModel(); monaco.editor.setModelMarkers(editorModel, this.tabId, markers); } @@ -378,7 +378,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode startColumn: start.column, endLineNumber: end.line, endColumn: end.column, - code: error.code + code: error.code, }; } } diff --git a/src/Explorer/Tabs/SettingsTabV2.tsx b/src/Explorer/Tabs/SettingsTabV2.tsx index c5cafdbf6..fab1f4161 100644 --- a/src/Explorer/Tabs/SettingsTabV2.tsx +++ b/src/Explorer/Tabs/SettingsTabV2.tsx @@ -24,7 +24,7 @@ export default class SettingsTabV2 extends TabsBase { this.options = options; this.tabId = "SettingsV2-" + this.tabId; const props: SettingsComponentProps = { - settingsTab: this + settingsTab: this, }; this.settingsComponentAdapter = new SettingsComponentAdapter(props); this.currentCollection = this.collection as ViewModels.Collection; @@ -54,7 +54,7 @@ export default class SettingsTabV2 extends TabsBase { this.notification = data; this.notificationRead(true); }, - error => { + (error) => { const errorMessage = getErrorMessage(error); this.notification = undefined; this.notificationRead(true); @@ -68,7 +68,7 @@ export default class SettingsTabV2 extends TabsBase { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle, error: errorMessage, - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, this.options.onLoadStartKey ); diff --git a/src/Explorer/Tabs/SparkMasterTab.html b/src/Explorer/Tabs/SparkMasterTab.html index 58c56b09d..ee27f00ee 100644 --- a/src/Explorer/Tabs/SparkMasterTab.html +++ b/src/Explorer/Tabs/SparkMasterTab.html @@ -1,7 +1,7 @@ -
- - -
+
+ + +
diff --git a/src/Explorer/Tabs/SparkMasterTab.ts b/src/Explorer/Tabs/SparkMasterTab.ts index 9ef5d3a51..ca62b5344 100644 --- a/src/Explorer/Tabs/SparkMasterTab.ts +++ b/src/Explorer/Tabs/SparkMasterTab.ts @@ -1,35 +1,35 @@ -import * as ko from "knockout"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import TabsBase from "./TabsBase"; -import Explorer from "../Explorer"; - -interface SparkMasterTabOptions extends ViewModels.TabOptions { - clusterConnectionInfo: DataModels.SparkClusterConnectionInfo; - container: Explorer; -} - -export default class SparkMasterTab extends TabsBase { - public sparkMasterSrc: ko.Observable; - - private _clusterConnectionInfo: DataModels.SparkClusterConnectionInfo; - private _container: Explorer; - - constructor(options: SparkMasterTabOptions) { - super(options); - super.onActivate.bind(this); - this._container = options.container; - this._clusterConnectionInfo = options.clusterConnectionInfo; - const sparkMasterEndpoint = - this._clusterConnectionInfo && - this._clusterConnectionInfo.endpoints && - this._clusterConnectionInfo.endpoints.find( - endpoint => endpoint.kind === DataModels.SparkClusterEndpointKind.SparkUI - ); - this.sparkMasterSrc = ko.observable(sparkMasterEndpoint && sparkMasterEndpoint.endpoint); - } - - protected getContainer() { - return this._container; - } -} +import * as ko from "knockout"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import TabsBase from "./TabsBase"; +import Explorer from "../Explorer"; + +interface SparkMasterTabOptions extends ViewModels.TabOptions { + clusterConnectionInfo: DataModels.SparkClusterConnectionInfo; + container: Explorer; +} + +export default class SparkMasterTab extends TabsBase { + public sparkMasterSrc: ko.Observable; + + private _clusterConnectionInfo: DataModels.SparkClusterConnectionInfo; + private _container: Explorer; + + constructor(options: SparkMasterTabOptions) { + super(options); + super.onActivate.bind(this); + this._container = options.container; + this._clusterConnectionInfo = options.clusterConnectionInfo; + const sparkMasterEndpoint = + this._clusterConnectionInfo && + this._clusterConnectionInfo.endpoints && + this._clusterConnectionInfo.endpoints.find( + (endpoint) => endpoint.kind === DataModels.SparkClusterEndpointKind.SparkUI + ); + this.sparkMasterSrc = ko.observable(sparkMasterEndpoint && sparkMasterEndpoint.endpoint); + } + + protected getContainer() { + return this._container; + } +} diff --git a/src/Explorer/Tabs/StoredProcedureTab.html b/src/Explorer/Tabs/StoredProcedureTab.html index 9e30d674d..93f985af8 100644 --- a/src/Explorer/Tabs/StoredProcedureTab.html +++ b/src/Explorer/Tabs/StoredProcedureTab.html @@ -1,89 +1,89 @@ -
- -
-
Stored Procedure Id
- - - -
Stored Procedure Body
- - -
-
-
- - Result -
-
- - console.log -
-
- - - -
-
-
Errors:
-
- - - More details - -
-
- -
-
+
+ +
+
Stored Procedure Id
+ + + +
Stored Procedure Body
+ + +
+
+
+ + Result +
+
+ + console.log +
+
+ + + +
+
+
Errors:
+
+ + + More details + +
+
+ +
+
diff --git a/src/Explorer/Tabs/StoredProcedureTab.ts b/src/Explorer/Tabs/StoredProcedureTab.ts index b86400022..fd7a1c0ac 100644 --- a/src/Explorer/Tabs/StoredProcedureTab.ts +++ b/src/Explorer/Tabs/StoredProcedureTab.ts @@ -1,296 +1,296 @@ -import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; -import * as ko from "knockout"; -import Q from "q"; -import * as _ from "underscore"; -import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; -import * as Constants from "../../Common/Constants"; -import { createStoredProcedure } from "../../Common/dataAccess/createStoredProcedure"; -import { updateStoredProcedure } from "../../Common/dataAccess/updateStoredProcedure"; -import editable from "../../Common/EditableUtility"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import StoredProcedure from "../Tree/StoredProcedure"; -import ScriptTabBase from "./ScriptTabBase"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; - -enum ToggleState { - Result = "result", - Logs = "logs" -} - -export default class StoredProcedureTab extends ScriptTabBase { - public collection: ViewModels.Collection; - public node: StoredProcedure; - public executeResultsEditorId: string; - public executeLogsEditorId: string; - public toggleState: ko.Observable; - public originalSprocBody: ViewModels.Editable; - public resultsData: ko.Observable; - public logsData: ko.Observable; - public error: ko.Observable; - public hasResults: ko.Observable; - public hasErrors: ko.Observable; - - constructor(options: ViewModels.ScriptTabOption) { - super(options); - super.onActivate.bind(this); - - this.executeResultsEditorId = `executestoredprocedureresults${this.tabId}`; - this.executeLogsEditorId = `executestoredprocedurelogs${this.tabId}`; - this.toggleState = ko.observable(ToggleState.Result); - this.originalSprocBody = editable.observable(this.editorContent()); - this.resultsData = ko.observable(); - this.logsData = ko.observable(); - this.error = ko.observable(); - this.hasResults = ko.observable(false); - this.hasErrors = ko.observable(false); - this.error.subscribe((error: string) => { - this.hasErrors(error != null); - this.hasResults(error == null); - }); - - this.ariaLabel("Stored Procedure Body"); - this.buildCommandBarOptions(); - } - - public onSaveClick = (): Promise => { - return this._createStoredProcedure({ - id: this.id(), - body: this.editorContent() - }); - }; - - public onDiscard = (): Q.Promise => { - this.setBaselines(); - const original = this.editorContent.getEditableOriginalValue(); - this.originalSprocBody(original); - this.originalSprocBody.valueHasMutated(); // trigger a re-render of the editor - - return Q(); - }; - - public onUpdateClick = (): Promise => { - const data = this._getResource(); - - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateStoredProcedure, { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }); - return updateStoredProcedure(this.collection.databaseId, this.collection.id(), data) - .then( - updatedResource => { - this.resource(updatedResource); - this.tabTitle(updatedResource.id); - this.node.id(updatedResource.id); - this.node.body(updatedResource.body as string); - this.setBaselines(); - - const editorModel = this.editor() && this.editor().getModel(); - editorModel && editorModel.setValue(updatedResource.body as string); - this.editorContent.setBaseline(updatedResource.body as string); - TelemetryProcessor.traceSuccess( - Action.UpdateStoredProcedure, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - }, - (error: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.UpdateStoredProcedure, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - }; - - public onExecuteSprocsResult(result: any, logsData: any): void { - const resultData: string = this.renderObjectForEditor(_.omit(result, "scriptLogs").result, null, 4); - const scriptLogs: string = (result.scriptLogs && decodeURIComponent(result.scriptLogs)) || ""; - const logs: string = this.renderObjectForEditor(scriptLogs, null, 4); - this.error(null); - this.resultsData(resultData); - this.logsData(logs); - } - - public onExecuteSprocsError(error: string): void { - this.isExecutionError(true); - console.error(error); - this.error(error); - } - - public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { - this.collection && this.collection.container.expandConsole(); - - return false; - }; - - public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.onErrorDetailsClick(src, null); - return false; - } - - return true; - }; - - public toggleResult(): void { - this.toggleState(ToggleState.Result); - this.resultsData.valueHasMutated(); // needed to refresh the json-editor component - } - - public toggleLogs(): void { - this.toggleState(ToggleState.Logs); - this.logsData.valueHasMutated(); // needed to refresh the json-editor component - } - - public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.LeftArrow) { - this.toggleResult(); - event.stopPropagation(); - return false; - } else if (event.keyCode === Constants.KeyCodes.RightArrow) { - this.toggleLogs(); - event.stopPropagation(); - return false; - } - - return true; - }; - - public isResultToggled(): boolean { - return this.toggleState() === ToggleState.Result; - } - - public isLogsToggled(): boolean { - return this.toggleState() === ToggleState.Logs; - } - - protected updateSelectedNode(): void { - if (this.collection == null) { - return; - } - - const database: ViewModels.Database = this.collection.getDatabase(); - if (!database.isDatabaseExpanded()) { - this.collection.container.selectedNode(database); - } else if (!this.collection.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) { - this.collection.container.selectedNode(this.collection); - } else { - this.collection.container.selectedNode(this.node); - } - } - - protected buildCommandBarOptions(): void { - ko.computed(() => ko.toJSON([this.isNew, this.formIsDirty])).subscribe(() => this.updateNavbarWithTabsButtons()); - super.buildCommandBarOptions(); - } - - protected getTabsButtons(): CommandButtonComponentProps[] { - const label = "Execute"; - return super.getTabsButtons().concat({ - iconSrc: ExecuteQueryIcon, - iconAlt: label, - onCommandClick: () => { - this.collection && this.collection.container.executeSprocParamsPane.open(); - }, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: this.isNew() || this.formIsDirty() - }); - } - - private _getResource() { - return { - id: this.id(), - body: this.editorContent() - }; - } - - private _createStoredProcedure(resource: StoredProcedureDefinition): Promise { - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateStoredProcedure, { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }); - - return createStoredProcedure(this.collection.databaseId, this.collection.id(), resource) - .then( - createdResource => { - this.tabTitle(createdResource.id); - this.isNew(false); - this.resource(createdResource); - this.hashLocation( - `${Constants.HashRoutePrefixes.collectionsWithIds( - this.collection.databaseId, - this.collection.id() - )}/sprocs/${createdResource.id}` - ); - this.setBaselines(); - - const editorModel = this.editor() && this.editor().getModel(); - editorModel && editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - this.node = this.collection.createStoredProcedureNode(createdResource); - TelemetryProcessor.traceSuccess( - Action.CreateStoredProcedure, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() - }, - startKey - ); - this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); - return createdResource; - }, - createError => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateStoredProcedure, - { - databaseAccountName: this.collection && this.collection.container.databaseAccount().name, - defaultExperience: this.collection && this.collection.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError) - }, - startKey - ); - return Promise.reject(createError); - } - ) - .finally(() => this.isExecuting(false)); - } - - public onDelete(): Q.Promise { - // TODO - return Q(); - } -} +import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; +import * as ko from "knockout"; +import Q from "q"; +import * as _ from "underscore"; +import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; +import * as Constants from "../../Common/Constants"; +import { createStoredProcedure } from "../../Common/dataAccess/createStoredProcedure"; +import { updateStoredProcedure } from "../../Common/dataAccess/updateStoredProcedure"; +import editable from "../../Common/EditableUtility"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import StoredProcedure from "../Tree/StoredProcedure"; +import ScriptTabBase from "./ScriptTabBase"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; + +enum ToggleState { + Result = "result", + Logs = "logs", +} + +export default class StoredProcedureTab extends ScriptTabBase { + public collection: ViewModels.Collection; + public node: StoredProcedure; + public executeResultsEditorId: string; + public executeLogsEditorId: string; + public toggleState: ko.Observable; + public originalSprocBody: ViewModels.Editable; + public resultsData: ko.Observable; + public logsData: ko.Observable; + public error: ko.Observable; + public hasResults: ko.Observable; + public hasErrors: ko.Observable; + + constructor(options: ViewModels.ScriptTabOption) { + super(options); + super.onActivate.bind(this); + + this.executeResultsEditorId = `executestoredprocedureresults${this.tabId}`; + this.executeLogsEditorId = `executestoredprocedurelogs${this.tabId}`; + this.toggleState = ko.observable(ToggleState.Result); + this.originalSprocBody = editable.observable(this.editorContent()); + this.resultsData = ko.observable(); + this.logsData = ko.observable(); + this.error = ko.observable(); + this.hasResults = ko.observable(false); + this.hasErrors = ko.observable(false); + this.error.subscribe((error: string) => { + this.hasErrors(error != null); + this.hasResults(error == null); + }); + + this.ariaLabel("Stored Procedure Body"); + this.buildCommandBarOptions(); + } + + public onSaveClick = (): Promise => { + return this._createStoredProcedure({ + id: this.id(), + body: this.editorContent(), + }); + }; + + public onDiscard = (): Q.Promise => { + this.setBaselines(); + const original = this.editorContent.getEditableOriginalValue(); + this.originalSprocBody(original); + this.originalSprocBody.valueHasMutated(); // trigger a re-render of the editor + + return Q(); + }; + + public onUpdateClick = (): Promise => { + const data = this._getResource(); + + this.isExecutionError(false); + this.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateStoredProcedure, { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }); + return updateStoredProcedure(this.collection.databaseId, this.collection.id(), data) + .then( + (updatedResource) => { + this.resource(updatedResource); + this.tabTitle(updatedResource.id); + this.node.id(updatedResource.id); + this.node.body(updatedResource.body as string); + this.setBaselines(); + + const editorModel = this.editor() && this.editor().getModel(); + editorModel && editorModel.setValue(updatedResource.body as string); + this.editorContent.setBaseline(updatedResource.body as string); + TelemetryProcessor.traceSuccess( + Action.UpdateStoredProcedure, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }, + startKey + ); + }, + (error: any) => { + this.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.UpdateStoredProcedure, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + ) + .finally(() => this.isExecuting(false)); + }; + + public onExecuteSprocsResult(result: any, logsData: any): void { + const resultData: string = this.renderObjectForEditor(_.omit(result, "scriptLogs").result, null, 4); + const scriptLogs: string = (result.scriptLogs && decodeURIComponent(result.scriptLogs)) || ""; + const logs: string = this.renderObjectForEditor(scriptLogs, null, 4); + this.error(null); + this.resultsData(resultData); + this.logsData(logs); + } + + public onExecuteSprocsError(error: string): void { + this.isExecutionError(true); + console.error(error); + this.error(error); + } + + public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { + this.collection && this.collection.container.expandConsole(); + + return false; + }; + + public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.onErrorDetailsClick(src, null); + return false; + } + + return true; + }; + + public toggleResult(): void { + this.toggleState(ToggleState.Result); + this.resultsData.valueHasMutated(); // needed to refresh the json-editor component + } + + public toggleLogs(): void { + this.toggleState(ToggleState.Logs); + this.logsData.valueHasMutated(); // needed to refresh the json-editor component + } + + public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.LeftArrow) { + this.toggleResult(); + event.stopPropagation(); + return false; + } else if (event.keyCode === Constants.KeyCodes.RightArrow) { + this.toggleLogs(); + event.stopPropagation(); + return false; + } + + return true; + }; + + public isResultToggled(): boolean { + return this.toggleState() === ToggleState.Result; + } + + public isLogsToggled(): boolean { + return this.toggleState() === ToggleState.Logs; + } + + protected updateSelectedNode(): void { + if (this.collection == null) { + return; + } + + const database: ViewModels.Database = this.collection.getDatabase(); + if (!database.isDatabaseExpanded()) { + this.collection.container.selectedNode(database); + } else if (!this.collection.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) { + this.collection.container.selectedNode(this.collection); + } else { + this.collection.container.selectedNode(this.node); + } + } + + protected buildCommandBarOptions(): void { + ko.computed(() => ko.toJSON([this.isNew, this.formIsDirty])).subscribe(() => this.updateNavbarWithTabsButtons()); + super.buildCommandBarOptions(); + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const label = "Execute"; + return super.getTabsButtons().concat({ + iconSrc: ExecuteQueryIcon, + iconAlt: label, + onCommandClick: () => { + this.collection && this.collection.container.executeSprocParamsPane.open(); + }, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: this.isNew() || this.formIsDirty(), + }); + } + + private _getResource() { + return { + id: this.id(), + body: this.editorContent(), + }; + } + + private _createStoredProcedure(resource: StoredProcedureDefinition): Promise { + this.isExecutionError(false); + this.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateStoredProcedure, { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }); + + return createStoredProcedure(this.collection.databaseId, this.collection.id(), resource) + .then( + (createdResource) => { + this.tabTitle(createdResource.id); + this.isNew(false); + this.resource(createdResource); + this.hashLocation( + `${Constants.HashRoutePrefixes.collectionsWithIds( + this.collection.databaseId, + this.collection.id() + )}/sprocs/${createdResource.id}` + ); + this.setBaselines(); + + const editorModel = this.editor() && this.editor().getModel(); + editorModel && editorModel.setValue(createdResource.body as string); + this.editorContent.setBaseline(createdResource.body as string); + this.node = this.collection.createStoredProcedureNode(createdResource); + TelemetryProcessor.traceSuccess( + Action.CreateStoredProcedure, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + }, + startKey + ); + this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); + return createdResource; + }, + (createError) => { + this.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.CreateStoredProcedure, + { + databaseAccountName: this.collection && this.collection.container.databaseAccount().name, + defaultExperience: this.collection && this.collection.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + return Promise.reject(createError); + } + ) + .finally(() => this.isExecuting(false)); + } + + public onDelete(): Q.Promise { + // TODO + return Q(); + } +} diff --git a/src/Explorer/Tabs/TabComponents.ts b/src/Explorer/Tabs/TabComponents.ts index 8d0490ff2..23ee05c02 100644 --- a/src/Explorer/Tabs/TabComponents.ts +++ b/src/Explorer/Tabs/TabComponents.ts @@ -1,196 +1,196 @@ -import DocumentsTabTemplate from "./DocumentsTab.html"; -import ConflictsTabTemplate from "./ConflictsTab.html"; -import GraphTabTemplate from "./GraphTab.html"; -import SparkMasterTabTemplate from "./SparkMasterTab.html"; -import NotebookV2TabTemplate from "./NotebookV2Tab.html"; -import TerminalTabTemplate from "./TerminalTab.html"; -import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html"; -import MongoQueryTabTemplate from "./MongoQueryTab.html"; -import MongoShellTabTemplate from "./MongoShellTab.html"; -import QueryTabTemplate from "./QueryTab.html"; -import QueryTablesTabTemplate from "./QueryTablesTab.html"; -import SettingsTabV2Template from "./SettingsTabV2.html"; -import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html"; -import StoredProcedureTabTemplate from "./StoredProcedureTab.html"; -import TriggerTabTemplate from "./TriggerTab.html"; -import UserDefinedFunctionTabTemplate from "./UserDefinedFunctionTab.html"; -import GalleryTabTemplate from "./GalleryTab.html"; -import NotebookViewerTabTemplate from "./NotebookViewerTab.html"; -import TabsManagerTemplate from "./TabsManager.html"; - -export class TabComponent { - constructor(data: any) { - return data.data; - } -} - -export class TabsManager { - constructor() { - return { - viewModel: TabComponent, - template: TabsManagerTemplate - }; - } -} - -export class DocumentsTab { - constructor() { - return { - viewModel: TabComponent, - template: DocumentsTabTemplate - }; - } -} - -export class ConflictsTab { - constructor() { - return { - viewModel: TabComponent, - template: ConflictsTabTemplate - }; - } -} - -export class GraphTab { - constructor() { - return { - viewModel: TabComponent, - template: GraphTabTemplate - }; - } -} - -export class SparkMasterTab { - constructor() { - return { - viewModel: TabComponent, - template: SparkMasterTabTemplate - }; - } -} - -export class NotebookV2Tab { - constructor() { - return { - viewModel: TabComponent, - template: NotebookV2TabTemplate - }; - } -} - -export class TerminalTab { - constructor() { - return { - viewModel: TabComponent, - template: TerminalTabTemplate - }; - } -} - -export class MongoDocumentsTab { - constructor() { - return { - viewModel: TabComponent, - template: MongoDocumentsTabTemplate - }; - } -} - -export class MongoQueryTab { - constructor() { - return { - viewModel: TabComponent, - template: MongoQueryTabTemplate - }; - } -} - -export class MongoShellTab { - constructor() { - return { - viewModel: TabComponent, - template: MongoShellTabTemplate - }; - } -} - -export class QueryTab { - constructor() { - return { - viewModel: TabComponent, - template: QueryTabTemplate - }; - } -} - -export class QueryTablesTab { - constructor() { - return { - viewModel: TabComponent, - template: QueryTablesTabTemplate - }; - } -} - -export class SettingsTabV2 { - constructor() { - return { - viewModel: TabComponent, - template: SettingsTabV2Template - }; - } -} - -export class DatabaseSettingsTab { - constructor() { - return { - viewModel: TabComponent, - template: DatabaseSettingsTabTemplate - }; - } -} - -export class StoredProcedureTab { - constructor() { - return { - viewModel: TabComponent, - template: StoredProcedureTabTemplate - }; - } -} - -export class TriggerTab { - constructor() { - return { - viewModel: TabComponent, - template: TriggerTabTemplate - }; - } -} - -export class UserDefinedFunctionTab { - constructor() { - return { - viewModel: TabComponent, - template: UserDefinedFunctionTabTemplate - }; - } -} - -export class GalleryTab { - constructor() { - return { - viewModel: TabComponent, - template: GalleryTabTemplate - }; - } -} - -export class NotebookViewerTab { - constructor() { - return { - viewModel: TabComponent, - template: NotebookViewerTabTemplate - }; - } -} +import DocumentsTabTemplate from "./DocumentsTab.html"; +import ConflictsTabTemplate from "./ConflictsTab.html"; +import GraphTabTemplate from "./GraphTab.html"; +import SparkMasterTabTemplate from "./SparkMasterTab.html"; +import NotebookV2TabTemplate from "./NotebookV2Tab.html"; +import TerminalTabTemplate from "./TerminalTab.html"; +import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html"; +import MongoQueryTabTemplate from "./MongoQueryTab.html"; +import MongoShellTabTemplate from "./MongoShellTab.html"; +import QueryTabTemplate from "./QueryTab.html"; +import QueryTablesTabTemplate from "./QueryTablesTab.html"; +import SettingsTabV2Template from "./SettingsTabV2.html"; +import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html"; +import StoredProcedureTabTemplate from "./StoredProcedureTab.html"; +import TriggerTabTemplate from "./TriggerTab.html"; +import UserDefinedFunctionTabTemplate from "./UserDefinedFunctionTab.html"; +import GalleryTabTemplate from "./GalleryTab.html"; +import NotebookViewerTabTemplate from "./NotebookViewerTab.html"; +import TabsManagerTemplate from "./TabsManager.html"; + +export class TabComponent { + constructor(data: any) { + return data.data; + } +} + +export class TabsManager { + constructor() { + return { + viewModel: TabComponent, + template: TabsManagerTemplate, + }; + } +} + +export class DocumentsTab { + constructor() { + return { + viewModel: TabComponent, + template: DocumentsTabTemplate, + }; + } +} + +export class ConflictsTab { + constructor() { + return { + viewModel: TabComponent, + template: ConflictsTabTemplate, + }; + } +} + +export class GraphTab { + constructor() { + return { + viewModel: TabComponent, + template: GraphTabTemplate, + }; + } +} + +export class SparkMasterTab { + constructor() { + return { + viewModel: TabComponent, + template: SparkMasterTabTemplate, + }; + } +} + +export class NotebookV2Tab { + constructor() { + return { + viewModel: TabComponent, + template: NotebookV2TabTemplate, + }; + } +} + +export class TerminalTab { + constructor() { + return { + viewModel: TabComponent, + template: TerminalTabTemplate, + }; + } +} + +export class MongoDocumentsTab { + constructor() { + return { + viewModel: TabComponent, + template: MongoDocumentsTabTemplate, + }; + } +} + +export class MongoQueryTab { + constructor() { + return { + viewModel: TabComponent, + template: MongoQueryTabTemplate, + }; + } +} + +export class MongoShellTab { + constructor() { + return { + viewModel: TabComponent, + template: MongoShellTabTemplate, + }; + } +} + +export class QueryTab { + constructor() { + return { + viewModel: TabComponent, + template: QueryTabTemplate, + }; + } +} + +export class QueryTablesTab { + constructor() { + return { + viewModel: TabComponent, + template: QueryTablesTabTemplate, + }; + } +} + +export class SettingsTabV2 { + constructor() { + return { + viewModel: TabComponent, + template: SettingsTabV2Template, + }; + } +} + +export class DatabaseSettingsTab { + constructor() { + return { + viewModel: TabComponent, + template: DatabaseSettingsTabTemplate, + }; + } +} + +export class StoredProcedureTab { + constructor() { + return { + viewModel: TabComponent, + template: StoredProcedureTabTemplate, + }; + } +} + +export class TriggerTab { + constructor() { + return { + viewModel: TabComponent, + template: TriggerTabTemplate, + }; + } +} + +export class UserDefinedFunctionTab { + constructor() { + return { + viewModel: TabComponent, + template: UserDefinedFunctionTabTemplate, + }; + } +} + +export class GalleryTab { + constructor() { + return { + viewModel: TabComponent, + template: GalleryTabTemplate, + }; + } +} + +export class NotebookViewerTab { + constructor() { + return { + viewModel: TabComponent, + template: NotebookViewerTabTemplate, + }; + } +} diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index 10072976d..802e4ed51 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -1,210 +1,210 @@ -import * as ko from "knockout"; -import Q from "q"; -import * as Constants from "../../Common/Constants"; -import * as ViewModels from "../../Contracts/ViewModels"; -import * as DataModels from "../../Contracts/DataModels"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import { RouteHandler } from "../../RouteHandlers/RouteHandler"; -import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import ThemeUtility from "../../Common/ThemeUtility"; -import Explorer from "../Explorer"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; - -// TODO: Use specific actions for logging telemetry data -export default class TabsBase extends WaitsForTemplateViewModel { - public closeTabButton: ViewModels.Button; - public node: ViewModels.TreeNode; - public collection: ViewModels.CollectionBase; - public database: ViewModels.Database; - public rid: string; - public hasFocus: ko.Observable; - public isActive: ko.Observable; - public isMouseOver: ko.Observable; - public tabId: string; - public tabKind: ViewModels.CollectionTabKind; - public tabTitle: ko.Observable; - public tabPath: ko.Observable; - public closeButtonTabIndex: ko.Computed; - public errorDetailsTabIndex: ko.Computed; - public hashLocation: ko.Observable; - public isExecutionError: ko.Observable; - public isExecuting: ko.Observable; - public pendingNotification?: ko.Observable; - - protected _theme: string; - public onLoadStartKey: number; - - constructor(options: ViewModels.TabOptions) { - super(); - const id = new Date().getTime().toString(); - - this._theme = ThemeUtility.getMonacoTheme(options.theme); - this.node = options.node; - this.collection = options.collection; - this.database = options.database; - this.rid = options.rid || (this.collection && this.collection.rid) || ""; - this.hasFocus = ko.observable(false); - this.isActive = options.isActive || ko.observable(false); - this.isMouseOver = ko.observable(false); - this.tabId = `tab${id}`; - this.tabKind = options.tabKind; - this.tabTitle = ko.observable(options.title); - this.tabPath = - (options.tabPath && ko.observable(options.tabPath)) || - (this.collection && - ko.observable(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`)); - this.closeButtonTabIndex = ko.computed(() => (this.isActive() ? 0 : null)); - this.errorDetailsTabIndex = ko.computed(() => (this.isActive() ? 0 : null)); - this.isExecutionError = ko.observable(false); - this.isExecuting = ko.observable(false); - this.pendingNotification = ko.observable(undefined); - this.onLoadStartKey = options.onLoadStartKey; - this.hashLocation = ko.observable(options.hashLocation || ""); - this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation)); - - this.isActive.subscribe((isActive: boolean) => { - if (isActive) { - this.onActivate(); - } - }); - - this.closeTabButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }) - }; - } - - public onCloseTabButtonClick(): void { - const explorer = this.getContainer(); - explorer.tabsManager.closeTab(this.tabId, explorer); - - TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { - tabName: this.constructor.name, - databaseAccountName: this.getContainer().databaseAccount().name, - defaultExperience: this.getContainer().defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - tabId: this.tabId - }); - } - - public onTabClick(): void { - this.getContainer().tabsManager.activateTab(this); - } - - protected updateSelectedNode(): void { - const relatedDatabase = (this.collection && this.collection.getDatabase()) || this.database; - if (relatedDatabase && !relatedDatabase.isDatabaseExpanded()) { - this.getContainer().selectedNode(relatedDatabase); - } else if (this.collection && !this.collection.isCollectionExpanded()) { - this.getContainer().selectedNode(this.collection); - } else { - this.getContainer().selectedNode(this.node); - } - } - - private onSpaceOrEnterKeyPress(event: KeyboardEvent, callback: () => void): boolean { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - callback(); - event.stopPropagation(); - return false; - } - - return true; - } - - public onKeyPressActivate = (source: any, event: KeyboardEvent): boolean => { - return this.onSpaceOrEnterKeyPress(event, () => this.onTabClick()); - }; - - public onKeyPressClose = (source: any, event: KeyboardEvent): boolean => { - return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick()); - }; - - public onActivate(): void { - this.updateSelectedNode(); - if (!!this.collection) { - this.collection.selectedSubnodeKind(this.tabKind); - } - - if (!!this.database) { - this.database.selectedSubnodeKind(this.tabKind); - } - - this.hasFocus(true); - this.updateGlobalHash(this.hashLocation()); - - this.updateNavbarWithTabsButtons(); - - TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, { - tabName: this.constructor.name, - databaseAccountName: this.getContainer().databaseAccount().name, - defaultExperience: this.getContainer().defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - tabId: this.tabId - }); - } - - public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { - if (this.collection && this.collection.container) { - this.collection.container.expandConsole(); - } - - if (this.database && this.database.container) { - this.database.container.expandConsole(); - } - return false; - }; - - public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.onErrorDetailsClick(src, null); - return false; - } - - return true; - }; - - public refresh(): Q.Promise { - location.reload(); - return Q(); - } - - protected getContainer(): Explorer { - return (this.collection && this.collection.container) || (this.database && this.database.container); - } - - /** Renders a Javascript object to be displayed inside Monaco Editor */ - protected renderObjectForEditor(value: any, replacer: any, space: string | number): string { - return JSON.stringify(value, replacer, space); - } - - private updateGlobalHash(newHash: string): void { - RouteHandler.getInstance().updateRouteHashLocation(newHash); - } - - /** - * @return buttons that are displayed in the navbar - */ - protected getTabsButtons(): CommandButtonComponentProps[] { - return []; - } - - protected updateNavbarWithTabsButtons = (): void => { - if (this.isActive()) { - this.getContainer().onUpdateTabsButtons(this.getTabsButtons()); - } - }; -} - -interface EditorPosition { - line: number; - column: number; -} +import * as ko from "knockout"; +import Q from "q"; +import * as Constants from "../../Common/Constants"; +import * as ViewModels from "../../Contracts/ViewModels"; +import * as DataModels from "../../Contracts/DataModels"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import { RouteHandler } from "../../RouteHandlers/RouteHandler"; +import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import ThemeUtility from "../../Common/ThemeUtility"; +import Explorer from "../Explorer"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; + +// TODO: Use specific actions for logging telemetry data +export default class TabsBase extends WaitsForTemplateViewModel { + public closeTabButton: ViewModels.Button; + public node: ViewModels.TreeNode; + public collection: ViewModels.CollectionBase; + public database: ViewModels.Database; + public rid: string; + public hasFocus: ko.Observable; + public isActive: ko.Observable; + public isMouseOver: ko.Observable; + public tabId: string; + public tabKind: ViewModels.CollectionTabKind; + public tabTitle: ko.Observable; + public tabPath: ko.Observable; + public closeButtonTabIndex: ko.Computed; + public errorDetailsTabIndex: ko.Computed; + public hashLocation: ko.Observable; + public isExecutionError: ko.Observable; + public isExecuting: ko.Observable; + public pendingNotification?: ko.Observable; + + protected _theme: string; + public onLoadStartKey: number; + + constructor(options: ViewModels.TabOptions) { + super(); + const id = new Date().getTime().toString(); + + this._theme = ThemeUtility.getMonacoTheme(options.theme); + this.node = options.node; + this.collection = options.collection; + this.database = options.database; + this.rid = options.rid || (this.collection && this.collection.rid) || ""; + this.hasFocus = ko.observable(false); + this.isActive = options.isActive || ko.observable(false); + this.isMouseOver = ko.observable(false); + this.tabId = `tab${id}`; + this.tabKind = options.tabKind; + this.tabTitle = ko.observable(options.title); + this.tabPath = + (options.tabPath && ko.observable(options.tabPath)) || + (this.collection && + ko.observable(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`)); + this.closeButtonTabIndex = ko.computed(() => (this.isActive() ? 0 : null)); + this.errorDetailsTabIndex = ko.computed(() => (this.isActive() ? 0 : null)); + this.isExecutionError = ko.observable(false); + this.isExecuting = ko.observable(false); + this.pendingNotification = ko.observable(undefined); + this.onLoadStartKey = options.onLoadStartKey; + this.hashLocation = ko.observable(options.hashLocation || ""); + this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation)); + + this.isActive.subscribe((isActive: boolean) => { + if (isActive) { + this.onActivate(); + } + }); + + this.closeTabButton = { + enabled: ko.computed(() => { + return true; + }), + + visible: ko.computed(() => { + return true; + }), + }; + } + + public onCloseTabButtonClick(): void { + const explorer = this.getContainer(); + explorer.tabsManager.closeTab(this.tabId, explorer); + + TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { + tabName: this.constructor.name, + databaseAccountName: this.getContainer().databaseAccount().name, + defaultExperience: this.getContainer().defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + tabId: this.tabId, + }); + } + + public onTabClick(): void { + this.getContainer().tabsManager.activateTab(this); + } + + protected updateSelectedNode(): void { + const relatedDatabase = (this.collection && this.collection.getDatabase()) || this.database; + if (relatedDatabase && !relatedDatabase.isDatabaseExpanded()) { + this.getContainer().selectedNode(relatedDatabase); + } else if (this.collection && !this.collection.isCollectionExpanded()) { + this.getContainer().selectedNode(this.collection); + } else { + this.getContainer().selectedNode(this.node); + } + } + + private onSpaceOrEnterKeyPress(event: KeyboardEvent, callback: () => void): boolean { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + callback(); + event.stopPropagation(); + return false; + } + + return true; + } + + public onKeyPressActivate = (source: any, event: KeyboardEvent): boolean => { + return this.onSpaceOrEnterKeyPress(event, () => this.onTabClick()); + }; + + public onKeyPressClose = (source: any, event: KeyboardEvent): boolean => { + return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick()); + }; + + public onActivate(): void { + this.updateSelectedNode(); + if (!!this.collection) { + this.collection.selectedSubnodeKind(this.tabKind); + } + + if (!!this.database) { + this.database.selectedSubnodeKind(this.tabKind); + } + + this.hasFocus(true); + this.updateGlobalHash(this.hashLocation()); + + this.updateNavbarWithTabsButtons(); + + TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, { + tabName: this.constructor.name, + databaseAccountName: this.getContainer().databaseAccount().name, + defaultExperience: this.getContainer().defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.tabTitle(), + tabId: this.tabId, + }); + } + + public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { + if (this.collection && this.collection.container) { + this.collection.container.expandConsole(); + } + + if (this.database && this.database.container) { + this.database.container.expandConsole(); + } + return false; + }; + + public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.onErrorDetailsClick(src, null); + return false; + } + + return true; + }; + + public refresh(): Q.Promise { + location.reload(); + return Q(); + } + + protected getContainer(): Explorer { + return (this.collection && this.collection.container) || (this.database && this.database.container); + } + + /** Renders a Javascript object to be displayed inside Monaco Editor */ + protected renderObjectForEditor(value: any, replacer: any, space: string | number): string { + return JSON.stringify(value, replacer, space); + } + + private updateGlobalHash(newHash: string): void { + RouteHandler.getInstance().updateRouteHashLocation(newHash); + } + + /** + * @return buttons that are displayed in the navbar + */ + protected getTabsButtons(): CommandButtonComponentProps[] { + return []; + } + + protected updateNavbarWithTabsButtons = (): void => { + if (this.isActive()) { + this.getContainer().onUpdateTabsButtons(this.getTabsButtons()); + } + }; +} + +interface EditorPosition { + line: number; + column: number; +} diff --git a/src/Explorer/Tabs/TabsManager.test.ts b/src/Explorer/Tabs/TabsManager.test.ts index c786dd73b..402078193 100644 --- a/src/Explorer/Tabs/TabsManager.test.ts +++ b/src/Explorer/Tabs/TabsManager.test.ts @@ -24,13 +24,13 @@ describe("Tabs manager tests", () => { type: "", kind: "", tags: "", - properties: undefined + properties: undefined, }); database = { container: explorer, id: ko.observable("test"), - isDatabaseShared: () => false + isDatabaseShared: () => false, } as ViewModels.Database; database.isDatabaseExpanded = ko.observable(true); database.selectedSubnodeKind = ko.observable(); @@ -38,7 +38,7 @@ describe("Tabs manager tests", () => { collection = { container: explorer, databaseId: "test", - id: ko.observable("test") + id: ko.observable("test"), } as ViewModels.Collection; collection.getDatabase = (): ViewModels.Database => database; collection.isCollectionExpanded = ko.observable(true); @@ -52,7 +52,7 @@ describe("Tabs manager tests", () => { tabPath: "", isActive: ko.observable(false), hashLocation: "", - onUpdateTabsButtons: undefined + onUpdateTabsButtons: undefined, }); documentsTab = new DocumentsTab({ @@ -64,7 +64,7 @@ describe("Tabs manager tests", () => { tabPath: "", hashLocation: "", isActive: ko.observable(false), - onUpdateTabsButtons: undefined + onUpdateTabsButtons: undefined, }); // make sure tabs have different tabId @@ -112,7 +112,7 @@ describe("Tabs manager tests", () => { const documentsTabs = tabsManager.getTabs( ViewModels.CollectionTabKind.Documents, - tab => tab.tabId === documentsTab.tabId + (tab) => tab.tabId === documentsTab.tabId ); expect(documentsTabs.length).toBe(1); expect(documentsTabs[0]).toEqual(documentsTab); @@ -129,7 +129,7 @@ describe("Tabs manager tests", () => { expect(queryTab.isActive()).toBe(true); expect(documentsTab.isActive()).toBe(false); - tabsManager.closeTabsByComparator(tab => tab.tabId === queryTab.tabId); + tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId); expect(tabsManager.openedTabs().length).toBe(0); expect(tabsManager.activeTab()).toEqual(undefined); expect(queryTab.isActive()).toBe(false); diff --git a/src/Explorer/Tabs/TabsManager.ts b/src/Explorer/Tabs/TabsManager.ts index 86bf6556e..5765e44c1 100644 --- a/src/Explorer/Tabs/TabsManager.ts +++ b/src/Explorer/Tabs/TabsManager.ts @@ -90,6 +90,6 @@ function TabsManagerWrapperViewModel(params: { data: TabsManager }) { export function TabsManagerKOComponent(): unknown { return { viewModel: TabsManagerWrapperViewModel, - template: TabsManagerTemplate + template: TabsManagerTemplate, }; } diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 8ee5658d3..6ebcd72cb 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -91,7 +91,7 @@ export default class TerminalTab extends TabsBase { const info: DataModels.NotebookWorkspaceConnectionInfo = options.container.notebookServerInfo(); return { authToken: info.authToken, - notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}` + notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`, }; } } diff --git a/src/Explorer/Tabs/TriggerTab.html b/src/Explorer/Tabs/TriggerTab.html index bfa074c9f..62aab2dad 100644 --- a/src/Explorer/Tabs/TriggerTab.html +++ b/src/Explorer/Tabs/TriggerTab.html @@ -1,39 +1,39 @@ -
- -
-
Trigger Id
- - - - -
Trigger Type
- - - -
Trigger Operation
- - - -
Trigger Body
-
-
- -
+
+ +
+
Trigger Id
+ + + + +
Trigger Type
+ + + +
Trigger Operation
+ + + +
Trigger Body
+
+
+ +
diff --git a/src/Explorer/Tabs/TriggerTab.ts b/src/Explorer/Tabs/TriggerTab.ts index 2351d7e56..b780dedb1 100644 --- a/src/Explorer/Tabs/TriggerTab.ts +++ b/src/Explorer/Tabs/TriggerTab.ts @@ -33,7 +33,7 @@ export default class TriggerTab extends ScriptTabBase { id: this.id(), body: this.editorContent(), triggerOperation: this.triggerOperation() as TriggerOperation, - triggerType: this.triggerType() as TriggerType + triggerType: this.triggerType() as TriggerType, }); }; @@ -44,17 +44,17 @@ export default class TriggerTab extends ScriptTabBase { const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }); return updateTrigger(this.collection.databaseId, this.collection.id(), { id: this.id(), body: this.editorContent(), triggerOperation: this.triggerOperation() as TriggerOperation, - triggerType: this.triggerType() as TriggerType + triggerType: this.triggerType() as TriggerType, }) .then( - createdResource => { + (createdResource) => { this.resource(createdResource); this.tabTitle(createdResource.id); @@ -68,7 +68,7 @@ export default class TriggerTab extends ScriptTabBase { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, startKey ); @@ -89,7 +89,7 @@ export default class TriggerTab extends ScriptTabBase { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: getErrorMessage(createError), - errorStack: getErrorStack(createError) + errorStack: getErrorStack(createError), }, startKey ); @@ -128,12 +128,12 @@ export default class TriggerTab extends ScriptTabBase { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }); return createTrigger(this.collection.databaseId, this.collection.id(), resource) .then( - createdResource => { + (createdResource) => { this.tabTitle(createdResource.id); this.isNew(false); this.resource(createdResource); @@ -156,7 +156,7 @@ export default class TriggerTab extends ScriptTabBase { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, startKey ); @@ -173,7 +173,7 @@ export default class TriggerTab extends ScriptTabBase { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: getErrorMessage(createError), - errorStack: getErrorStack(createError) + errorStack: getErrorStack(createError), }, startKey ); @@ -188,7 +188,7 @@ export default class TriggerTab extends ScriptTabBase { id: this.id(), body: this.editorContent(), triggerOperation: this.triggerOperation(), - triggerType: this.triggerType() + triggerType: this.triggerType(), }; } } diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.html b/src/Explorer/Tabs/UserDefinedFunctionTab.html index 5a5ac2bee..259604f29 100644 --- a/src/Explorer/Tabs/UserDefinedFunctionTab.html +++ b/src/Explorer/Tabs/UserDefinedFunctionTab.html @@ -1,30 +1,30 @@ -
- -
-
User Defined Function Id
- - - -
User Defined Function Body
-
-
- -
+
+ +
+
User Defined Function Id
+ + + +
User Defined Function Body
+
+
+ +
diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.ts b/src/Explorer/Tabs/UserDefinedFunctionTab.ts index 88e23a9fc..bd2f8c04f 100644 --- a/src/Explorer/Tabs/UserDefinedFunctionTab.ts +++ b/src/Explorer/Tabs/UserDefinedFunctionTab.ts @@ -33,12 +33,12 @@ export default class UserDefinedFunctionTab extends ScriptTabBase { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }); return updateUserDefinedFunction(this.collection.databaseId, this.collection.id(), data) .then( - createdResource => { + (createdResource) => { this.resource(createdResource); this.tabTitle(createdResource.id); @@ -50,7 +50,7 @@ export default class UserDefinedFunctionTab extends ScriptTabBase { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, startKey ); @@ -71,7 +71,7 @@ export default class UserDefinedFunctionTab extends ScriptTabBase { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: getErrorMessage(createError), - errorStack: getErrorStack(createError) + errorStack: getErrorStack(createError), }, startKey ); @@ -104,12 +104,12 @@ export default class UserDefinedFunctionTab extends ScriptTabBase { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }); return createUserDefinedFunction(this.collection.databaseId, this.collection.id(), resource) .then( - createdResource => { + (createdResource) => { this.tabTitle(createdResource.id); this.isNew(false); this.resource(createdResource); @@ -131,7 +131,7 @@ export default class UserDefinedFunctionTab extends ScriptTabBase { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, dataExplorerArea: Constants.Areas.Tab, defaultExperience: this.collection && this.collection.container.defaultExperience(), - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), }, startKey ); @@ -148,7 +148,7 @@ export default class UserDefinedFunctionTab extends ScriptTabBase { dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), error: getErrorMessage(createError), - errorStack: getErrorStack(createError) + errorStack: getErrorStack(createError), }, startKey ); @@ -163,7 +163,7 @@ export default class UserDefinedFunctionTab extends ScriptTabBase { _rid: this.resource()._rid, _self: this.resource()._self, id: this.id(), - body: this.editorContent() + body: this.editorContent(), }; return resource; diff --git a/src/Explorer/Tree/AccessibleVerticalList.ts b/src/Explorer/Tree/AccessibleVerticalList.ts index 96db22df8..9a0ce3943 100644 --- a/src/Explorer/Tree/AccessibleVerticalList.ts +++ b/src/Explorer/Tree/AccessibleVerticalList.ts @@ -1,123 +1,123 @@ -import * as _ from "underscore"; -import * as ko from "knockout"; - -enum ScrollPosition { - Top, - Bottom -} - -export class AccessibleVerticalList { - private items: any[] = []; - private onSelect?: (item: any) => void; - - public currentItemIndex: ko.Observable; - public currentItem: ko.Computed; - - constructor(initialSetOfItems: any[]) { - this.items = initialSetOfItems; - this.currentItemIndex = this.items != null && this.items.length > 0 ? ko.observable(0) : ko.observable(-1); - this.currentItem = ko.computed(() => this.items[this.currentItemIndex()]); - } - - public setOnSelect(onSelect: (item: any) => void): void { - this.onSelect = onSelect; - } - - public onKeyDown = (source: any, event: KeyboardEvent): boolean => { - const targetContainer: Element = event.target; - if (this.items == null || this.items.length === 0) { - // no items so this should be a noop - return true; - } - if (event.keyCode === 32 || event.keyCode === 13) { - // on space or enter keydown - this.onSelect && this.onSelect(this.currentItem()); - event.stopPropagation(); - return false; - } - if (event.keyCode === 38) { - // on UpArrow keydown - event.preventDefault(); - this.selectPreviousItem(); - const targetElement = targetContainer - .getElementsByClassName("accessibleListElement") - .item(this.currentItemIndex()); - if (targetElement) { - this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top); - } - return false; - } - if (event.keyCode === 40) { - // on DownArrow keydown - event.preventDefault(); - this.selectNextItem(); - const targetElement = targetContainer - .getElementsByClassName("accessibleListElement") - .item(this.currentItemIndex()); - if (targetElement) { - this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top); - } - return false; - } - return true; - }; - - public updateItemList(newItemList: any[]) { - if (newItemList == null || newItemList.length === 0) { - this.currentItemIndex(-1); - this.items = []; - return; - } else if (this.currentItemIndex() < 0) { - this.currentItemIndex(0); - } - this.items = newItemList; - } - - public updateCurrentItem(item: any) { - const updatedIndex: number = this.isItemListEmpty() ? -1 : _.indexOf(this.items, item); - this.currentItemIndex(updatedIndex); - } - - private isElementVisibleInContainer(element: Element, container: Element): boolean { - const elementTop = element.getBoundingClientRect().top; - const elementBottom = element.getBoundingClientRect().bottom; - const containerTop = container.getBoundingClientRect().top; - const containerBottom = container.getBoundingClientRect().bottom; - - return elementTop >= containerTop && elementBottom <= containerBottom; - } - - private scrollElementIntoContainerViewIfNeeded( - element: Element, - container: Element, - scrollPosition: ScrollPosition - ): void { - if (!this.isElementVisibleInContainer(element, container)) { - if (scrollPosition === ScrollPosition.Top) { - container.scrollTop = - element.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop; - } else { - container.scrollTop = - element.getBoundingClientRect().bottom - element.getBoundingClientRect().top + container.scrollTop; - } - } - } - - private selectPreviousItem(): void { - if (this.currentItemIndex() <= 0 || this.isItemListEmpty()) { - return; - } - this.currentItemIndex(this.currentItemIndex() - 1); - } - - private selectNextItem(): void { - if (this.isItemListEmpty() || this.currentItemIndex() === this.items.length - 1) { - return; - } - this.currentItemIndex(this.currentItemIndex() + 1); - } - - private isItemListEmpty(): boolean { - return this.items == null || this.items.length === 0; - } -} +import * as _ from "underscore"; +import * as ko from "knockout"; + +enum ScrollPosition { + Top, + Bottom, +} + +export class AccessibleVerticalList { + private items: any[] = []; + private onSelect?: (item: any) => void; + + public currentItemIndex: ko.Observable; + public currentItem: ko.Computed; + + constructor(initialSetOfItems: any[]) { + this.items = initialSetOfItems; + this.currentItemIndex = this.items != null && this.items.length > 0 ? ko.observable(0) : ko.observable(-1); + this.currentItem = ko.computed(() => this.items[this.currentItemIndex()]); + } + + public setOnSelect(onSelect: (item: any) => void): void { + this.onSelect = onSelect; + } + + public onKeyDown = (source: any, event: KeyboardEvent): boolean => { + const targetContainer: Element = event.target; + if (this.items == null || this.items.length === 0) { + // no items so this should be a noop + return true; + } + if (event.keyCode === 32 || event.keyCode === 13) { + // on space or enter keydown + this.onSelect && this.onSelect(this.currentItem()); + event.stopPropagation(); + return false; + } + if (event.keyCode === 38) { + // on UpArrow keydown + event.preventDefault(); + this.selectPreviousItem(); + const targetElement = targetContainer + .getElementsByClassName("accessibleListElement") + .item(this.currentItemIndex()); + if (targetElement) { + this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top); + } + return false; + } + if (event.keyCode === 40) { + // on DownArrow keydown + event.preventDefault(); + this.selectNextItem(); + const targetElement = targetContainer + .getElementsByClassName("accessibleListElement") + .item(this.currentItemIndex()); + if (targetElement) { + this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top); + } + return false; + } + return true; + }; + + public updateItemList(newItemList: any[]) { + if (newItemList == null || newItemList.length === 0) { + this.currentItemIndex(-1); + this.items = []; + return; + } else if (this.currentItemIndex() < 0) { + this.currentItemIndex(0); + } + this.items = newItemList; + } + + public updateCurrentItem(item: any) { + const updatedIndex: number = this.isItemListEmpty() ? -1 : _.indexOf(this.items, item); + this.currentItemIndex(updatedIndex); + } + + private isElementVisibleInContainer(element: Element, container: Element): boolean { + const elementTop = element.getBoundingClientRect().top; + const elementBottom = element.getBoundingClientRect().bottom; + const containerTop = container.getBoundingClientRect().top; + const containerBottom = container.getBoundingClientRect().bottom; + + return elementTop >= containerTop && elementBottom <= containerBottom; + } + + private scrollElementIntoContainerViewIfNeeded( + element: Element, + container: Element, + scrollPosition: ScrollPosition + ): void { + if (!this.isElementVisibleInContainer(element, container)) { + if (scrollPosition === ScrollPosition.Top) { + container.scrollTop = + element.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop; + } else { + container.scrollTop = + element.getBoundingClientRect().bottom - element.getBoundingClientRect().top + container.scrollTop; + } + } + } + + private selectPreviousItem(): void { + if (this.currentItemIndex() <= 0 || this.isItemListEmpty()) { + return; + } + this.currentItemIndex(this.currentItemIndex() - 1); + } + + private selectNextItem(): void { + if (this.isItemListEmpty() || this.currentItemIndex() === this.items.length - 1) { + return; + } + this.currentItemIndex(this.currentItemIndex() + 1); + } + + private isItemListEmpty(): boolean { + return this.items == null || this.items.length === 0; + } +} diff --git a/src/Explorer/Tree/Collection.test.ts b/src/Explorer/Tree/Collection.test.ts index 58d3be7bb..ff7090dba 100644 --- a/src/Explorer/Tree/Collection.test.ts +++ b/src/Explorer/Tree/Collection.test.ts @@ -1,112 +1,112 @@ -import * as DataModels from "../../Contracts/DataModels"; -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import Collection from "./Collection"; -import Explorer from "../Explorer"; -jest.mock("monaco-editor"); - -describe("Collection", () => { - function generateCollection( - container: Explorer, - databaseId: string, - data: DataModels.Collection, - offer: DataModels.Offer - ): Collection { - return new Collection(container, databaseId, data); - } - - function generateMockCollectionsDataModelWithPartitionKey( - partitionKey: DataModels.PartitionKey - ): DataModels.Collection { - return { - defaultTtl: 1, - indexingPolicy: {} as DataModels.IndexingPolicy, - partitionKey, - _rid: "", - _self: "", - _etag: "", - _ts: 1, - id: "" - }; - } - - function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection { - const mockContainer = {} as Explorer; - mockContainer.isPreferredApiMongoDB = ko.computed(() => { - return false; - }); - mockContainer.isPreferredApiCassandra = ko.computed(() => { - return false; - }); - mockContainer.isDatabaseNodeOrNoneSelected = () => { - return false; - }; - mockContainer.isPreferredApiDocumentDB = ko.computed(() => { - return true; - }); - mockContainer.isPreferredApiGraph = ko.computed(() => { - return false; - }); - mockContainer.deleteCollectionText = ko.observable("delete collection"); - - return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer); - } - - describe("Partition key path parsing", () => { - let collection: Collection; - - it("should strip out multiple forward slashes from partition key paths", () => { - const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ - paths: ["/somePartitionKey/anotherPartitionKey"], - kind: "Hash", - version: 2 - }); - collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyProperty).toBe("somePartitionKey.anotherPartitionKey"); - }); - - it("should strip out forward slashes from single partition key paths", () => { - const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ - paths: ["/somePartitionKey"], - kind: "Hash", - version: 2 - }); - collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyProperty).toBe("somePartitionKey"); - }); - }); - - describe("Partition key path header", () => { - let collection: Collection; - - it("should preserve forward slashes on partition keys", () => { - const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ - paths: ["/somePartitionKey/anotherPartitionKey"], - kind: "Hash", - version: 2 - }); - collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey/anotherPartitionKey"); - }); - - it("should preserve forward slash on a single partition key", () => { - const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ - paths: ["/somePartitionKey"], - kind: "Hash", - version: 2 - }); - collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey"); - }); - - it("should be null if there is no partition key", () => { - const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ - version: 2, - paths: [], - kind: "Hash" - }); - collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyPropertyHeader).toBeNull; - }); - }); -}); +import * as DataModels from "../../Contracts/DataModels"; +import * as ko from "knockout"; +import * as ViewModels from "../../Contracts/ViewModels"; +import Collection from "./Collection"; +import Explorer from "../Explorer"; +jest.mock("monaco-editor"); + +describe("Collection", () => { + function generateCollection( + container: Explorer, + databaseId: string, + data: DataModels.Collection, + offer: DataModels.Offer + ): Collection { + return new Collection(container, databaseId, data); + } + + function generateMockCollectionsDataModelWithPartitionKey( + partitionKey: DataModels.PartitionKey + ): DataModels.Collection { + return { + defaultTtl: 1, + indexingPolicy: {} as DataModels.IndexingPolicy, + partitionKey, + _rid: "", + _self: "", + _etag: "", + _ts: 1, + id: "", + }; + } + + function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection { + const mockContainer = {} as Explorer; + mockContainer.isPreferredApiMongoDB = ko.computed(() => { + return false; + }); + mockContainer.isPreferredApiCassandra = ko.computed(() => { + return false; + }); + mockContainer.isDatabaseNodeOrNoneSelected = () => { + return false; + }; + mockContainer.isPreferredApiDocumentDB = ko.computed(() => { + return true; + }); + mockContainer.isPreferredApiGraph = ko.computed(() => { + return false; + }); + mockContainer.deleteCollectionText = ko.observable("delete collection"); + + return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer); + } + + describe("Partition key path parsing", () => { + let collection: Collection; + + it("should strip out multiple forward slashes from partition key paths", () => { + const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ + paths: ["/somePartitionKey/anotherPartitionKey"], + kind: "Hash", + version: 2, + }); + collection = generateMockCollectionWithDataModel(collectionsDataModel); + expect(collection.partitionKeyProperty).toBe("somePartitionKey.anotherPartitionKey"); + }); + + it("should strip out forward slashes from single partition key paths", () => { + const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ + paths: ["/somePartitionKey"], + kind: "Hash", + version: 2, + }); + collection = generateMockCollectionWithDataModel(collectionsDataModel); + expect(collection.partitionKeyProperty).toBe("somePartitionKey"); + }); + }); + + describe("Partition key path header", () => { + let collection: Collection; + + it("should preserve forward slashes on partition keys", () => { + const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ + paths: ["/somePartitionKey/anotherPartitionKey"], + kind: "Hash", + version: 2, + }); + collection = generateMockCollectionWithDataModel(collectionsDataModel); + expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey/anotherPartitionKey"); + }); + + it("should preserve forward slash on a single partition key", () => { + const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ + paths: ["/somePartitionKey"], + kind: "Hash", + version: 2, + }); + collection = generateMockCollectionWithDataModel(collectionsDataModel); + expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey"); + }); + + it("should be null if there is no partition key", () => { + const collectionsDataModel = generateMockCollectionsDataModelWithPartitionKey({ + version: 2, + paths: [], + kind: "Hash", + }); + collection = generateMockCollectionWithDataModel(collectionsDataModel); + expect(collection.partitionKeyPropertyHeader).toBeNull; + }); + }); +}); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 0cc27d48d..5311e8b3f 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -122,10 +122,7 @@ export default class Collection implements ViewModels.Collection { this.partitionKey.paths && this.partitionKey.paths.length && this.partitionKey.paths.length > 0 && - this.partitionKey.paths[0] - .replace(/[/]+/g, ".") - .substr(1) - .replace(/[']+/g, "")) || + this.partitionKey.paths[0].replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")) || null; this.partitionKeyPropertyHeader = (this.partitionKey && @@ -155,28 +152,28 @@ export default class Collection implements ViewModels.Collection { this.focusedSubnodeKind = ko.observable(); this.documentsFocused = ko.observable(); - this.documentsFocused.subscribe(focus => { + this.documentsFocused.subscribe((focus) => { console.log("Focus set on Documents: " + focus); this.focusedSubnodeKind(ViewModels.CollectionTabKind.Documents); }); this.settingsFocused = ko.observable(false); - this.settingsFocused.subscribe(focus => { + this.settingsFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.Settings); }); this.storedProceduresFocused = ko.observable(false); - this.storedProceduresFocused.subscribe(focus => { + this.storedProceduresFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); }); this.userDefinedFunctionsFocused = ko.observable(false); - this.userDefinedFunctionsFocused.subscribe(focus => { + this.userDefinedFunctionsFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); }); this.triggersFocused = ko.observable(false); - this.triggersFocused.subscribe(focus => { + this.triggersFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.Triggers); }); @@ -184,20 +181,20 @@ export default class Collection implements ViewModels.Collection { this.storedProcedures = ko.computed(() => { return this.children() - .filter(node => node.nodeKind === "StoredProcedure") - .map(node => node); + .filter((node) => node.nodeKind === "StoredProcedure") + .map((node) => node); }); this.userDefinedFunctions = ko.computed(() => { return this.children() - .filter(node => node.nodeKind === "UserDefinedFunction") - .map(node => node); + .filter((node) => node.nodeKind === "UserDefinedFunction") + .map((node) => node); }); this.triggers = ko.computed(() => { return this.children() - .filter(node => node.nodeKind === "Trigger") - .map(node => node); + .filter((node) => node.nodeKind === "Trigger") + .map((node) => node); }); const showScriptsMenus: boolean = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); @@ -228,7 +225,7 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); if (this.isCollectionExpanded()) { this.collapseCollection(); @@ -237,7 +234,7 @@ export default class Collection implements ViewModels.Collection { } this.container.onUpdateTabsButtons([]); this.container.tabsManager.refreshActiveTab( - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ); } @@ -253,7 +250,7 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); } @@ -269,7 +266,7 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); return Q.resolve(); @@ -284,12 +281,12 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( ViewModels.CollectionTabKind.Documents, - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ) as DocumentsTab[]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; @@ -302,7 +299,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: "Items" + tabTitle: "Items", }); this.documentIds([]); @@ -317,7 +314,7 @@ export default class Collection implements ViewModels.Collection { tabPath: `${this.databaseId}>${this.id()}>Documents`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/documents`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(documentsTab); @@ -333,12 +330,12 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); const conflictsTabs: ConflictsTab[] = this.container.tabsManager.getTabs( ViewModels.CollectionTabKind.Conflicts, - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ) as ConflictsTab[]; let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0]; @@ -351,7 +348,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: "Conflicts" + tabTitle: "Conflicts", }); this.documentIds([]); @@ -366,7 +363,7 @@ export default class Collection implements ViewModels.Collection { tabPath: `${this.databaseId}>${this.id()}>Conflicts`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/conflicts`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(conflictsTab); @@ -382,7 +379,7 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); if (this.container.isPreferredApiCassandra() && !this.cassandraKeys) { @@ -393,7 +390,7 @@ export default class Collection implements ViewModels.Collection { const queryTablesTabs: QueryTablesTab[] = this.container.tabsManager.getTabs( ViewModels.CollectionTabKind.QueryTables, - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ) as QueryTablesTab[]; let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0]; @@ -411,7 +408,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: title + tabTitle: title, }); queryTablesTab = new QueryTablesTab({ @@ -425,7 +422,7 @@ export default class Collection implements ViewModels.Collection { hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`, isActive: ko.observable(false), onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(queryTablesTab); @@ -441,12 +438,12 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); const graphTabs: GraphTab[] = this.container.tabsManager.getTabs( ViewModels.CollectionTabKind.Graph, - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ) as GraphTab[]; let graphTab: GraphTab = graphTabs && graphTabs[0]; @@ -461,7 +458,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: title + tabTitle: title, }); graphTab = new GraphTab({ @@ -480,7 +477,7 @@ export default class Collection implements ViewModels.Collection { databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(graphTab); @@ -496,12 +493,12 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs( ViewModels.CollectionTabKind.Documents, - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ) as MongoDocumentsTab[]; let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0]; @@ -514,7 +511,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: "Documents" + tabTitle: "Documents", }); this.documentIds([]); @@ -531,7 +528,7 @@ export default class Collection implements ViewModels.Collection { hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`, isActive: ko.observable(false), onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(mongoDocumentsTab); } @@ -546,12 +543,12 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); - const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, tab => { + const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => { return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); }); @@ -561,7 +558,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: tabTitle + tabTitle: tabTitle, }; const settingsTabOptions: ViewModels.TabOptions = { @@ -572,7 +569,7 @@ export default class Collection implements ViewModels.Collection { node: this, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`, isActive: ko.observable(false), - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }; let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2); @@ -587,7 +584,7 @@ export default class Collection implements ViewModels.Collection { ): void => { const settingsTabV2Options: ViewModels.SettingsTabV2Options = { ...settingsTabOptions, - getPendingNotification: getPendingNotification + getPendingNotification: getPendingNotification, }; if (!settingsTabV2) { @@ -611,7 +608,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: title + tabTitle: title, }); const queryTab: QueryTab = new QueryTab({ @@ -625,7 +622,7 @@ export default class Collection implements ViewModels.Collection { queryText: queryText, partitionKey: collection.partitionKey, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(queryTab); @@ -642,7 +639,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: title + tabTitle: title, }); const mongoQueryTab: MongoQueryTab = new MongoQueryTab({ @@ -655,7 +652,7 @@ export default class Collection implements ViewModels.Collection { isActive: ko.observable(false), partitionKey: collection.partitionKey, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(mongoQueryTab); @@ -671,7 +668,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: title + tabTitle: title, }); const graphTab: GraphTab = new GraphTab({ @@ -689,7 +686,7 @@ export default class Collection implements ViewModels.Collection { databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(graphTab); @@ -705,7 +702,7 @@ export default class Collection implements ViewModels.Collection { node: this, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`, isActive: ko.observable(false), - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(mongoShellTab); @@ -767,7 +764,7 @@ export default class Collection implements ViewModels.Collection { this.expandStoredProcedures(); } this.container.tabsManager.refreshActiveTab( - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ); } @@ -785,10 +782,10 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); }, - error => { + (error) => { TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Failed, { description: "Stored procedures node", databaseAccountName: this.container.databaseAccount().name, @@ -796,7 +793,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error) + error: getErrorMessage(error), }); } ); @@ -814,7 +811,7 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); } @@ -826,7 +823,7 @@ export default class Collection implements ViewModels.Collection { this.expandUserDefinedFunctions(); } this.container.tabsManager.refreshActiveTab( - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ); } @@ -844,10 +841,10 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); }, - error => { + (error) => { TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Failed, { description: "UDF node", databaseAccountName: this.container.databaseAccount().name, @@ -855,7 +852,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error) + error: getErrorMessage(error), }); } ); @@ -873,7 +870,7 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); } @@ -885,7 +882,7 @@ export default class Collection implements ViewModels.Collection { this.expandTriggers(); } this.container.tabsManager.refreshActiveTab( - tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() ); } @@ -903,10 +900,10 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); }, - error => { + (error) => { this.isTriggersExpanded(true); TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, { description: "Triggers node", @@ -915,7 +912,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error) + error: getErrorMessage(error), }); } ); @@ -933,36 +930,36 @@ export default class Collection implements ViewModels.Collection { databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); } public loadStoredProcedures(): Promise { - return readStoredProcedures(this.databaseId, this.id()).then(storedProcedures => { + return readStoredProcedures(this.databaseId, this.id()).then((storedProcedures) => { const storedProceduresNodes: ViewModels.TreeNode[] = storedProcedures.map( - storedProcedure => new StoredProcedure(this.container, this, storedProcedure) + (storedProcedure) => new StoredProcedure(this.container, this, storedProcedure) ); - const otherNodes = this.children().filter(node => node.nodeKind !== "StoredProcedure"); + const otherNodes = this.children().filter((node) => node.nodeKind !== "StoredProcedure"); const allNodes = otherNodes.concat(storedProceduresNodes); this.children(allNodes); }); } public loadUserDefinedFunctions(): Promise { - return readUserDefinedFunctions(this.databaseId, this.id()).then(userDefinedFunctions => { + return readUserDefinedFunctions(this.databaseId, this.id()).then((userDefinedFunctions) => { const userDefinedFunctionsNodes: ViewModels.TreeNode[] = userDefinedFunctions.map( - udf => new UserDefinedFunction(this.container, this, udf) + (udf) => new UserDefinedFunction(this.container, this, udf) ); - const otherNodes = this.children().filter(node => node.nodeKind !== "UserDefinedFunction"); + const otherNodes = this.children().filter((node) => node.nodeKind !== "UserDefinedFunction"); const allNodes = otherNodes.concat(userDefinedFunctionsNodes); this.children(allNodes); }); } public loadTriggers(): Promise { - return readTriggers(this.databaseId, this.id()).then(triggers => { - const triggerNodes: ViewModels.TreeNode[] = triggers.map(trigger => new Trigger(this.container, this, trigger)); - const otherNodes = this.children().filter(node => node.nodeKind !== "Trigger"); + return readTriggers(this.databaseId, this.id()).then((triggers) => { + const triggerNodes: ViewModels.TreeNode[] = triggers.map((trigger) => new Trigger(this.container, this, trigger)); + const otherNodes = this.children().filter((node) => node.nodeKind !== "Trigger"); const allNodes = otherNodes.concat(triggerNodes); this.children(allNodes); }); @@ -1039,8 +1036,8 @@ export default class Collection implements ViewModels.Collection { endpoint: userContext.endpoint, accessToken: userContext.accessToken, platform: configContext.platform, - databaseAccount: userContext.databaseAccount - } + databaseAccount: userContext.databaseAccount, + }, }; documentUploader.postMessage(uploaderMessage); @@ -1072,7 +1069,7 @@ export default class Collection implements ViewModels.Collection { const reader = new FileReader(); reader.onload = (evt: any): void => { const fileData: string = evt.target.result; - this._createDocumentsFromFile(file.name, fileData).then(record => { + this._createDocumentsFromFile(file.name, fileData).then((record) => { deferred.resolve(record); }); }; @@ -1082,7 +1079,7 @@ export default class Collection implements ViewModels.Collection { fileName: file.name, numSucceeded: 0, numFailed: 1, - errors: [(evt as any).error.message] + errors: [(evt as any).error.message], }); }; @@ -1096,7 +1093,7 @@ export default class Collection implements ViewModels.Collection { fileName: fileName, numSucceeded: 0, numFailed: 0, - errors: [] + errors: [], }; try { @@ -1104,7 +1101,7 @@ export default class Collection implements ViewModels.Collection { if (Array.isArray(content)) { await Promise.all( - content.map(async documentContent => { + content.map(async (documentContent) => { await createDocument(this, documentContent); record.numSucceeded++; }) @@ -1153,7 +1150,7 @@ export default class Collection implements ViewModels.Collection { error: getErrorMessage(error), accountName: this.container && this.container.databaseAccount(), databaseName: this.databaseId, - collectionName: this.id() + collectionName: this.id(), }), "Settings tree node" ); @@ -1247,13 +1244,13 @@ export default class Collection implements ViewModels.Collection { databaseAccountName: this.container.databaseAccount().name, databaseName: this.databaseId, collectionName: this.id(), - defaultExperience: this.container.defaultExperience() + defaultExperience: this.container.defaultExperience(), }); const params: DataModels.ReadCollectionOfferParams = { collectionId: this.id(), collectionResourceId: this.self, - databaseId: this.databaseId + databaseId: this.databaseId, }; try { @@ -1266,7 +1263,7 @@ export default class Collection implements ViewModels.Collection { databaseAccountName: this.container.databaseAccount().name, databaseName: this.databaseId, collectionName: this.id(), - defaultExperience: this.container.defaultExperience() + defaultExperience: this.container.defaultExperience(), }, startKey ); @@ -1279,7 +1276,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), error: getErrorMessage(error), - errorStack: getErrorStack(error) + errorStack: getErrorStack(error), }, startKey ); diff --git a/src/Explorer/Tree/ConflictId.ts b/src/Explorer/Tree/ConflictId.ts index 9b43019e9..c9cf1a98a 100644 --- a/src/Explorer/Tree/ConflictId.ts +++ b/src/Explorer/Tree/ConflictId.ts @@ -139,7 +139,7 @@ export default class ConflictId { id, partitionKeyValue: partitionKeyValueResolved, partitionKeyProperty: this.partitionKeyProperty, - partitionKey: this.partitionKey + partitionKey: this.partitionKey, }, partitionKeyValueResolved ); diff --git a/src/Explorer/Tree/Database.test.ts b/src/Explorer/Tree/Database.test.ts index a067f6747..b41e5a3d0 100644 --- a/src/Explorer/Tree/Database.test.ts +++ b/src/Explorer/Tree/Database.test.ts @@ -25,9 +25,9 @@ updateUserContext({ documentEndpoint: "fakeEndpoint", tableEndpoint: "fakeEndpoint", gremlinEndpoint: "fakeEndpoint", - cassandraEndpoint: "fakeEndpoint" - } - } + cassandraEndpoint: "fakeEndpoint", + }, + }, }); describe("Add Schema", () => { @@ -70,7 +70,7 @@ describe("Add Schema", () => { resourceGroup: userContext.resourceGroup, accountName: userContext.databaseAccount.name, resource: `dbs/${database.id}/colls/${collection.id}`, - status: "new" + status: "new", }); expect(checkForSchema).not.toBeNull(); expect(database.junoClient.getSchema).toBeCalledWith( diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.ts index c160d44b8..271cb3ab8 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.ts @@ -1,357 +1,357 @@ -import * as _ from "underscore"; -import * as ko from "knockout"; -import Q from "q"; -import * as ViewModels from "../../Contracts/ViewModels"; -import * as Constants from "../../Common/Constants"; -import * as DataModels from "../../Contracts/DataModels"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab"; -import Collection from "./Collection"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import * as Logger from "../../Common/Logger"; -import Explorer from "../Explorer"; -import { readCollections } from "../../Common/dataAccess/readCollections"; -import { JunoClient, IJunoResponse } from "../../Juno/JunoClient"; -import { userContext } from "../../UserContext"; -import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; -import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; -import { fetchPortalNotifications } from "../../Common/PortalNotifications"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; - -export default class Database implements ViewModels.Database { - public nodeKind: string; - public container: Explorer; - public self: string; - public rid: string; - public id: ko.Observable; - public offer: ko.Observable; - public collections: ko.ObservableArray; - public isDatabaseExpanded: ko.Observable; - public isDatabaseShared: ko.Computed; - public selectedSubnodeKind: ko.Observable; - public junoClient: JunoClient; - - constructor(container: Explorer, data: any) { - this.nodeKind = "Database"; - this.container = container; - this.self = data._self; - this.rid = data._rid; - this.id = ko.observable(data.id); - this.offer = ko.observable(); - this.collections = ko.observableArray(); - this.isDatabaseExpanded = ko.observable(false); - this.selectedSubnodeKind = ko.observable(); - this.isDatabaseShared = ko.pureComputed(() => { - return this.offer && !!this.offer(); - }); - this.junoClient = new JunoClient(); - } - - public onSettingsClick = () => { - this.container.selectedNode(this); - this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Settings node", - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - - const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); - const matchingTabs = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.DatabaseSettings, - tab => tab.node?.id() === this.id() - ); - let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab); - if (!settingsTab) { - const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { - databaseAccountName: this.container.databaseAccount().name, - databaseName: this.id(), - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: "Scale" - }); - pendingNotificationsPromise.then( - (data: any) => { - const pendingNotification: DataModels.Notification = data && data[0]; - settingsTab = new DatabaseSettingsTab({ - tabKind: ViewModels.CollectionTabKind.DatabaseSettings, - title: "Scale", - tabPath: "", - node: this, - rid: this.rid, - database: this, - hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`, - isActive: ko.observable(false), - onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons - }); - - settingsTab.pendingNotification(pendingNotification); - this.container.tabsManager.activateNewTab(settingsTab); - }, - (error: any) => { - const errorMessage = getErrorMessage(error); - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseAccountName: this.container.databaseAccount().name, - databaseName: this.id(), - collectionName: this.id(), - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: "Scale", - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while fetching database settings for database ${this.id()}: ${errorMessage}` - ); - throw error; - } - ); - } else { - pendingNotificationsPromise.then( - (pendingNotification: DataModels.Notification) => { - settingsTab.pendingNotification(pendingNotification); - this.container.tabsManager.activateTab(settingsTab); - }, - (error: any) => { - settingsTab.pendingNotification(undefined); - this.container.tabsManager.activateTab(settingsTab); - } - ); - } - }; - - public isDatabaseNodeSelected(): boolean { - return ( - !this.isDatabaseExpanded() && - this.container.selectedNode && - this.container.selectedNode() && - this.container.selectedNode().nodeKind === "Database" && - this.container.selectedNode().id() === this.id() - ); - } - - public onDeleteDatabaseContextMenuClick(source: ViewModels.Database, event: MouseEvent | KeyboardEvent) { - this.container.deleteDatabaseConfirmationPane.open(); - } - - public selectDatabase() { - this.container.selectedNode(this); - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Database node", - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - public async expandDatabase() { - if (this.isDatabaseExpanded()) { - return; - } - - await this.loadOffer(); - await this.loadCollections(); - this.isDatabaseExpanded(true); - TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, { - description: "Database node", - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - public collapseDatabase() { - if (!this.isDatabaseExpanded()) { - return; - } - - this.isDatabaseExpanded(false); - TelemetryProcessor.trace(Action.CollapseTreeNode, ActionModifiers.Mark, { - description: "Database node", - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - public async loadCollections(): Promise { - const collectionVMs: Collection[] = []; - const collections: DataModels.Collection[] = await readCollections(this.id()); - const deltaCollections = this.getDeltaCollections(collections); - - collections.forEach((collection: DataModels.Collection) => { - this.addSchema(collection); - }); - - deltaCollections.toAdd.forEach((collection: DataModels.Collection) => { - const collectionVM: Collection = new Collection(this.container, this.id(), collection); - collectionVMs.push(collectionVM); - }); - - //merge collections - this.addCollectionsToList(collectionVMs); - this.deleteCollectionsFromList(deltaCollections.toDelete); - } - - public openAddCollection(database: Database, event: MouseEvent) { - database.container.addCollectionPane.databaseId(database.id()); - database.container.addCollectionPane.open(); - } - - public findCollectionWithId(collectionId: string): ViewModels.Collection { - return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId); - } - - public async loadOffer(): Promise { - if (!this.container.isServerlessEnabled() && !this.offer()) { - const params: DataModels.ReadDatabaseOfferParams = { - databaseId: this.id(), - databaseResourceId: this.self - }; - this.offer(await readDatabaseOffer(params)); - } - } - - private _getPendingThroughputSplitNotification(): Q.Promise { - if (!this.container) { - return Q.resolve(undefined); - } - - const deferred: Q.Deferred = Q.defer(); - fetchPortalNotifications().then( - notifications => { - if (!notifications || notifications.length === 0) { - deferred.resolve(undefined); - return; - } - - const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => { - const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress"); - return ( - notification.kind === "message" && - !notification.collectionName && - notification.databaseName === this.id() && - notification.description && - throughputUpdateRegExp.test(notification.description) - ); - }); - - deferred.resolve(pendingNotification); - }, - (error: any) => { - Logger.logError( - JSON.stringify({ - error: getErrorMessage(error), - accountName: this.container && this.container.databaseAccount(), - databaseName: this.id(), - collectionName: this.id() - }), - "Settings tree node" - ); - deferred.resolve(undefined); - } - ); - - return deferred.promise; - } - - private getDeltaCollections( - updatedCollectionsList: DataModels.Collection[] - ): { toAdd: DataModels.Collection[]; toDelete: Collection[] } { - const collectionsToAdd: DataModels.Collection[] = _.filter( - updatedCollectionsList, - (collection: DataModels.Collection) => { - const collectionExists = _.some( - this.collections(), - (existingCollection: Collection) => existingCollection.id() === collection.id - ); - return !collectionExists; - } - ); - - let collectionsToDelete: Collection[] = []; - ko.utils.arrayForEach(this.collections(), (collection: Collection) => { - const collectionPresentInUpdatedList = _.some( - updatedCollectionsList, - (coll: DataModels.Collection) => coll.id === collection.id() - ); - if (!collectionPresentInUpdatedList) { - collectionsToDelete.push(collection); - } - }); - - return { toAdd: collectionsToAdd, toDelete: collectionsToDelete }; - } - - private addCollectionsToList(collections: Collection[]): void { - this.collections( - this.collections() - .concat(collections) - .sort((collection1, collection2) => collection1.id().localeCompare(collection2.id())) - ); - } - - private deleteCollectionsFromList(collectionsToRemove: Collection[]): void { - if (collectionsToRemove.length === 0) { - return; - } - - const collectionsToKeep: Collection[] = []; - - ko.utils.arrayForEach(this.collections(), (collection: Collection) => { - const shouldRemoveCollection = _.some(collectionsToRemove, (coll: Collection) => coll.id() === collection.id()); - if (!shouldRemoveCollection) { - collectionsToKeep.push(collection); - } - }); - - this.collections(collectionsToKeep); - } - - public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout { - let checkForSchema: NodeJS.Timeout = null; - interval = interval || 5000; - - if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) { - collection.requestSchema = () => { - this.junoClient.requestSchema({ - id: undefined, - subscriptionId: userContext.subscriptionId, - resourceGroup: userContext.resourceGroup, - accountName: userContext.databaseAccount.name, - resource: `dbs/${this.id}/colls/${collection.id}`, - status: "new" - }); - checkForSchema = setInterval(async () => { - const response: IJunoResponse = await this.junoClient.getSchema( - userContext.databaseAccount.name, - this.id(), - collection.id - ); - - if (response.status >= 404) { - clearInterval(checkForSchema); - } - - if (response.data !== null) { - clearInterval(checkForSchema); - collection.schema = response.data; - } - }, interval); - }; - - collection.requestSchema(); - } - - return checkForSchema; - } -} +import * as _ from "underscore"; +import * as ko from "knockout"; +import Q from "q"; +import * as ViewModels from "../../Contracts/ViewModels"; +import * as Constants from "../../Common/Constants"; +import * as DataModels from "../../Contracts/DataModels"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab"; +import Collection from "./Collection"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; +import * as Logger from "../../Common/Logger"; +import Explorer from "../Explorer"; +import { readCollections } from "../../Common/dataAccess/readCollections"; +import { JunoClient, IJunoResponse } from "../../Juno/JunoClient"; +import { userContext } from "../../UserContext"; +import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; +import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; +import { fetchPortalNotifications } from "../../Common/PortalNotifications"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; + +export default class Database implements ViewModels.Database { + public nodeKind: string; + public container: Explorer; + public self: string; + public rid: string; + public id: ko.Observable; + public offer: ko.Observable; + public collections: ko.ObservableArray; + public isDatabaseExpanded: ko.Observable; + public isDatabaseShared: ko.Computed; + public selectedSubnodeKind: ko.Observable; + public junoClient: JunoClient; + + constructor(container: Explorer, data: any) { + this.nodeKind = "Database"; + this.container = container; + this.self = data._self; + this.rid = data._rid; + this.id = ko.observable(data.id); + this.offer = ko.observable(); + this.collections = ko.observableArray(); + this.isDatabaseExpanded = ko.observable(false); + this.selectedSubnodeKind = ko.observable(); + this.isDatabaseShared = ko.pureComputed(() => { + return this.offer && !!this.offer(); + }); + this.junoClient = new JunoClient(); + } + + public onSettingsClick = () => { + this.container.selectedNode(this); + this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); + TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { + description: "Settings node", + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + + const pendingNotificationsPromise: Q.Promise = this._getPendingThroughputSplitNotification(); + const matchingTabs = this.container.tabsManager.getTabs( + ViewModels.CollectionTabKind.DatabaseSettings, + (tab) => tab.node?.id() === this.id() + ); + let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab); + if (!settingsTab) { + const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { + databaseAccountName: this.container.databaseAccount().name, + databaseName: this.id(), + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: "Scale", + }); + pendingNotificationsPromise.then( + (data: any) => { + const pendingNotification: DataModels.Notification = data && data[0]; + settingsTab = new DatabaseSettingsTab({ + tabKind: ViewModels.CollectionTabKind.DatabaseSettings, + title: "Scale", + tabPath: "", + node: this, + rid: this.rid, + database: this, + hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`, + isActive: ko.observable(false), + onLoadStartKey: startKey, + onUpdateTabsButtons: this.container.onUpdateTabsButtons, + }); + + settingsTab.pendingNotification(pendingNotification); + this.container.tabsManager.activateNewTab(settingsTab); + }, + (error: any) => { + const errorMessage = getErrorMessage(error); + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseAccountName: this.container.databaseAccount().name, + databaseName: this.id(), + collectionName: this.id(), + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.Tab, + tabTitle: "Scale", + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Error while fetching database settings for database ${this.id()}: ${errorMessage}` + ); + throw error; + } + ); + } else { + pendingNotificationsPromise.then( + (pendingNotification: DataModels.Notification) => { + settingsTab.pendingNotification(pendingNotification); + this.container.tabsManager.activateTab(settingsTab); + }, + (error: any) => { + settingsTab.pendingNotification(undefined); + this.container.tabsManager.activateTab(settingsTab); + } + ); + } + }; + + public isDatabaseNodeSelected(): boolean { + return ( + !this.isDatabaseExpanded() && + this.container.selectedNode && + this.container.selectedNode() && + this.container.selectedNode().nodeKind === "Database" && + this.container.selectedNode().id() === this.id() + ); + } + + public onDeleteDatabaseContextMenuClick(source: ViewModels.Database, event: MouseEvent | KeyboardEvent) { + this.container.deleteDatabaseConfirmationPane.open(); + } + + public selectDatabase() { + this.container.selectedNode(this); + TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { + description: "Database node", + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + } + + public async expandDatabase() { + if (this.isDatabaseExpanded()) { + return; + } + + await this.loadOffer(); + await this.loadCollections(); + this.isDatabaseExpanded(true); + TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, { + description: "Database node", + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + } + + public collapseDatabase() { + if (!this.isDatabaseExpanded()) { + return; + } + + this.isDatabaseExpanded(false); + TelemetryProcessor.trace(Action.CollapseTreeNode, ActionModifiers.Mark, { + description: "Database node", + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + } + + public async loadCollections(): Promise { + const collectionVMs: Collection[] = []; + const collections: DataModels.Collection[] = await readCollections(this.id()); + const deltaCollections = this.getDeltaCollections(collections); + + collections.forEach((collection: DataModels.Collection) => { + this.addSchema(collection); + }); + + deltaCollections.toAdd.forEach((collection: DataModels.Collection) => { + const collectionVM: Collection = new Collection(this.container, this.id(), collection); + collectionVMs.push(collectionVM); + }); + + //merge collections + this.addCollectionsToList(collectionVMs); + this.deleteCollectionsFromList(deltaCollections.toDelete); + } + + public openAddCollection(database: Database, event: MouseEvent) { + database.container.addCollectionPane.databaseId(database.id()); + database.container.addCollectionPane.open(); + } + + public findCollectionWithId(collectionId: string): ViewModels.Collection { + return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId); + } + + public async loadOffer(): Promise { + if (!this.container.isServerlessEnabled() && !this.offer()) { + const params: DataModels.ReadDatabaseOfferParams = { + databaseId: this.id(), + databaseResourceId: this.self, + }; + this.offer(await readDatabaseOffer(params)); + } + } + + private _getPendingThroughputSplitNotification(): Q.Promise { + if (!this.container) { + return Q.resolve(undefined); + } + + const deferred: Q.Deferred = Q.defer(); + fetchPortalNotifications().then( + (notifications) => { + if (!notifications || notifications.length === 0) { + deferred.resolve(undefined); + return; + } + + const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => { + const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress"); + return ( + notification.kind === "message" && + !notification.collectionName && + notification.databaseName === this.id() && + notification.description && + throughputUpdateRegExp.test(notification.description) + ); + }); + + deferred.resolve(pendingNotification); + }, + (error: any) => { + Logger.logError( + JSON.stringify({ + error: getErrorMessage(error), + accountName: this.container && this.container.databaseAccount(), + databaseName: this.id(), + collectionName: this.id(), + }), + "Settings tree node" + ); + deferred.resolve(undefined); + } + ); + + return deferred.promise; + } + + private getDeltaCollections( + updatedCollectionsList: DataModels.Collection[] + ): { toAdd: DataModels.Collection[]; toDelete: Collection[] } { + const collectionsToAdd: DataModels.Collection[] = _.filter( + updatedCollectionsList, + (collection: DataModels.Collection) => { + const collectionExists = _.some( + this.collections(), + (existingCollection: Collection) => existingCollection.id() === collection.id + ); + return !collectionExists; + } + ); + + let collectionsToDelete: Collection[] = []; + ko.utils.arrayForEach(this.collections(), (collection: Collection) => { + const collectionPresentInUpdatedList = _.some( + updatedCollectionsList, + (coll: DataModels.Collection) => coll.id === collection.id() + ); + if (!collectionPresentInUpdatedList) { + collectionsToDelete.push(collection); + } + }); + + return { toAdd: collectionsToAdd, toDelete: collectionsToDelete }; + } + + private addCollectionsToList(collections: Collection[]): void { + this.collections( + this.collections() + .concat(collections) + .sort((collection1, collection2) => collection1.id().localeCompare(collection2.id())) + ); + } + + private deleteCollectionsFromList(collectionsToRemove: Collection[]): void { + if (collectionsToRemove.length === 0) { + return; + } + + const collectionsToKeep: Collection[] = []; + + ko.utils.arrayForEach(this.collections(), (collection: Collection) => { + const shouldRemoveCollection = _.some(collectionsToRemove, (coll: Collection) => coll.id() === collection.id()); + if (!shouldRemoveCollection) { + collectionsToKeep.push(collection); + } + }); + + this.collections(collectionsToKeep); + } + + public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout { + let checkForSchema: NodeJS.Timeout = null; + interval = interval || 5000; + + if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) { + collection.requestSchema = () => { + this.junoClient.requestSchema({ + id: undefined, + subscriptionId: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + accountName: userContext.databaseAccount.name, + resource: `dbs/${this.id}/colls/${collection.id}`, + status: "new", + }); + checkForSchema = setInterval(async () => { + const response: IJunoResponse = await this.junoClient.getSchema( + userContext.databaseAccount.name, + this.id(), + collection.id + ); + + if (response.status >= 404) { + clearInterval(checkForSchema); + } + + if (response.data !== null) { + clearInterval(checkForSchema); + collection.schema = response.data; + } + }, interval); + }; + + collection.requestSchema(); + } + + return checkForSchema; + } +} diff --git a/src/Explorer/Tree/DocumentId.ts b/src/Explorer/Tree/DocumentId.ts index 2c043430c..a30c0b534 100644 --- a/src/Explorer/Tree/DocumentId.ts +++ b/src/Explorer/Tree/DocumentId.ts @@ -1,71 +1,71 @@ -import * as ko from "knockout"; -import * as DataModels from "../../Contracts/DataModels"; -import DocumentsTab from "../Tabs/DocumentsTab"; - -export default class DocumentId { - public container: DocumentsTab; - public rid: string; - public self: string; - public ts: string; - public id: ko.Observable; - public partitionKeyProperty: string; - public partitionKey: DataModels.PartitionKey; - public partitionKeyValue: any; - public stringPartitionKeyValue: string; - public isDirty: ko.Observable; - - constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { - this.container = container; - this.self = data._self; - this.rid = data._rid; - this.ts = data._ts; - this.partitionKeyValue = partitionKeyValue; - this.partitionKeyProperty = container && container.partitionKeyProperty; - this.partitionKey = container && container.partitionKey; - this.stringPartitionKeyValue = this.getPartitionKeyValueAsString(); - this.id = ko.observable(data.id); - this.isDirty = ko.observable(false); - } - - public click() { - if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) { - this.loadDocument(); - } - return; - } - - public partitionKeyHeader(): Object { - if (!this.partitionKeyProperty) { - return undefined; - } - - if (this.partitionKeyValue === undefined) { - return [{}]; - } - - return [this.partitionKeyValue]; - } - - public getPartitionKeyValueAsString(): string { - const partitionKeyValue: any = this.partitionKeyValue; - const typeOfPartitionKeyValue: string = typeof partitionKeyValue; - - if ( - typeOfPartitionKeyValue === "undefined" || - typeOfPartitionKeyValue === "null" || - typeOfPartitionKeyValue === "object" - ) { - return ""; - } - - if (typeOfPartitionKeyValue === "string") { - return partitionKeyValue; - } - - return JSON.stringify(partitionKeyValue); - } - - public async loadDocument(): Promise { - await this.container.selectDocument(this); - } -} +import * as ko from "knockout"; +import * as DataModels from "../../Contracts/DataModels"; +import DocumentsTab from "../Tabs/DocumentsTab"; + +export default class DocumentId { + public container: DocumentsTab; + public rid: string; + public self: string; + public ts: string; + public id: ko.Observable; + public partitionKeyProperty: string; + public partitionKey: DataModels.PartitionKey; + public partitionKeyValue: any; + public stringPartitionKeyValue: string; + public isDirty: ko.Observable; + + constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { + this.container = container; + this.self = data._self; + this.rid = data._rid; + this.ts = data._ts; + this.partitionKeyValue = partitionKeyValue; + this.partitionKeyProperty = container && container.partitionKeyProperty; + this.partitionKey = container && container.partitionKey; + this.stringPartitionKeyValue = this.getPartitionKeyValueAsString(); + this.id = ko.observable(data.id); + this.isDirty = ko.observable(false); + } + + public click() { + if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) { + this.loadDocument(); + } + return; + } + + public partitionKeyHeader(): Object { + if (!this.partitionKeyProperty) { + return undefined; + } + + if (this.partitionKeyValue === undefined) { + return [{}]; + } + + return [this.partitionKeyValue]; + } + + public getPartitionKeyValueAsString(): string { + const partitionKeyValue: any = this.partitionKeyValue; + const typeOfPartitionKeyValue: string = typeof partitionKeyValue; + + if ( + typeOfPartitionKeyValue === "undefined" || + typeOfPartitionKeyValue === "null" || + typeOfPartitionKeyValue === "object" + ) { + return ""; + } + + if (typeOfPartitionKeyValue === "string") { + return partitionKeyValue; + } + + return JSON.stringify(partitionKeyValue); + } + + public async loadDocument(): Promise { + await this.container.selectDocument(this); + } +} diff --git a/src/Explorer/Tree/ObjectId.ts b/src/Explorer/Tree/ObjectId.ts index 04be68be9..fc53a6a37 100644 --- a/src/Explorer/Tree/ObjectId.ts +++ b/src/Explorer/Tree/ObjectId.ts @@ -1,14 +1,14 @@ -import * as ko from "knockout"; -import DocumentId from "./DocumentId"; -import DocumentsTab from "../Tabs/DocumentsTab"; - -export default class ObjectId extends DocumentId { - constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { - super(container, data, partitionKeyValue); - if (typeof data._id === "object") { - this.id = ko.observable(data._id[Object.keys(data._id)[0]]); - } else { - this.id = ko.observable(data._id); - } - } -} +import * as ko from "knockout"; +import DocumentId from "./DocumentId"; +import DocumentsTab from "../Tabs/DocumentsTab"; + +export default class ObjectId extends DocumentId { + constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { + super(container, data, partitionKeyValue); + if (typeof data._id === "object") { + this.id = ko.observable(data._id[Object.keys(data._id)[0]]); + } else { + this.id = ko.observable(data._id); + } + } +} diff --git a/src/Explorer/Tree/ResourceTokenCollection.ts b/src/Explorer/Tree/ResourceTokenCollection.ts index 35361d23d..f929b3e7d 100644 --- a/src/Explorer/Tree/ResourceTokenCollection.ts +++ b/src/Explorer/Tree/ResourceTokenCollection.ts @@ -53,7 +53,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); return Q.resolve(); @@ -71,7 +71,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); } @@ -85,7 +85,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: title + tabTitle: title, }); const queryTab: QueryTab = new QueryTab({ @@ -100,7 +100,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas partitionKey: collection.partitionKey, resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(), onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(queryTab); @@ -115,7 +115,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas databaseName: this.databaseId, collectionName: this.id(), defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree + dataExplorerArea: Constants.Areas.ResourceTree, }); const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( @@ -135,7 +135,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: "Items" + tabTitle: "Items", }); documentsTab = new DocumentsTab({ @@ -150,7 +150,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas tabPath: `${this.databaseId}>${this.id()}>Documents`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/documents`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons + onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(documentsTab); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.ts b/src/Explorer/Tree/ResourceTreeAdapter.test.ts index e4b474372..5102149c1 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.ts +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.ts @@ -10,15 +10,15 @@ describe("ResourceTreeAdapter", () => { selectedNode: ko.observable({ nodeKind: "nodeKind", rid: "rid", - id: ko.observable("id") + id: ko.observable("id"), }), tabsManager: { activeTab: ko.observable({ - tabKind: ViewModels.CollectionTabKind.Documents - } as TabsBase) + tabKind: ViewModels.CollectionTabKind.Documents, + } as TabsBase), }, isNotebookEnabled: ko.observable(true), - nonSystemDatabases: ko.observable([]) + nonSystemDatabases: ko.observable([]), } as unknown) as Explorer); // TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths @@ -52,11 +52,11 @@ describe("ResourceTreeAdapter", () => { nodeKind: "Database", rid: "dbrid", id: ko.observable("dbid"), - selectedSubnodeKind: ko.observable(subNodeKind) + selectedSubnodeKind: ko.observable(subNodeKind), } as unknown) as ViewModels.TreeNode); const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", undefined, [ - ViewModels.CollectionTabKind.Documents + ViewModels.CollectionTabKind.Documents, ]); expect(isDataNodeSelected).toBeTruthy(); }); @@ -65,14 +65,14 @@ describe("ResourceTreeAdapter", () => { let subNodeKind = ViewModels.CollectionTabKind.Documents; const explorer = mockContainer(); explorer.tabsManager.activeTab({ - tabKind: subNodeKind + tabKind: subNodeKind, } as TabsBase); explorer.selectedNode(({ nodeKind: "Collection", rid: "collrid", databaseId: "dbid", id: ko.observable("collid"), - selectedSubnodeKind: ko.observable(subNodeKind) + selectedSubnodeKind: ko.observable(subNodeKind), } as unknown) as ViewModels.TreeNode); const resourceTreeAdapter = new ResourceTreeAdapter(explorer); let isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); @@ -80,14 +80,14 @@ describe("ResourceTreeAdapter", () => { subNodeKind = ViewModels.CollectionTabKind.Graph; explorer.tabsManager.activeTab({ - tabKind: subNodeKind + tabKind: subNodeKind, } as TabsBase); explorer.selectedNode(({ nodeKind: "Collection", rid: "collrid", databaseId: "dbid", id: ko.observable("collid"), - selectedSubnodeKind: ko.observable(subNodeKind) + selectedSubnodeKind: ko.observable(subNodeKind), } as unknown) as ViewModels.TreeNode); isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); expect(isDataNodeSelected).toBeTruthy(); @@ -100,14 +100,14 @@ describe("ResourceTreeAdapter", () => { rid: "collrid", databaseId: "dbid", id: ko.observable("collid"), - selectedSubnodeKind: ko.observable(ViewModels.CollectionTabKind.Documents) + selectedSubnodeKind: ko.observable(ViewModels.CollectionTabKind.Documents), } as unknown) as ViewModels.TreeNode); explorer.tabsManager.activeTab({ - tabKind: ViewModels.CollectionTabKind.Documents + tabKind: ViewModels.CollectionTabKind.Documents, } as TabsBase); const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [ - ViewModels.CollectionTabKind.Settings + ViewModels.CollectionTabKind.Settings, ]); expect(isDataNodeSelected).toBeFalsy(); }); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx index 076c7a0b0..a161eaf1d 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx @@ -16,196 +16,196 @@ const schema: DataModels.ISchema = { { dataType: { code: 15, - name: "String" + name: "String", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "_rid", path: "_rid", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 11, - name: "Int64" + name: "Int64", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "_ts", path: "_ts", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 15, - name: "String" + name: "String", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "id", path: "id", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 15, - name: "String" + name: "String", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "pk", path: "pk", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 15, - name: "String" + name: "String", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "other", path: "other", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 15, - name: "String" + name: "String", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "name", path: "nested.name", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 11, - name: "Int64" + name: "Int64", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "someNumber", path: "nested.someNumber", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 17, - name: "Double" + name: "Double", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "anotherNumber", path: "nested.anotherNumber", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 + maxDefinitionLevel: 1, }, { dataType: { code: 15, - name: "String" + name: "String", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "name", path: "items.list.items.name", maxRepetitionLevel: 1, - maxDefinitionLevel: 3 + maxDefinitionLevel: 3, }, { dataType: { code: 11, - name: "Int64" + name: "Int64", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "someNumber", path: "items.list.items.someNumber", maxRepetitionLevel: 1, - maxDefinitionLevel: 3 + maxDefinitionLevel: 3, }, { dataType: { code: 17, - name: "Double" + name: "Double", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "anotherNumber", path: "items.list.items.anotherNumber", maxRepetitionLevel: 1, - maxDefinitionLevel: 3 + maxDefinitionLevel: 3, }, { dataType: { code: 15, - name: "String" + name: "String", }, hasNulls: true, isArray: false, schemaType: { code: 0, - name: "Data" + name: "Data", }, name: "_etag", path: "_etag", maxRepetitionLevel: 0, - maxDefinitionLevel: 1 - } - ] + maxDefinitionLevel: 1, + }, + ], }; const createMockContainer = (): Explorer => { @@ -243,7 +243,7 @@ describe("Resource tree for schema", () => { const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection()); const props: TreeComponentProps = { rootNode, - className: "dataResourceTree" + className: "dataResourceTree", }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index db00e691c..6ccbe5a2f 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -57,7 +57,7 @@ export class ResourceTreeAdapter implements ReactAdapter { this.container.selectedNode.subscribe((newValue: any) => this.triggerRender()); this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender()); - this.container.isNotebookEnabled.subscribe(newValue => this.triggerRender()); + this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender()); this.koSubsDatabaseIdMap = new ArrayHashMap(); this.koSubsCollectionIdMap = new ArrayHashMap(); @@ -80,7 +80,7 @@ export class ResourceTreeAdapter implements ReactAdapter { if (myNotebooksTree.children) { // Count 1st generation children (tree is lazy-loaded) const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; - myNotebooksTree.children.forEach(treeNode => { + myNotebooksTree.children.forEach((treeNode) => { switch ((treeNode as NotebookContentItem).type) { case NotebookContentItemType.File: nodeCounts.files++; @@ -129,13 +129,13 @@ export class ResourceTreeAdapter implements ReactAdapter { this.galleryContentRoot = { name: "Gallery", path: "Gallery", - type: NotebookContentItemType.File + type: NotebookContentItemType.File, }; this.myNotebooksContentRoot = { name: ResourceTreeAdapter.MyNotebooksTitle, path: this.container.getNotebookBasePath(), - type: NotebookContentItemType.Directory + type: NotebookContentItemType.Directory, }; // Only if notebook server is available we can refresh @@ -152,7 +152,7 @@ export class ResourceTreeAdapter implements ReactAdapter { this.gitHubNotebooksContentRoot = { name: ResourceTreeAdapter.GitHubReposTitle, path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory + type: NotebookContentItemType.Directory, }; } else { this.gitHubNotebooksContentRoot = undefined; @@ -164,20 +164,20 @@ export class ResourceTreeAdapter implements ReactAdapter { public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { if (this.gitHubNotebooksContentRoot) { this.gitHubNotebooksContentRoot.children = []; - pinnedRepos?.forEach(pinnedRepo => { + pinnedRepos?.forEach((pinnedRepo) => { const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); const repoTreeItem: NotebookContentItem = { name: repoFullName, path: ResourceTreeAdapter.PseudoDirPath, type: NotebookContentItemType.Directory, - children: [] + children: [], }; - pinnedRepo.branches.forEach(branch => { + pinnedRepo.branches.forEach((branch) => { repoTreeItem.children.push({ name: branch.name, path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), - type: NotebookContentItemType.Directory + type: NotebookContentItemType.Directory, }); }); @@ -198,7 +198,7 @@ export class ResourceTreeAdapter implements ReactAdapter { children: [], isSelected: () => this.isDataNodeSelected(database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container), - onClick: async isExpanded => { + onClick: async (isExpanded) => { // Rewritten version of expandCollapseDatabase(): if (isExpanded) { database.collapseDatabase(); @@ -213,7 +213,7 @@ export class ResourceTreeAdapter implements ReactAdapter { this.container.onUpdateTabsButtons([]); this.container.tabsManager.refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); }, - onContextMenuOpen: () => this.container.selectedNode(database) + onContextMenuOpen: () => this.container.selectedNode(database), }; if (database.isDatabaseShared()) { @@ -221,7 +221,7 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Scale", isSelected: () => this.isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettings]), - onClick: database.onSettingsClick.bind(database) + onClick: database.onSettingsClick.bind(database), }); } @@ -244,7 +244,7 @@ export class ResourceTreeAdapter implements ReactAdapter { return { label: undefined, isExpanded: true, - children: databaseTreeNodes + children: databaseTreeNodes, }; } @@ -269,16 +269,16 @@ export class ResourceTreeAdapter implements ReactAdapter { description: "Data", data: { databaseId: collection.databaseId, - collectionId: collection.id() - } + collectionId: collection.id(), + }, }); }, isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id(), [ ViewModels.CollectionTabKind.Documents, - ViewModels.CollectionTabKind.Graph + ViewModels.CollectionTabKind.Graph, ]), - contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection) + contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), }); if (!this.container.isPreferredApiCassandra() || !this.container.isServerlessEnabled()) { @@ -286,7 +286,7 @@ export class ResourceTreeAdapter implements ReactAdapter { label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", onClick: collection.onSettingsClick.bind(collection), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]) + this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]), }); } @@ -315,7 +315,7 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Conflicts", onClick: collection.onConflictsClick.bind(collection), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]) + this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]), }); } @@ -343,7 +343,7 @@ export class ResourceTreeAdapter implements ReactAdapter { } }, isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id()), - onContextMenuOpen: () => this.container.selectedNode(collection) + onContextMenuOpen: () => this.container.selectedNode(collection), }; } @@ -355,9 +355,9 @@ export class ResourceTreeAdapter implements ReactAdapter { onClick: sp.open.bind(sp), isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.StoredProcedures + ViewModels.CollectionTabKind.StoredProcedures, ]), - contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp) + contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp), })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); @@ -365,7 +365,7 @@ export class ResourceTreeAdapter implements ReactAdapter { (tab: TabsBase) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId ); - } + }, }; } @@ -377,9 +377,12 @@ export class ResourceTreeAdapter implements ReactAdapter { onClick: udf.open.bind(udf), isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.UserDefinedFunctions + ViewModels.CollectionTabKind.UserDefinedFunctions, ]), - contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container, udf) + contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems( + this.container, + udf + ), })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); @@ -387,7 +390,7 @@ export class ResourceTreeAdapter implements ReactAdapter { (tab: TabsBase) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId ); - } + }, }; } @@ -399,7 +402,7 @@ export class ResourceTreeAdapter implements ReactAdapter { onClick: trigger.open.bind(trigger), isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]), - contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger) + contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger), })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers); @@ -407,7 +410,7 @@ export class ResourceTreeAdapter implements ReactAdapter { (tab: TabsBase) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId ); - } + }, }; } @@ -428,7 +431,7 @@ export class ResourceTreeAdapter implements ReactAdapter { this.container.tabsManager.refreshActiveTab( (tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid ); - } + }, }; } @@ -484,7 +487,7 @@ export class ResourceTreeAdapter implements ReactAdapter { let notebooksTree: TreeNode = { label: undefined, isExpanded: true, - children: [] + children: [], }; if (this.galleryContentRoot) { @@ -497,7 +500,7 @@ export class ResourceTreeAdapter implements ReactAdapter { if (this.gitHubNotebooksContentRoot) { // collapse all other notebook nodes - notebooksTree.children.forEach(node => (node.isExpanded = false)); + notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.push(this.buildGitHubNotebooksTree()); } @@ -523,7 +526,7 @@ export class ResourceTreeAdapter implements ReactAdapter { LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); this.triggerRender(); }, - setInitialFocus: true + setInitialFocus: true, }; const openGalleryProps: ILinkProps = { @@ -531,7 +534,7 @@ export class ResourceTreeAdapter implements ReactAdapter { LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); this.container.openGallery(); this.triggerRender(); - } + }, }; return ( @@ -559,7 +562,7 @@ export class ResourceTreeAdapter implements ReactAdapter { isSelected: () => { const activeTab = this.container.tabsManager.activeTab(); return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; - } + }, }; } @@ -567,7 +570,7 @@ export class ResourceTreeAdapter implements ReactAdapter { const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( this.myNotebooksContentRoot, (item: NotebookContentItem) => { - this.container.openNotebook(item).then(hasOpened => { + this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { this.pushItemToMostRecent(item); } @@ -580,7 +583,7 @@ export class ResourceTreeAdapter implements ReactAdapter { myNotebooksTree.isExpanded = true; myNotebooksTree.isAlphaSorted = true; // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter(menuItem => menuItem.label !== "Delete"); + myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); return myNotebooksTree; } @@ -588,7 +591,7 @@ export class ResourceTreeAdapter implements ReactAdapter { const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( this.gitHubNotebooksContentRoot, (item: NotebookContentItem) => { - this.container.openNotebook(item).then(hasOpened => { + this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { this.pushItemToMostRecent(item); } @@ -601,7 +604,7 @@ export class ResourceTreeAdapter implements ReactAdapter { gitHubNotebooksTree.contextMenu = [ { label: "Manage GitHub settings", - onClick: () => this.container.gitHubReposPane.open() + onClick: () => this.container.gitHubReposPane.open(), }, { label: "Disconnect from GitHub", @@ -609,11 +612,11 @@ export class ResourceTreeAdapter implements ReactAdapter { TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience && this.container.defaultExperience(), - dataExplorerArea: Areas.Notebook + dataExplorerArea: Areas.Notebook, }); this.container.notebookManager?.gitHubOAuthService.logout(); - } - } + }, + }, ]; gitHubNotebooksTree.isExpanded = true; @@ -629,8 +632,8 @@ export class ResourceTreeAdapter implements ReactAdapter { description: "Notebook", data: { name: item.name, - path: item.path - } + path: item.path, + }, }); } @@ -643,7 +646,7 @@ export class ResourceTreeAdapter implements ReactAdapter { if (!item || !item.children) { return []; } else { - return item.children.map(item => { + return item.children.map((item) => { const result = item.type === NotebookContentItemType.Directory ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) @@ -676,7 +679,7 @@ export class ResourceTreeAdapter implements ReactAdapter { ); }, contextMenu: createFileContextMenu && this.createFileContextMenu(item), - data: item + data: item, }; } @@ -685,7 +688,7 @@ export class ResourceTreeAdapter implements ReactAdapter { { label: "Rename", iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item) + onClick: () => this.container.renameNotebook(item), }, { label: "Delete", @@ -699,23 +702,23 @@ export class ResourceTreeAdapter implements ReactAdapter { "Cancel", undefined ); - } + }, }, { label: "Copy to ...", iconSrc: CopyIcon, - onClick: () => this.copyNotebook(item) + onClick: () => this.copyNotebook(item), }, { label: "Download", iconSrc: NotebookIcon, - onClick: () => this.container.downloadFile(item) - } + onClick: () => this.container.downloadFile(item), + }, ]; // "Copy to ..." isn't needed if github locations are not available if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - items = items.filter(item => item.label !== "Copy to ..."); + items = items.filter((item) => item.label !== "Copy to ..."); } return items; @@ -733,7 +736,7 @@ export class ResourceTreeAdapter implements ReactAdapter { { label: "Refresh", iconSrc: RefreshIcon, - onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()) + onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()), }, { label: "Delete", @@ -747,34 +750,34 @@ export class ResourceTreeAdapter implements ReactAdapter { "Cancel", undefined ); - } + }, }, { label: "Rename", iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()) + onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()), }, { label: "New Directory", iconSrc: NewNotebookIcon, - onClick: () => this.container.onCreateDirectory(item) + onClick: () => this.container.onCreateDirectory(item), }, { label: "New Notebook", iconSrc: NewNotebookIcon, - onClick: () => this.container.onNewNotebookClicked(item) + onClick: () => this.container.onNewNotebookClicked(item), }, { label: "Upload File", iconSrc: NewNotebookIcon, - onClick: () => this.container.onUploadToNotebookServerClicked(item) - } + onClick: () => this.container.onUploadToNotebookServerClicked(item), + }, ]; // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" if (GitHubUtils.fromContentUri(item.path)) { items = items.filter( - item => + (item) => item.label !== "Delete" && item.label !== "Rename" && item.label !== "New Directory" && @@ -818,7 +821,7 @@ export class ResourceTreeAdapter implements ReactAdapter { ? this.createDirectoryContextMenu(item) : undefined, data: item, - children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) + children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), }; } diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx index 0f37de89a..c5dbe9986 100644 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx @@ -41,7 +41,7 @@ describe("Resource tree for resource token", () => { const rootNode: TreeNode = resourceTree.buildCollectionNode(); const props: TreeComponentProps = { rootNode, - className: "dataResourceTree" + className: "dataResourceTree", }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx index 7fb82c745..b7bc32c08 100644 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx @@ -34,7 +34,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { return { label: undefined, isExpanded: true, - children: [] + children: [], }; } @@ -50,12 +50,12 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { description: "Data", data: { databaseId: collection.databaseId, - collectionId: collection.id() - } + collectionId: collection.id(), + }, }); }, isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents) + this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents), }); const collectionNode: TreeNode = { @@ -69,16 +69,16 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter { this.container.selectedNode(collection); this.container.onUpdateTabsButtons([]); this.container.tabsManager.refreshActiveTab( - tab => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + (tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId ); }, - isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id()) + isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id()), }; return { label: undefined, isExpanded: true, - children: [collectionNode] + children: [collectionNode], }; } diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index 8135b6f0d..3bcd46796 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -1,175 +1,175 @@ -import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; -import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import Explorer from "../Explorer"; -import StoredProcedureTab from "../Tabs/StoredProcedureTab"; -import TabsBase from "../Tabs/TabsBase"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; - -const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE -function sample(prefix) { - var collection = getContext().getCollection(); - - // Query documents and take 1st item. - var isAccepted = collection.queryDocuments( - collection.getSelfLink(), - 'SELECT * FROM root r', - function (err, feed, options) { - if (err) throw err; - - // Check the feed and if empty, set the body to 'no docs found',  - // else take 1st element from feed - if (!feed || !feed.length) { - var response = getContext().getResponse(); - response.setBody('no docs found'); - } - else { - var response = getContext().getResponse(); - var body = { prefix: prefix, feed: feed[0] }; - response.setBody(JSON.stringify(body)); - } - }); - - if (!isAccepted) throw new Error('The query was not accepted by the server.'); -}`; - -export default class StoredProcedure { - public nodeKind: string; - public container: Explorer; - public collection: ViewModels.Collection; - public self: string; - public rid: string; - public id: ko.Observable; - public body: ko.Observable; - public isExecuteEnabled: boolean; - - constructor(container: Explorer, collection: ViewModels.Collection, data: StoredProcedureDefinition & Resource) { - this.nodeKind = "StoredProcedure"; - this.container = container; - this.collection = collection; - this.self = data._self; - this.rid = data._rid; - this.id = ko.observable(data.id); - this.body = ko.observable(data.body as string); - this.isExecuteEnabled = this.container.isFeatureEnabled(Constants.Features.executeSproc); - } - - public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1; - const storedProcedure = { - id: "", - body: sampleStoredProcedureBody - }; - - const storedProcedureTab: StoredProcedureTab = new StoredProcedureTab({ - resource: storedProcedure, - isNew: true, - tabKind: ViewModels.CollectionTabKind.StoredProcedures, - title: `New Stored Procedure ${id}`, - tabPath: `${source.databaseId}>${source.id()}>New Stored Procedure ${id}`, - collection: source, - node: source, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/sproc`, - isActive: ko.observable(false), - onUpdateTabsButtons: source.container.onUpdateTabsButtons - }); - - source.container.tabsManager.activateNewTab(storedProcedureTab); - } - - public select() { - this.container.selectedNode(this); - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Stored procedure node", - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - public open = () => { - this.select(); - - const storedProcedureTabs: StoredProcedureTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.StoredProcedures, - (tab: TabsBase) => tab.node && tab.node.rid === this.rid - ) as StoredProcedureTab[]; - let storedProcedureTab: StoredProcedureTab = storedProcedureTabs && storedProcedureTabs[0]; - - if (storedProcedureTab) { - this.container.tabsManager.activateTab(storedProcedureTab); - } else { - const storedProcedureData = { - _rid: this.rid, - _self: this.self, - id: this.id(), - body: this.body() - }; - - storedProcedureTab = new StoredProcedureTab({ - resource: storedProcedureData, - isNew: false, - tabKind: ViewModels.CollectionTabKind.StoredProcedures, - title: storedProcedureData.id, - tabPath: `${this.collection.databaseId}>${this.collection.id()}>${storedProcedureData.id}`, - collection: this.collection, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( - this.collection.databaseId, - this.collection.id() - )}/sprocs/${this.id()}`, - isActive: ko.observable(false), - onUpdateTabsButtons: this.container.onUpdateTabsButtons - }); - - this.container.tabsManager.activateNewTab(storedProcedureTab); - } - }; - - public delete() { - if (!window.confirm("Are you sure you want to delete the stored procedure?")) { - return; - } - - deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( - () => { - this.container.tabsManager.removeTabByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); - }, - reason => {} - ); - } - - public execute(params: string[], partitionKeyValue?: string): void { - const sprocTabs = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.StoredProcedures, - (tab: TabsBase) => tab.node && tab.node.rid === this.rid - ) as StoredProcedureTab[]; - const sprocTab = sprocTabs && sprocTabs.length > 0 && sprocTabs[0]; - sprocTab.isExecuting(true); - this.container && - executeStoredProcedure(this.collection, this, partitionKeyValue, params) - .then( - (result: any) => { - sprocTab.onExecuteSprocsResult(result, result.scriptLogs); - }, - (error: any) => { - sprocTab.onExecuteSprocsError(getErrorMessage(error)); - } - ) - .finally(() => { - sprocTab.isExecuting(false); - this.onFocusAfterExecute(); - }); - } - - public onFocusAfterExecute(): void { - const focusElement = document.getElementById("execute-storedproc-toggles"); - focusElement && focusElement.focus(); - } -} +import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; +import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import Explorer from "../Explorer"; +import StoredProcedureTab from "../Tabs/StoredProcedureTab"; +import TabsBase from "../Tabs/TabsBase"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; + +const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE +function sample(prefix) { + var collection = getContext().getCollection(); + + // Query documents and take 1st item. + var isAccepted = collection.queryDocuments( + collection.getSelfLink(), + 'SELECT * FROM root r', + function (err, feed, options) { + if (err) throw err; + + // Check the feed and if empty, set the body to 'no docs found',  + // else take 1st element from feed + if (!feed || !feed.length) { + var response = getContext().getResponse(); + response.setBody('no docs found'); + } + else { + var response = getContext().getResponse(); + var body = { prefix: prefix, feed: feed[0] }; + response.setBody(JSON.stringify(body)); + } + }); + + if (!isAccepted) throw new Error('The query was not accepted by the server.'); +}`; + +export default class StoredProcedure { + public nodeKind: string; + public container: Explorer; + public collection: ViewModels.Collection; + public self: string; + public rid: string; + public id: ko.Observable; + public body: ko.Observable; + public isExecuteEnabled: boolean; + + constructor(container: Explorer, collection: ViewModels.Collection, data: StoredProcedureDefinition & Resource) { + this.nodeKind = "StoredProcedure"; + this.container = container; + this.collection = collection; + this.self = data._self; + this.rid = data._rid; + this.id = ko.observable(data.id); + this.body = ko.observable(data.body as string); + this.isExecuteEnabled = this.container.isFeatureEnabled(Constants.Features.executeSproc); + } + + public static create(source: ViewModels.Collection, event: MouseEvent) { + const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1; + const storedProcedure = { + id: "", + body: sampleStoredProcedureBody, + }; + + const storedProcedureTab: StoredProcedureTab = new StoredProcedureTab({ + resource: storedProcedure, + isNew: true, + tabKind: ViewModels.CollectionTabKind.StoredProcedures, + title: `New Stored Procedure ${id}`, + tabPath: `${source.databaseId}>${source.id()}>New Stored Procedure ${id}`, + collection: source, + node: source, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/sproc`, + isActive: ko.observable(false), + onUpdateTabsButtons: source.container.onUpdateTabsButtons, + }); + + source.container.tabsManager.activateNewTab(storedProcedureTab); + } + + public select() { + this.container.selectedNode(this); + TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { + description: "Stored procedure node", + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + } + + public open = () => { + this.select(); + + const storedProcedureTabs: StoredProcedureTab[] = this.container.tabsManager.getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + (tab: TabsBase) => tab.node && tab.node.rid === this.rid + ) as StoredProcedureTab[]; + let storedProcedureTab: StoredProcedureTab = storedProcedureTabs && storedProcedureTabs[0]; + + if (storedProcedureTab) { + this.container.tabsManager.activateTab(storedProcedureTab); + } else { + const storedProcedureData = { + _rid: this.rid, + _self: this.self, + id: this.id(), + body: this.body(), + }; + + storedProcedureTab = new StoredProcedureTab({ + resource: storedProcedureData, + isNew: false, + tabKind: ViewModels.CollectionTabKind.StoredProcedures, + title: storedProcedureData.id, + tabPath: `${this.collection.databaseId}>${this.collection.id()}>${storedProcedureData.id}`, + collection: this.collection, + node: this, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( + this.collection.databaseId, + this.collection.id() + )}/sprocs/${this.id()}`, + isActive: ko.observable(false), + onUpdateTabsButtons: this.container.onUpdateTabsButtons, + }); + + this.container.tabsManager.activateNewTab(storedProcedureTab); + } + }; + + public delete() { + if (!window.confirm("Are you sure you want to delete the stored procedure?")) { + return; + } + + deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + this.container.tabsManager.removeTabByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); + } + + public execute(params: string[], partitionKeyValue?: string): void { + const sprocTabs = this.container.tabsManager.getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + (tab: TabsBase) => tab.node && tab.node.rid === this.rid + ) as StoredProcedureTab[]; + const sprocTab = sprocTabs && sprocTabs.length > 0 && sprocTabs[0]; + sprocTab.isExecuting(true); + this.container && + executeStoredProcedure(this.collection, this, partitionKeyValue, params) + .then( + (result: any) => { + sprocTab.onExecuteSprocsResult(result, result.scriptLogs); + }, + (error: any) => { + sprocTab.onExecuteSprocsError(getErrorMessage(error)); + } + ) + .finally(() => { + sprocTab.isExecuting(false); + this.onFocusAfterExecute(); + }); + } + + public onFocusAfterExecute(): void { + const focusElement = document.getElementById("execute-storedproc-toggles"); + focusElement && focusElement.focus(); + } +} diff --git a/src/Explorer/Tree/Trigger.ts b/src/Explorer/Tree/Trigger.ts index 4462e0fb7..d6ee69210 100644 --- a/src/Explorer/Tree/Trigger.ts +++ b/src/Explorer/Tree/Trigger.ts @@ -1,123 +1,123 @@ -import { StoredProcedureDefinition } from "@azure/cosmos"; -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import { deleteTrigger } from "../../Common/dataAccess/deleteTrigger"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import Explorer from "../Explorer"; -import TriggerTab from "../Tabs/TriggerTab"; - -export default class Trigger { - public nodeKind: string; - public container: Explorer; - public collection: ViewModels.Collection; - public self: string; - public rid: string; - public id: ko.Observable; - public body: ko.Observable; - public triggerType: ko.Observable; - public triggerOperation: ko.Observable; - - constructor(container: Explorer, collection: ViewModels.Collection, data: any) { - this.nodeKind = "Trigger"; - this.container = container; - this.collection = collection; - this.self = data._self; - this.rid = data._rid; - this.id = ko.observable(data.id); - this.body = ko.observable(data.body); - this.triggerOperation = ko.observable(data.triggerOperation); - this.triggerType = ko.observable(data.triggerType); - } - - public select() { - this.container.selectedNode(this); - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Trigger node", - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; - const trigger = { - id: "", - body: "function trigger(){}", - triggerOperation: "All", - triggerType: "Pre" - }; - - const triggerTab: TriggerTab = new TriggerTab({ - resource: trigger, - isNew: true, - tabKind: ViewModels.CollectionTabKind.Triggers, - title: `New Trigger ${id}`, - tabPath: "", - collection: source, - node: source, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`, - isActive: ko.observable(false), - onUpdateTabsButtons: source.container.onUpdateTabsButtons - }); - - source.container.tabsManager.activateNewTab(triggerTab); - } - - public open = () => { - this.select(); - - const triggerTabs: TriggerTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Triggers, - tab => tab.node && tab.node.rid === this.rid - ) as TriggerTab[]; - let triggerTab: TriggerTab = triggerTabs && triggerTabs[0]; - - if (triggerTab) { - this.container.tabsManager.activateTab(triggerTab); - } else { - const triggerData = { - _rid: this.rid, - _self: this.self, - id: this.id(), - body: this.body(), - triggerOperation: this.triggerOperation(), - triggerType: this.triggerType() - }; - - triggerTab = new TriggerTab({ - resource: triggerData, - isNew: false, - tabKind: ViewModels.CollectionTabKind.Triggers, - title: triggerData.id, - tabPath: "", - collection: this.collection, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( - this.collection.databaseId, - this.collection.id() - )}/triggers/${this.id()}`, - isActive: ko.observable(false), - onUpdateTabsButtons: this.container.onUpdateTabsButtons - }); - - this.container.tabsManager.activateNewTab(triggerTab); - } - }; - - public delete() { - if (!window.confirm("Are you sure you want to delete the trigger?")) { - return; - } - - deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( - () => { - this.container.tabsManager.removeTabByComparator(tab => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); - }, - reason => {} - ); - } -} +import { StoredProcedureDefinition } from "@azure/cosmos"; +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import { deleteTrigger } from "../../Common/dataAccess/deleteTrigger"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import Explorer from "../Explorer"; +import TriggerTab from "../Tabs/TriggerTab"; + +export default class Trigger { + public nodeKind: string; + public container: Explorer; + public collection: ViewModels.Collection; + public self: string; + public rid: string; + public id: ko.Observable; + public body: ko.Observable; + public triggerType: ko.Observable; + public triggerOperation: ko.Observable; + + constructor(container: Explorer, collection: ViewModels.Collection, data: any) { + this.nodeKind = "Trigger"; + this.container = container; + this.collection = collection; + this.self = data._self; + this.rid = data._rid; + this.id = ko.observable(data.id); + this.body = ko.observable(data.body); + this.triggerOperation = ko.observable(data.triggerOperation); + this.triggerType = ko.observable(data.triggerType); + } + + public select() { + this.container.selectedNode(this); + TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { + description: "Trigger node", + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + } + + public static create(source: ViewModels.Collection, event: MouseEvent) { + const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; + const trigger = { + id: "", + body: "function trigger(){}", + triggerOperation: "All", + triggerType: "Pre", + }; + + const triggerTab: TriggerTab = new TriggerTab({ + resource: trigger, + isNew: true, + tabKind: ViewModels.CollectionTabKind.Triggers, + title: `New Trigger ${id}`, + tabPath: "", + collection: source, + node: source, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`, + isActive: ko.observable(false), + onUpdateTabsButtons: source.container.onUpdateTabsButtons, + }); + + source.container.tabsManager.activateNewTab(triggerTab); + } + + public open = () => { + this.select(); + + const triggerTabs: TriggerTab[] = this.container.tabsManager.getTabs( + ViewModels.CollectionTabKind.Triggers, + (tab) => tab.node && tab.node.rid === this.rid + ) as TriggerTab[]; + let triggerTab: TriggerTab = triggerTabs && triggerTabs[0]; + + if (triggerTab) { + this.container.tabsManager.activateTab(triggerTab); + } else { + const triggerData = { + _rid: this.rid, + _self: this.self, + id: this.id(), + body: this.body(), + triggerOperation: this.triggerOperation(), + triggerType: this.triggerType(), + }; + + triggerTab = new TriggerTab({ + resource: triggerData, + isNew: false, + tabKind: ViewModels.CollectionTabKind.Triggers, + title: triggerData.id, + tabPath: "", + collection: this.collection, + node: this, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( + this.collection.databaseId, + this.collection.id() + )}/triggers/${this.id()}`, + isActive: ko.observable(false), + onUpdateTabsButtons: this.container.onUpdateTabsButtons, + }); + + this.container.tabsManager.activateNewTab(triggerTab); + } + }; + + public delete() { + if (!window.confirm("Are you sure you want to delete the trigger?")) { + return; + } + + deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + this.container.tabsManager.removeTabByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); + } +} diff --git a/src/Explorer/Tree/UserDefinedFunction.ts b/src/Explorer/Tree/UserDefinedFunction.ts index 7a326cc3c..b3aad4a54 100644 --- a/src/Explorer/Tree/UserDefinedFunction.ts +++ b/src/Explorer/Tree/UserDefinedFunction.ts @@ -1,116 +1,116 @@ -import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import { deleteUserDefinedFunction } from "../../Common/dataAccess/deleteUserDefinedFunction"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import Explorer from "../Explorer"; -import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; - -export default class UserDefinedFunction { - public nodeKind: string; - public container: Explorer; - public collection: ViewModels.Collection; - public self: string; - public rid: string; - public id: ko.Observable; - public body: ko.Observable; - - constructor(container: Explorer, collection: ViewModels.Collection, data: UserDefinedFunctionDefinition & Resource) { - this.nodeKind = "UserDefinedFunction"; - this.container = container; - - this.collection = collection; - this.self = data._self; - this.rid = data._rid; - this.id = ko.observable(data.id); - this.body = ko.observable(data.body as string); - } - - public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1; - const userDefinedFunction = { - id: "", - body: "function userDefinedFunction(){}" - }; - - const userDefinedFunctionTab: UserDefinedFunctionTab = new UserDefinedFunctionTab({ - resource: userDefinedFunction, - isNew: true, - tabKind: ViewModels.CollectionTabKind.UserDefinedFunctions, - title: `New UDF ${id}`, - tabPath: "", - collection: source, - node: source, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`, - isActive: ko.observable(false), - onUpdateTabsButtons: source.container.onUpdateTabsButtons - }); - - source.container.tabsManager.activateNewTab(userDefinedFunctionTab); - } - - public open = () => { - this.select(); - - const userDefinedFunctionTabs: UserDefinedFunctionTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.UserDefinedFunctions, - tab => tab.node?.rid === this.rid - ) as UserDefinedFunctionTab[]; - let userDefinedFunctionTab: UserDefinedFunctionTab = userDefinedFunctionTabs && userDefinedFunctionTabs[0]; - - if (userDefinedFunctionTab) { - this.container.tabsManager.activateTab(userDefinedFunctionTab); - } else { - const userDefinedFunctionData = { - _rid: this.rid, - _self: this.self, - id: this.id(), - body: this.body() - }; - - userDefinedFunctionTab = new UserDefinedFunctionTab({ - resource: userDefinedFunctionData, - isNew: false, - tabKind: ViewModels.CollectionTabKind.UserDefinedFunctions, - title: userDefinedFunctionData.id, - tabPath: "", - collection: this.collection, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( - this.collection.databaseId, - this.collection.id() - )}/udfs/${this.id()}`, - isActive: ko.observable(false), - onUpdateTabsButtons: this.container.onUpdateTabsButtons - }); - - this.container.tabsManager.activateNewTab(userDefinedFunctionTab); - } - }; - - public select() { - this.container.selectedNode(this); - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "UDF item node", - databaseAccountName: this.container.databaseAccount().name, - defaultExperience: this.container.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - public delete() { - if (!window.confirm("Are you sure you want to delete the user defined function?")) { - return; - } - - deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( - () => { - this.container.tabsManager.removeTabByComparator(tab => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); - }, - reason => {} - ); - } -} +import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; +import * as ko from "knockout"; +import * as Constants from "../../Common/Constants"; +import { deleteUserDefinedFunction } from "../../Common/dataAccess/deleteUserDefinedFunction"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import Explorer from "../Explorer"; +import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; + +export default class UserDefinedFunction { + public nodeKind: string; + public container: Explorer; + public collection: ViewModels.Collection; + public self: string; + public rid: string; + public id: ko.Observable; + public body: ko.Observable; + + constructor(container: Explorer, collection: ViewModels.Collection, data: UserDefinedFunctionDefinition & Resource) { + this.nodeKind = "UserDefinedFunction"; + this.container = container; + + this.collection = collection; + this.self = data._self; + this.rid = data._rid; + this.id = ko.observable(data.id); + this.body = ko.observable(data.body as string); + } + + public static create(source: ViewModels.Collection, event: MouseEvent) { + const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1; + const userDefinedFunction = { + id: "", + body: "function userDefinedFunction(){}", + }; + + const userDefinedFunctionTab: UserDefinedFunctionTab = new UserDefinedFunctionTab({ + resource: userDefinedFunction, + isNew: true, + tabKind: ViewModels.CollectionTabKind.UserDefinedFunctions, + title: `New UDF ${id}`, + tabPath: "", + collection: source, + node: source, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`, + isActive: ko.observable(false), + onUpdateTabsButtons: source.container.onUpdateTabsButtons, + }); + + source.container.tabsManager.activateNewTab(userDefinedFunctionTab); + } + + public open = () => { + this.select(); + + const userDefinedFunctionTabs: UserDefinedFunctionTab[] = this.container.tabsManager.getTabs( + ViewModels.CollectionTabKind.UserDefinedFunctions, + (tab) => tab.node?.rid === this.rid + ) as UserDefinedFunctionTab[]; + let userDefinedFunctionTab: UserDefinedFunctionTab = userDefinedFunctionTabs && userDefinedFunctionTabs[0]; + + if (userDefinedFunctionTab) { + this.container.tabsManager.activateTab(userDefinedFunctionTab); + } else { + const userDefinedFunctionData = { + _rid: this.rid, + _self: this.self, + id: this.id(), + body: this.body(), + }; + + userDefinedFunctionTab = new UserDefinedFunctionTab({ + resource: userDefinedFunctionData, + isNew: false, + tabKind: ViewModels.CollectionTabKind.UserDefinedFunctions, + title: userDefinedFunctionData.id, + tabPath: "", + collection: this.collection, + node: this, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( + this.collection.databaseId, + this.collection.id() + )}/udfs/${this.id()}`, + isActive: ko.observable(false), + onUpdateTabsButtons: this.container.onUpdateTabsButtons, + }); + + this.container.tabsManager.activateNewTab(userDefinedFunctionTab); + } + }; + + public select() { + this.container.selectedNode(this); + TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { + description: "UDF item node", + databaseAccountName: this.container.databaseAccount().name, + defaultExperience: this.container.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + }); + } + + public delete() { + if (!window.confirm("Are you sure you want to delete the user defined function?")) { + return; + } + + deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + this.container.tabsManager.removeTabByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); + } +} diff --git a/src/Explorer/WaitsForTemplateViewModel.ts b/src/Explorer/WaitsForTemplateViewModel.ts index 9361456d9..fa6977e9d 100644 --- a/src/Explorer/WaitsForTemplateViewModel.ts +++ b/src/Explorer/WaitsForTemplateViewModel.ts @@ -1,37 +1,37 @@ -import * as ko from "knockout"; -import * as ViewModels from "../Contracts/ViewModels"; -import * as Constants from "../Common/Constants"; - -export abstract class WaitsForTemplateViewModel implements ViewModels.WaitsForTemplate { - public isTemplateReady: ko.Observable; - - constructor() { - this.isTemplateReady = ko.observable(false).extend({ rateLimit: 100 }); - } - - protected onTemplateReady(callback: (isTemplateReady: boolean) => void) { - this.isTemplateReady.subscribe((value: boolean) => { - callback(value); - }); - - document.addEventListener("keydown", function(e: KeyboardEvent) { - // To trap keyboard focus in AddCollection pane - let firstFocusableElement = document.getElementById("closeBtnAddCollection"); - let lastFocusableElement = document.getElementById("submitBtnAddCollection"); - var isTabPressed = e.keyCode === Constants.KeyCodes.Tab; - if (isTabPressed) { - if (e.shiftKey) { - /* shift + tab */ if (document.activeElement === firstFocusableElement) { - lastFocusableElement && lastFocusableElement.focus(); - e.preventDefault(); - } - } /* tab */ else { - if (document.activeElement === lastFocusableElement) { - firstFocusableElement && firstFocusableElement.focus(); - e.preventDefault(); - } - } - } - }); - } -} +import * as ko from "knockout"; +import * as ViewModels from "../Contracts/ViewModels"; +import * as Constants from "../Common/Constants"; + +export abstract class WaitsForTemplateViewModel implements ViewModels.WaitsForTemplate { + public isTemplateReady: ko.Observable; + + constructor() { + this.isTemplateReady = ko.observable(false).extend({ rateLimit: 100 }); + } + + protected onTemplateReady(callback: (isTemplateReady: boolean) => void) { + this.isTemplateReady.subscribe((value: boolean) => { + callback(value); + }); + + document.addEventListener("keydown", function (e: KeyboardEvent) { + // To trap keyboard focus in AddCollection pane + let firstFocusableElement = document.getElementById("closeBtnAddCollection"); + let lastFocusableElement = document.getElementById("submitBtnAddCollection"); + var isTabPressed = e.keyCode === Constants.KeyCodes.Tab; + if (isTabPressed) { + if (e.shiftKey) { + /* shift + tab */ if (document.activeElement === firstFocusableElement) { + lastFocusableElement && lastFocusableElement.focus(); + e.preventDefault(); + } + } /* tab */ else { + if (document.activeElement === lastFocusableElement) { + firstFocusableElement && firstFocusableElement.focus(); + e.preventDefault(); + } + } + } + }); + } +} diff --git a/src/GalleryViewer/GalleryViewer.tsx b/src/GalleryViewer/GalleryViewer.tsx index 3810b5522..f8993ff30 100644 --- a/src/GalleryViewer/GalleryViewer.tsx +++ b/src/GalleryViewer/GalleryViewer.tsx @@ -7,7 +7,7 @@ import { initializeConfiguration } from "../ConfigContext"; import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent"; import { GalleryAndNotebookViewerComponent, - GalleryAndNotebookViewerComponentProps + GalleryAndNotebookViewerComponentProps, } from "../Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponent"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import { JunoClient } from "../Juno/JunoClient"; @@ -27,7 +27,7 @@ const onInit = async () => { junoClient: new JunoClient(), selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples, sortBy: galleryViewerProps.sortBy || SortBy.MostViewed, - searchText: galleryViewerProps.searchText + searchText: galleryViewerProps.searchText, }; const element = ( diff --git a/src/GalleryViewer/galleryViewer.html b/src/GalleryViewer/galleryViewer.html index 6d01a848b..be4c94cc2 100644 --- a/src/GalleryViewer/galleryViewer.html +++ b/src/GalleryViewer/galleryViewer.html @@ -7,7 +7,7 @@ - +
diff --git a/src/GitHub/GitHubClient.ts b/src/GitHub/GitHubClient.ts index 6125836cf..016bca4b6 100644 --- a/src/GitHub/GitHubClient.ts +++ b/src/GitHub/GitHubClient.ts @@ -237,18 +237,18 @@ export class GitHubClient { try { const response = (await this.ocktokit.graphql(repositoryQuery, { owner, - repo + repo, } as RepositoryQueryParams)) as RepositoryQueryResponse; return { status: HttpStatusCodes.OK, - data: GitHubClient.toGitHubRepo(response.repository) + data: GitHubClient.toGitHubRepo(response.repository), }; } catch (error) { Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed"); return { status: GitHubClient.SelfErrorCode, - data: undefined + data: undefined, }; } } @@ -257,19 +257,19 @@ export class GitHubClient { try { const response = (await this.ocktokit.graphql(repositoriesQuery, { pageSize, - endCursor + endCursor, } as RepositoriesQueryParams)) as RepositoriesQueryResponse; return { status: HttpStatusCodes.OK, - data: response.viewer.repositories.nodes.map(repo => GitHubClient.toGitHubRepo(repo)), - pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo) + data: response.viewer.repositories.nodes.map((repo) => GitHubClient.toGitHubRepo(repo)), + pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo), }; } catch (error) { Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed"); return { status: GitHubClient.SelfErrorCode, - data: undefined + data: undefined, }; } } @@ -286,19 +286,19 @@ export class GitHubClient { repo, refPrefix: "refs/heads/", pageSize, - endCursor + endCursor, } as BranchesQueryParams)) as BranchesQueryResponse; return { status: HttpStatusCodes.OK, - data: response.repository.refs.nodes.map(ref => GitHubClient.toGitHubBranch(ref)), - pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo) + data: response.repository.refs.nodes.map((ref) => GitHubClient.toGitHubBranch(ref)), + pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo), }; } catch (error) { Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getBranchesAsync failed"); return { status: GitHubClient.SelfErrorCode, - data: undefined + data: undefined, }; } } @@ -315,13 +315,13 @@ export class GitHubClient { repo, ref: `refs/heads/${branch}`, path: path || undefined, - objectExpression: `refs/heads/${branch}:${path || ""}` + objectExpression: `refs/heads/${branch}:${path || ""}`, } as ContentsQueryParams)) as ContentsQueryResponse; if (!response.repository.object) { return { status: HttpStatusCodes.NotFound, - data: undefined + data: undefined, }; } @@ -332,7 +332,7 @@ export class GitHubClient { const gitHubCommit = GitHubClient.toGitHubCommit(response.repository.ref.target.history.nodes[0]); if (Array.isArray(entries)) { - data = entries.map(entry => + data = entries.map((entry) => GitHubClient.toGitHubFile( entry, (path && UrlUtility.createUri(path, entry.name)) || entry.name, @@ -346,7 +346,7 @@ export class GitHubClient { { name: NotebookUtil.getName(path), type: "blob", - object: response.repository.object + object: response.repository.object, }, path, gitHubRepo, @@ -357,13 +357,13 @@ export class GitHubClient { return { status: HttpStatusCodes.OK, - data + data, }; } catch (error) { Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getContentsAsync failed"); return { status: GitHubClient.SelfErrorCode, - data: undefined + data: undefined, }; } } @@ -384,7 +384,7 @@ export class GitHubClient { path, message, content, - sha + sha, }); let data: IGitHubCommit; @@ -409,8 +409,8 @@ export class GitHubClient { repo, ref, headers: { - "If-None-Match": "" // disable 60s cache - } + "If-None-Match": "", // disable 60s cache + }, }); const currentTree = await this.ocktokit.git.getTree({ @@ -419,13 +419,13 @@ export class GitHubClient { tree_sha: currentRef.data.object.sha, recursive: "1", headers: { - "If-None-Match": "" // disable 60s cache - } + "If-None-Match": "", // disable 60s cache + }, }); // API infers tree from paths so we need to filter them out - const currentTreeItems = currentTree.data.tree.filter(item => item.type !== "tree"); - currentTreeItems.forEach(item => { + const currentTreeItems = currentTree.data.tree.filter((item) => item.type !== "tree"); + currentTreeItems.forEach((item) => { if (item.path === newPath) { throw new Error("File with the path already exists"); } @@ -434,12 +434,12 @@ export class GitHubClient { const updatedTree = await this.ocktokit.git.createTree({ owner, repo, - tree: currentTreeItems.map(item => ({ + tree: currentTreeItems.map((item) => ({ path: item.path === oldPath ? newPath : item.path, mode: item.mode as "100644" | "100755" | "040000" | "160000" | "120000", type: item.type as "blob" | "tree" | "commit", - sha: item.sha - })) + sha: item.sha, + })), }); const newCommit = await this.ocktokit.git.createCommit({ @@ -447,19 +447,19 @@ export class GitHubClient { repo, message, parents: [currentRef.data.object.sha], - tree: updatedTree.data.sha + tree: updatedTree.data.sha, }); const updatedRef = await this.ocktokit.git.updateRef({ owner, repo, ref, - sha: newCommit.data.sha + sha: newCommit.data.sha, }); return { status: updatedRef.status, - data: GitHubClient.toGitHubCommit(newCommit.data) + data: GitHubClient.toGitHubCommit(newCommit.data), }; } @@ -470,7 +470,7 @@ export class GitHubClient { path: file.path, message, sha: file.sha, - branch: file.branch.name + branch: file.branch.name, }); let data: IGitHubCommit; @@ -487,11 +487,11 @@ export class GitHubClient { repo, file_sha: sha, mediaType: { - format: "raw" + format: "raw", }, headers: { - "If-None-Match": "" // disable 60s cache - } + "If-None-Match": "", // disable 60s cache + }, }); return { status: response.status, data: (response.data) }; @@ -504,11 +504,11 @@ export class GitHubClient { debug: () => {}, info: (message?: any) => GitHubClient.log(Logger.logInfo, message), warn: (message?: any) => GitHubClient.log(Logger.logWarning, message), - error: (error?: any) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit") - } + error: (error?: any) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit"), + }, }); - this.ocktokit.hook.error("request", error => { + this.ocktokit.hook.error("request", (error) => { this.errorCallback(error); throw error; }); @@ -525,13 +525,13 @@ export class GitHubClient { return { owner: object.owner.login, name: object.name, - private: object.isPrivate + private: object.isPrivate, }; } private static toGitHubBranch(object: Ref): IGitHubBranch { return { - name: object.name + name: object.name, }; } @@ -546,14 +546,14 @@ export class GitHubClient { return { sha: object.sha || object.oid, message: object.message, - commitDate: object.committer.date + commitDate: object.committer.date, }; } private static toGitHubPageInfo(object: PageInfo): IGitHubPageInfo { return { endCursor: object.endCursor, - hasNextPage: object.hasNextPage + hasNextPage: object.hasNextPage, }; } @@ -576,7 +576,7 @@ export class GitHubClient { branch, commit, size: entry.object?.byteSize, - sha: entry.object?.oid + sha: entry.object?.oid, }; } } diff --git a/src/GitHub/GitHubConnector.ts b/src/GitHub/GitHubConnector.ts index 0700b7757..866f86f51 100644 --- a/src/GitHub/GitHubConnector.ts +++ b/src/GitHub/GitHubConnector.ts @@ -1,30 +1,30 @@ -export interface IGitHubConnectorParams { - state: string; - code: string; -} - -export const GitHubConnectorMsgType = "GitHubConnectorMsgType"; - -export class GitHubConnector { - public start(params: URLSearchParams, window: Window & typeof globalThis) { - window.postMessage( - { - type: GitHubConnectorMsgType, - data: { - state: params.get("state"), - code: params.get("code") - } as IGitHubConnectorParams - }, - window.location.origin - ); - } -} - -var connector = new GitHubConnector(); -window.addEventListener("load", () => { - const openerWindow = window.opener; - if (openerWindow) { - connector.start(new URLSearchParams(document.location.search), openerWindow); - window.close(); - } -}); +export interface IGitHubConnectorParams { + state: string; + code: string; +} + +export const GitHubConnectorMsgType = "GitHubConnectorMsgType"; + +export class GitHubConnector { + public start(params: URLSearchParams, window: Window & typeof globalThis) { + window.postMessage( + { + type: GitHubConnectorMsgType, + data: { + state: params.get("state"), + code: params.get("code"), + } as IGitHubConnectorParams, + }, + window.location.origin + ); + } +} + +var connector = new GitHubConnector(); +window.addEventListener("load", () => { + const openerWindow = window.opener; + if (openerWindow) { + connector.start(new URLSearchParams(document.location.search), openerWindow); + window.close(); + } +}); diff --git a/src/GitHub/GitHubContentProvider.test.ts b/src/GitHub/GitHubContentProvider.test.ts index 3fcc5d2f1..c7311bbf6 100644 --- a/src/GitHub/GitHubContentProvider.test.ts +++ b/src/GitHub/GitHubContentProvider.test.ts @@ -1,302 +1,302 @@ -import { IContent } from "@nteract/core"; -import { fixture } from "@nteract/fixtures"; -import { HttpStatusCodes } from "../Common/Constants"; -import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient"; -import { GitHubContentProvider } from "./GitHubContentProvider"; -import * as GitHubUtils from "../Utils/GitHubUtils"; - -const gitHubClient = new GitHubClient(() => {}); -const gitHubContentProvider = new GitHubContentProvider({ - gitHubClient, - promptForCommitMsg: () => Promise.resolve("commit msg") -}); -const gitHubCommit: IGitHubCommit = { - sha: "sha", - message: "message", - commitDate: "date" -}; -const sampleFile: IGitHubFile = { - type: "blob", - size: 0, - name: "name.ipynb", - path: "dir/name.ipynb", - content: fixture, - sha: "sha", - repo: { - owner: "owner", - name: "repo", - private: false - }, - branch: { - name: "branch" - }, - commit: gitHubCommit -}; -const sampleGitHubUri = GitHubUtils.toContentUri( - sampleFile.repo.owner, - sampleFile.repo.name, - sampleFile.branch.name, - sampleFile.path -); -const sampleNotebookModel: IContent<"notebook"> = { - name: sampleFile.name, - path: sampleGitHubUri, - type: "notebook", - writable: true, - created: "", - last_modified: "date", - mimetype: "application/x-ipynb+json", - content: sampleFile.content ? JSON.parse(sampleFile.content) : null, - format: "json" -}; - -describe("GitHubContentProvider remove", () => { - it("errors on invalid path", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.remove(null, "invalid path").toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toBeCalled(); - }); - - it("errors on failed read", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toBeCalled(); - }); - - it("errors on failed delete", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) - ); - spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toBeCalled(); - expect(gitHubClient.deleteFileAsync).toBeCalled(); - }); - - it("removes notebook", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) - ); - spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) - ); - - const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.NoContent); - expect(gitHubClient.deleteFileAsync).toBeCalled(); - expect(response.response).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider get", () => { - it("errors on invalid path", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toBeCalled(); - }); - - it("errors on failed read", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.get(null, sampleGitHubUri, null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toBeCalled(); - }); - - it("reads notebook", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) - ); - - const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(gitHubClient.getContentsAsync).toBeCalled(); - expect(response.response).toEqual(sampleNotebookModel); - }); -}); - -describe("GitHubContentProvider update", () => { - it("errors on invalid path", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toBeCalled(); - }); - - it("errors on failed read", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.update(null, sampleGitHubUri, null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toBeCalled(); - }); - - it("errors on failed rename", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) - ); - spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toBeCalled(); - expect(gitHubClient.renameFileAsync).toBeCalled(); - }); - - it("updates notebook", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) - ); - spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) - ); - - const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(gitHubClient.getContentsAsync).toBeCalled(); - expect(gitHubClient.renameFileAsync).toBeCalled(); - expect(response.response.type).toEqual(sampleNotebookModel.type); - expect(response.response.name).toEqual(sampleNotebookModel.name); - expect(response.response.path).toEqual(sampleNotebookModel.path); - expect(response.response.content).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider create", () => { - it("errors on invalid path", async () => { - spyOn(GitHubClient.prototype, "createOrUpdateFileAsync"); - - const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled(); - }); - - it("errors on failed create", async () => { - spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); - }); - - it("creates notebook", async () => { - spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit }) - ); - - const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.Created); - expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); - expect(response.response.type).toEqual(sampleNotebookModel.type); - expect(response.response.name).toBeDefined(); - expect(response.response.path).toBeDefined(); - expect(response.response.content).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider save", () => { - it("errors on invalid path", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toBeCalled(); - }); - - it("errors on failed read", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.save(null, sampleGitHubUri, null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toBeCalled(); - }); - - it("errors on failed update", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) - ); - spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 })); - - const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toBeCalled(); - expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); - }); - - it("saves notebook", async () => { - spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) - ); - spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue( - Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) - ); - - const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(gitHubClient.getContentsAsync).toBeCalled(); - expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); - expect(response.response.type).toEqual(sampleNotebookModel.type); - expect(response.response.name).toEqual(sampleNotebookModel.name); - expect(response.response.path).toEqual(sampleNotebookModel.path); - expect(response.response.content).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider listCheckpoints", () => { - it("errors for everything", async () => { - const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); - -describe("GitHubContentProvider createCheckpoint", () => { - it("errors for everything", async () => { - const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); - -describe("GitHubContentProvider deleteCheckpoint", () => { - it("errors for everything", async () => { - const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); - -describe("GitHubContentProvider restoreFromCheckpoint", () => { - it("errors for everything", async () => { - const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); +import { IContent } from "@nteract/core"; +import { fixture } from "@nteract/fixtures"; +import { HttpStatusCodes } from "../Common/Constants"; +import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient"; +import { GitHubContentProvider } from "./GitHubContentProvider"; +import * as GitHubUtils from "../Utils/GitHubUtils"; + +const gitHubClient = new GitHubClient(() => {}); +const gitHubContentProvider = new GitHubContentProvider({ + gitHubClient, + promptForCommitMsg: () => Promise.resolve("commit msg"), +}); +const gitHubCommit: IGitHubCommit = { + sha: "sha", + message: "message", + commitDate: "date", +}; +const sampleFile: IGitHubFile = { + type: "blob", + size: 0, + name: "name.ipynb", + path: "dir/name.ipynb", + content: fixture, + sha: "sha", + repo: { + owner: "owner", + name: "repo", + private: false, + }, + branch: { + name: "branch", + }, + commit: gitHubCommit, +}; +const sampleGitHubUri = GitHubUtils.toContentUri( + sampleFile.repo.owner, + sampleFile.repo.name, + sampleFile.branch.name, + sampleFile.path +); +const sampleNotebookModel: IContent<"notebook"> = { + name: sampleFile.name, + path: sampleGitHubUri, + type: "notebook", + writable: true, + created: "", + last_modified: "date", + mimetype: "application/x-ipynb+json", + content: sampleFile.content ? JSON.parse(sampleFile.content) : null, + format: "json", +}; + +describe("GitHubContentProvider remove", () => { + it("errors on invalid path", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync"); + + const response = await gitHubContentProvider.remove(null, "invalid path").toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + expect(gitHubClient.getContentsAsync).not.toBeCalled(); + }); + + it("errors on failed read", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.getContentsAsync).toBeCalled(); + }); + + it("errors on failed delete", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) + ); + spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.getContentsAsync).toBeCalled(); + expect(gitHubClient.deleteFileAsync).toBeCalled(); + }); + + it("removes notebook", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) + ); + spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) + ); + + const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(HttpStatusCodes.NoContent); + expect(gitHubClient.deleteFileAsync).toBeCalled(); + expect(response.response).toBeUndefined(); + }); +}); + +describe("GitHubContentProvider get", () => { + it("errors on invalid path", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync"); + + const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + expect(gitHubClient.getContentsAsync).not.toBeCalled(); + }); + + it("errors on failed read", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.get(null, sampleGitHubUri, null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.getContentsAsync).toBeCalled(); + }); + + it("reads notebook", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) + ); + + const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(gitHubClient.getContentsAsync).toBeCalled(); + expect(response.response).toEqual(sampleNotebookModel); + }); +}); + +describe("GitHubContentProvider update", () => { + it("errors on invalid path", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync"); + + const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + expect(gitHubClient.getContentsAsync).not.toBeCalled(); + }); + + it("errors on failed read", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.update(null, sampleGitHubUri, null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.getContentsAsync).toBeCalled(); + }); + + it("errors on failed rename", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) + ); + spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.getContentsAsync).toBeCalled(); + expect(gitHubClient.renameFileAsync).toBeCalled(); + }); + + it("updates notebook", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) + ); + spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) + ); + + const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(gitHubClient.getContentsAsync).toBeCalled(); + expect(gitHubClient.renameFileAsync).toBeCalled(); + expect(response.response.type).toEqual(sampleNotebookModel.type); + expect(response.response.name).toEqual(sampleNotebookModel.name); + expect(response.response.path).toEqual(sampleNotebookModel.path); + expect(response.response.content).toBeUndefined(); + }); +}); + +describe("GitHubContentProvider create", () => { + it("errors on invalid path", async () => { + spyOn(GitHubClient.prototype, "createOrUpdateFileAsync"); + + const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled(); + }); + + it("errors on failed create", async () => { + spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); + }); + + it("creates notebook", async () => { + spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit }) + ); + + const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(HttpStatusCodes.Created); + expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); + expect(response.response.type).toEqual(sampleNotebookModel.type); + expect(response.response.name).toBeDefined(); + expect(response.response.path).toBeDefined(); + expect(response.response.content).toBeUndefined(); + }); +}); + +describe("GitHubContentProvider save", () => { + it("errors on invalid path", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync"); + + const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + expect(gitHubClient.getContentsAsync).not.toBeCalled(); + }); + + it("errors on failed read", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.save(null, sampleGitHubUri, null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.getContentsAsync).toBeCalled(); + }); + + it("errors on failed update", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) + ); + spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 })); + + const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(888); + expect(gitHubClient.getContentsAsync).toBeCalled(); + expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); + }); + + it("saves notebook", async () => { + spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) + ); + spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue( + Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) + ); + + const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(gitHubClient.getContentsAsync).toBeCalled(); + expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); + expect(response.response.type).toEqual(sampleNotebookModel.type); + expect(response.response.name).toEqual(sampleNotebookModel.name); + expect(response.response.path).toEqual(sampleNotebookModel.path); + expect(response.response.content).toBeUndefined(); + }); +}); + +describe("GitHubContentProvider listCheckpoints", () => { + it("errors for everything", async () => { + const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + }); +}); + +describe("GitHubContentProvider createCheckpoint", () => { + it("errors for everything", async () => { + const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + }); +}); + +describe("GitHubContentProvider deleteCheckpoint", () => { + it("errors for everything", async () => { + const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + }); +}); + +describe("GitHubContentProvider restoreFromCheckpoint", () => { + it("errors for everything", async () => { + const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise(); + expect(response).toBeDefined(); + expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); + }); +}); diff --git a/src/GitHub/GitHubContentProvider.ts b/src/GitHub/GitHubContentProvider.ts index c406be440..d8f45aa97 100644 --- a/src/GitHub/GitHubContentProvider.ts +++ b/src/GitHub/GitHubContentProvider.ts @@ -1,431 +1,431 @@ -import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable"; -import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core"; -import { from, Observable, of } from "rxjs"; -import { AjaxResponse } from "rxjs/ajax"; -import * as Base64Utils from "../Utils/Base64Utils"; -import { HttpStatusCodes } from "../Common/Constants"; -import * as Logger from "../Common/Logger"; -import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; -import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient"; -import * as GitHubUtils from "../Utils/GitHubUtils"; -import UrlUtility from "../Common/UrlUtility"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; - -export interface GitHubContentProviderParams { - gitHubClient: GitHubClient; - promptForCommitMsg: (title: string, primaryButtonLabel: string) => Promise; -} - -class GitHubContentProviderError extends Error { - constructor(error: string, public errno: number = GitHubContentProvider.SelfErrorCode) { - super(error); - } -} - -// Provides 'contents' API for GitHub -// http://jupyter-api.surge.sh/#!/contents -export class GitHubContentProvider implements IContentProvider { - public static readonly SelfErrorCode = 555; - - constructor(private params: GitHubContentProviderParams) {} - - public remove(_: ServerConfig, uri: string): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - const commitMsg = await this.validateContentAndGetCommitMsg(content, "Delete", "Delete"); - const response = await this.params.gitHubClient.deleteFileAsync(content.data as IGitHubFile, commitMsg); - if (response.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to delete", response.status); - } - - return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/remove", error.errno); - return this.createErrorAjaxResponse(error); - } - }) - ); - } - - public get(_: ServerConfig, uri: string, params: Partial): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - if (content.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to get content", content.status); - } - - if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) { - const file = content.data; - file.content = ( - await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha) - ).data; - } - - return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params)); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/get", error.errno); - return this.createErrorAjaxResponse(error); - } - }) - ); - } - - public update( - _: ServerConfig, - uri: string, - model: Partial> - ): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - const gitHubFile = content.data as IGitHubFile; - const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename"); - const newUri = model.path; - const newPath = GitHubUtils.fromContentUri(newUri).path; - const response = await this.params.gitHubClient.renameFileAsync( - gitHubFile.repo.owner, - gitHubFile.repo.name, - gitHubFile.branch.name, - commitMsg, - gitHubFile.path, - newPath - ); - if (response.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to rename", response.status); - } - - gitHubFile.commit = response.data; - gitHubFile.path = newPath; - gitHubFile.name = NotebookUtil.getName(gitHubFile.path); - - return this.createSuccessAjaxResponse( - HttpStatusCodes.OK, - this.createContentModel(newUri, gitHubFile, { content: 0 }) - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); - return this.createErrorAjaxResponse(error); - } - }) - ); - } - - public create( - _: ServerConfig, - uri: string, - model: Partial> & { type: FT } - ): Observable { - return from( - this.params.promptForCommitMsg("Create New Notebook", "Create").then(async (commitMsg: string) => { - try { - if (!commitMsg) { - throw new GitHubContentProviderError("Couldn't get a commit message"); - } - - if (model.type !== "notebook") { - throw new GitHubContentProviderError("Unsupported content type"); - } - - const contentInfo = GitHubUtils.fromContentUri(uri); - if (!contentInfo) { - throw new GitHubContentProviderError(`Failed to parse ${uri}`); - } - - const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord()))); - const options: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - hour12: false - }; - const name = `Untitled-${new Date().toLocaleString("default", options)}.ipynb`; - let path = name; - if (contentInfo.path) { - path = UrlUtility.createUri(contentInfo.path, name); - } - - const response = await this.params.gitHubClient.createOrUpdateFileAsync( - contentInfo.owner, - contentInfo.repo, - contentInfo.branch, - path, - commitMsg, - content - ); - if (response.status !== HttpStatusCodes.Created) { - throw new GitHubContentProviderError("Failed to create", response.status); - } - - const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path); - const newGitHubFile: IGitHubFile = { - type: "blob", - name: NotebookUtil.getName(newUri), - path, - repo: { - owner: contentInfo.owner, - name: contentInfo.repo, - private: undefined - }, - branch: { - name: contentInfo.branch - }, - commit: response.data - }; - - return this.createSuccessAjaxResponse( - HttpStatusCodes.Created, - this.createContentModel(newUri, newGitHubFile, { content: 0 }) - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/create", error.errno); - return this.createErrorAjaxResponse(error); - } - }) - ); - } - - public save( - _: ServerConfig, - uri: string, - model: Partial> - ): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - let commitMsg: string; - if (content.status === HttpStatusCodes.NotFound) { - // We'll create a new file since it doesn't exist - commitMsg = await this.params.promptForCommitMsg("Save", "Save"); - if (!commitMsg) { - throw new GitHubContentProviderError("Couldn't get a commit message"); - } - } else { - commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save"); - } - - let updatedContent: string; - if (model.type === "notebook") { - updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook)); - } else if (model.type === "file") { - updatedContent = model.content as string; - if (model.format !== "base64") { - updatedContent = Base64Utils.utf8ToB64(updatedContent); - } - } else { - throw new GitHubContentProviderError("Unsupported content type"); - } - - const contentInfo = GitHubUtils.fromContentUri(uri); - let gitHubFile: IGitHubFile; - if (content.data) { - gitHubFile = content.data as IGitHubFile; - } - - const response = await this.params.gitHubClient.createOrUpdateFileAsync( - contentInfo.owner, - contentInfo.repo, - contentInfo.branch, - contentInfo.path, - commitMsg, - updatedContent, - gitHubFile?.sha - ); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) { - throw new GitHubContentProviderError("Failed to create or update", response.status); - } - - if (gitHubFile) { - gitHubFile.commit = response.data; - } else { - const contentResponse = await this.params.gitHubClient.getContentsAsync( - contentInfo.owner, - contentInfo.repo, - contentInfo.branch, - contentInfo.path - ); - if (contentResponse.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to get content", response.status); - } - - gitHubFile = contentResponse.data as IGitHubFile; - } - - return this.createSuccessAjaxResponse( - HttpStatusCodes.OK, - this.createContentModel(uri, gitHubFile, { content: 0 }) - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); - return this.createErrorAjaxResponse(error); - } - }) - ); - } - - public listCheckpoints(_: ServerConfig, path: string): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - public createCheckpoint(_: ServerConfig, path: string): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - private async validateContentAndGetCommitMsg( - content: IGitHubResponse, - promptTitle: string, - promptPrimaryButtonLabel: string - ): Promise { - if (content.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to get content", content.status); - } - - if (Array.isArray(content.data)) { - throw new GitHubContentProviderError("Operation not supported for collections"); - } - - const commitMsg = await this.params.promptForCommitMsg(promptTitle, promptPrimaryButtonLabel); - if (!commitMsg) { - throw new GitHubContentProviderError("Couldn't get a commit message"); - } - - return commitMsg; - } - - private async getContent(uri: string): Promise> { - const contentInfo = GitHubUtils.fromContentUri(uri); - if (contentInfo) { - const { owner, repo, branch, path } = contentInfo; - return this.params.gitHubClient.getContentsAsync(owner, repo, branch, path); - } - - return Promise.resolve({ status: GitHubContentProvider.SelfErrorCode, data: undefined }); - } - - private createContentModel( - uri: string, - content: IGitHubFile | IGitHubFile[], - params: Partial - ): IContent { - if (Array.isArray(content)) { - return this.createDirectoryModel(uri, content); - } - - if (content.type === "tree") { - return this.createDirectoryModel(uri, undefined); - } - - if (NotebookUtil.isNotebookFile(uri)) { - return this.createNotebookModel(content, params); - } - - return this.createFileModel(content, params); - } - - private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> { - return { - name: NotebookUtil.getName(uri), - path: uri, - type: "directory", - writable: true, // TODO: tamitta: we don't know this info here - created: "", // TODO: tamitta: we don't know this info here - last_modified: "", // TODO: tamitta: we don't know this info here - mimetype: undefined, - content: gitHubFiles?.map( - (file: IGitHubFile) => - this.createContentModel( - GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path), - file, - { - content: 0 - } - ) as IEmptyContent - ), - format: "json" - }; - } - - private createNotebookModel(gitHubFile: IGitHubFile, params: Partial): IContent<"notebook"> { - const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined; - return { - name: gitHubFile.name, - path: GitHubUtils.toContentUri( - gitHubFile.repo.owner, - gitHubFile.repo.name, - gitHubFile.branch.name, - gitHubFile.path - ), - type: "notebook", - writable: true, // TODO: tamitta: we don't know this info here - created: "", // TODO: tamitta: we don't know this info here - last_modified: gitHubFile.commit.commitDate, - mimetype: content ? "application/x-ipynb+json" : undefined, - content, - format: content ? "json" : undefined - }; - } - - private createFileModel(gitHubFile: IGitHubFile, params: Partial): IContent<"file"> { - const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined; - return { - name: gitHubFile.name, - path: GitHubUtils.toContentUri( - gitHubFile.repo.owner, - gitHubFile.repo.name, - gitHubFile.branch.name, - gitHubFile.path - ), - type: "file", - writable: true, // TODO: tamitta: we don't know this info here - created: "", // TODO: tamitta: we don't know this info here - last_modified: gitHubFile.commit.commitDate, - mimetype: content ? "text/plain" : undefined, - content, - format: content ? "text" : undefined - }; - } - - private createSuccessAjaxResponse(status: number, content: IContent): AjaxResponse { - return { - originalEvent: new Event("no-op"), - xhr: new XMLHttpRequest(), - request: {}, - status, - response: content ? content : undefined, - responseText: content ? JSON.stringify(content) : undefined, - responseType: "json" - }; - } - - private createErrorAjaxResponse(error: GitHubContentProviderError): AjaxResponse { - return { - originalEvent: new Event("no-op"), - xhr: new XMLHttpRequest(), - request: {}, - status: error.errno, - response: error, - responseText: getErrorMessage(error), - responseType: "json" - }; - } -} +import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable"; +import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core"; +import { from, Observable, of } from "rxjs"; +import { AjaxResponse } from "rxjs/ajax"; +import * as Base64Utils from "../Utils/Base64Utils"; +import { HttpStatusCodes } from "../Common/Constants"; +import * as Logger from "../Common/Logger"; +import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; +import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient"; +import * as GitHubUtils from "../Utils/GitHubUtils"; +import UrlUtility from "../Common/UrlUtility"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; + +export interface GitHubContentProviderParams { + gitHubClient: GitHubClient; + promptForCommitMsg: (title: string, primaryButtonLabel: string) => Promise; +} + +class GitHubContentProviderError extends Error { + constructor(error: string, public errno: number = GitHubContentProvider.SelfErrorCode) { + super(error); + } +} + +// Provides 'contents' API for GitHub +// http://jupyter-api.surge.sh/#!/contents +export class GitHubContentProvider implements IContentProvider { + public static readonly SelfErrorCode = 555; + + constructor(private params: GitHubContentProviderParams) {} + + public remove(_: ServerConfig, uri: string): Observable { + return from( + this.getContent(uri).then(async (content: IGitHubResponse) => { + try { + const commitMsg = await this.validateContentAndGetCommitMsg(content, "Delete", "Delete"); + const response = await this.params.gitHubClient.deleteFileAsync(content.data as IGitHubFile, commitMsg); + if (response.status !== HttpStatusCodes.OK) { + throw new GitHubContentProviderError("Failed to delete", response.status); + } + + return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined); + } catch (error) { + Logger.logError(getErrorMessage(error), "GitHubContentProvider/remove", error.errno); + return this.createErrorAjaxResponse(error); + } + }) + ); + } + + public get(_: ServerConfig, uri: string, params: Partial): Observable { + return from( + this.getContent(uri).then(async (content: IGitHubResponse) => { + try { + if (content.status !== HttpStatusCodes.OK) { + throw new GitHubContentProviderError("Failed to get content", content.status); + } + + if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) { + const file = content.data; + file.content = ( + await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha) + ).data; + } + + return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params)); + } catch (error) { + Logger.logError(getErrorMessage(error), "GitHubContentProvider/get", error.errno); + return this.createErrorAjaxResponse(error); + } + }) + ); + } + + public update( + _: ServerConfig, + uri: string, + model: Partial> + ): Observable { + return from( + this.getContent(uri).then(async (content: IGitHubResponse) => { + try { + const gitHubFile = content.data as IGitHubFile; + const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename"); + const newUri = model.path; + const newPath = GitHubUtils.fromContentUri(newUri).path; + const response = await this.params.gitHubClient.renameFileAsync( + gitHubFile.repo.owner, + gitHubFile.repo.name, + gitHubFile.branch.name, + commitMsg, + gitHubFile.path, + newPath + ); + if (response.status !== HttpStatusCodes.OK) { + throw new GitHubContentProviderError("Failed to rename", response.status); + } + + gitHubFile.commit = response.data; + gitHubFile.path = newPath; + gitHubFile.name = NotebookUtil.getName(gitHubFile.path); + + return this.createSuccessAjaxResponse( + HttpStatusCodes.OK, + this.createContentModel(newUri, gitHubFile, { content: 0 }) + ); + } catch (error) { + Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); + return this.createErrorAjaxResponse(error); + } + }) + ); + } + + public create( + _: ServerConfig, + uri: string, + model: Partial> & { type: FT } + ): Observable { + return from( + this.params.promptForCommitMsg("Create New Notebook", "Create").then(async (commitMsg: string) => { + try { + if (!commitMsg) { + throw new GitHubContentProviderError("Couldn't get a commit message"); + } + + if (model.type !== "notebook") { + throw new GitHubContentProviderError("Unsupported content type"); + } + + const contentInfo = GitHubUtils.fromContentUri(uri); + if (!contentInfo) { + throw new GitHubContentProviderError(`Failed to parse ${uri}`); + } + + const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord()))); + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: false, + }; + const name = `Untitled-${new Date().toLocaleString("default", options)}.ipynb`; + let path = name; + if (contentInfo.path) { + path = UrlUtility.createUri(contentInfo.path, name); + } + + const response = await this.params.gitHubClient.createOrUpdateFileAsync( + contentInfo.owner, + contentInfo.repo, + contentInfo.branch, + path, + commitMsg, + content + ); + if (response.status !== HttpStatusCodes.Created) { + throw new GitHubContentProviderError("Failed to create", response.status); + } + + const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path); + const newGitHubFile: IGitHubFile = { + type: "blob", + name: NotebookUtil.getName(newUri), + path, + repo: { + owner: contentInfo.owner, + name: contentInfo.repo, + private: undefined, + }, + branch: { + name: contentInfo.branch, + }, + commit: response.data, + }; + + return this.createSuccessAjaxResponse( + HttpStatusCodes.Created, + this.createContentModel(newUri, newGitHubFile, { content: 0 }) + ); + } catch (error) { + Logger.logError(getErrorMessage(error), "GitHubContentProvider/create", error.errno); + return this.createErrorAjaxResponse(error); + } + }) + ); + } + + public save( + _: ServerConfig, + uri: string, + model: Partial> + ): Observable { + return from( + this.getContent(uri).then(async (content: IGitHubResponse) => { + try { + let commitMsg: string; + if (content.status === HttpStatusCodes.NotFound) { + // We'll create a new file since it doesn't exist + commitMsg = await this.params.promptForCommitMsg("Save", "Save"); + if (!commitMsg) { + throw new GitHubContentProviderError("Couldn't get a commit message"); + } + } else { + commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save"); + } + + let updatedContent: string; + if (model.type === "notebook") { + updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook)); + } else if (model.type === "file") { + updatedContent = model.content as string; + if (model.format !== "base64") { + updatedContent = Base64Utils.utf8ToB64(updatedContent); + } + } else { + throw new GitHubContentProviderError("Unsupported content type"); + } + + const contentInfo = GitHubUtils.fromContentUri(uri); + let gitHubFile: IGitHubFile; + if (content.data) { + gitHubFile = content.data as IGitHubFile; + } + + const response = await this.params.gitHubClient.createOrUpdateFileAsync( + contentInfo.owner, + contentInfo.repo, + contentInfo.branch, + contentInfo.path, + commitMsg, + updatedContent, + gitHubFile?.sha + ); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) { + throw new GitHubContentProviderError("Failed to create or update", response.status); + } + + if (gitHubFile) { + gitHubFile.commit = response.data; + } else { + const contentResponse = await this.params.gitHubClient.getContentsAsync( + contentInfo.owner, + contentInfo.repo, + contentInfo.branch, + contentInfo.path + ); + if (contentResponse.status !== HttpStatusCodes.OK) { + throw new GitHubContentProviderError("Failed to get content", response.status); + } + + gitHubFile = contentResponse.data as IGitHubFile; + } + + return this.createSuccessAjaxResponse( + HttpStatusCodes.OK, + this.createContentModel(uri, gitHubFile, { content: 0 }) + ); + } catch (error) { + Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); + return this.createErrorAjaxResponse(error); + } + }) + ); + } + + public listCheckpoints(_: ServerConfig, path: string): Observable { + const error = new GitHubContentProviderError("Not implemented"); + Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno); + return of(this.createErrorAjaxResponse(error)); + } + + public createCheckpoint(_: ServerConfig, path: string): Observable { + const error = new GitHubContentProviderError("Not implemented"); + Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno); + return of(this.createErrorAjaxResponse(error)); + } + + public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable { + const error = new GitHubContentProviderError("Not implemented"); + Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno); + return of(this.createErrorAjaxResponse(error)); + } + + public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable { + const error = new GitHubContentProviderError("Not implemented"); + Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno); + return of(this.createErrorAjaxResponse(error)); + } + + private async validateContentAndGetCommitMsg( + content: IGitHubResponse, + promptTitle: string, + promptPrimaryButtonLabel: string + ): Promise { + if (content.status !== HttpStatusCodes.OK) { + throw new GitHubContentProviderError("Failed to get content", content.status); + } + + if (Array.isArray(content.data)) { + throw new GitHubContentProviderError("Operation not supported for collections"); + } + + const commitMsg = await this.params.promptForCommitMsg(promptTitle, promptPrimaryButtonLabel); + if (!commitMsg) { + throw new GitHubContentProviderError("Couldn't get a commit message"); + } + + return commitMsg; + } + + private async getContent(uri: string): Promise> { + const contentInfo = GitHubUtils.fromContentUri(uri); + if (contentInfo) { + const { owner, repo, branch, path } = contentInfo; + return this.params.gitHubClient.getContentsAsync(owner, repo, branch, path); + } + + return Promise.resolve({ status: GitHubContentProvider.SelfErrorCode, data: undefined }); + } + + private createContentModel( + uri: string, + content: IGitHubFile | IGitHubFile[], + params: Partial + ): IContent { + if (Array.isArray(content)) { + return this.createDirectoryModel(uri, content); + } + + if (content.type === "tree") { + return this.createDirectoryModel(uri, undefined); + } + + if (NotebookUtil.isNotebookFile(uri)) { + return this.createNotebookModel(content, params); + } + + return this.createFileModel(content, params); + } + + private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> { + return { + name: NotebookUtil.getName(uri), + path: uri, + type: "directory", + writable: true, // TODO: tamitta: we don't know this info here + created: "", // TODO: tamitta: we don't know this info here + last_modified: "", // TODO: tamitta: we don't know this info here + mimetype: undefined, + content: gitHubFiles?.map( + (file: IGitHubFile) => + this.createContentModel( + GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path), + file, + { + content: 0, + } + ) as IEmptyContent + ), + format: "json", + }; + } + + private createNotebookModel(gitHubFile: IGitHubFile, params: Partial): IContent<"notebook"> { + const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined; + return { + name: gitHubFile.name, + path: GitHubUtils.toContentUri( + gitHubFile.repo.owner, + gitHubFile.repo.name, + gitHubFile.branch.name, + gitHubFile.path + ), + type: "notebook", + writable: true, // TODO: tamitta: we don't know this info here + created: "", // TODO: tamitta: we don't know this info here + last_modified: gitHubFile.commit.commitDate, + mimetype: content ? "application/x-ipynb+json" : undefined, + content, + format: content ? "json" : undefined, + }; + } + + private createFileModel(gitHubFile: IGitHubFile, params: Partial): IContent<"file"> { + const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined; + return { + name: gitHubFile.name, + path: GitHubUtils.toContentUri( + gitHubFile.repo.owner, + gitHubFile.repo.name, + gitHubFile.branch.name, + gitHubFile.path + ), + type: "file", + writable: true, // TODO: tamitta: we don't know this info here + created: "", // TODO: tamitta: we don't know this info here + last_modified: gitHubFile.commit.commitDate, + mimetype: content ? "text/plain" : undefined, + content, + format: content ? "text" : undefined, + }; + } + + private createSuccessAjaxResponse(status: number, content: IContent): AjaxResponse { + return { + originalEvent: new Event("no-op"), + xhr: new XMLHttpRequest(), + request: {}, + status, + response: content ? content : undefined, + responseText: content ? JSON.stringify(content) : undefined, + responseType: "json", + }; + } + + private createErrorAjaxResponse(error: GitHubContentProviderError): AjaxResponse { + return { + originalEvent: new Event("no-op"), + xhr: new XMLHttpRequest(), + request: {}, + status: error.errno, + response: error, + responseText: getErrorMessage(error), + responseType: "json", + }; + } +} diff --git a/src/GitHub/GitHubOAuthService.test.ts b/src/GitHub/GitHubOAuthService.test.ts index 0229465db..9a2b092af 100644 --- a/src/GitHub/GitHubOAuthService.test.ts +++ b/src/GitHub/GitHubOAuthService.test.ts @@ -1,166 +1,166 @@ -import ko from "knockout"; -import { HttpStatusCodes } from "../Common/Constants"; -import * as DataModels from "../Contracts/DataModels"; -import { JunoClient } from "../Juno/JunoClient"; -import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector"; -import { GitHubOAuthService } from "./GitHubOAuthService"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import NotebookManager from "../Explorer/Notebook/NotebookManager"; -import Explorer from "../Explorer/Explorer"; - -const sampleDatabaseAccount: DataModels.DatabaseAccount = { - id: "id", - name: "name", - location: "location", - type: "type", - kind: "kind", - tags: [], - properties: { - documentEndpoint: "documentEndpoint", - gremlinEndpoint: "gremlinEndpoint", - tableEndpoint: "tableEndpoint", - cassandraEndpoint: "cassandraEndpoint" - } -}; - -describe("GitHubOAuthService", () => { - let junoClient: JunoClient; - let gitHubOAuthService: GitHubOAuthService; - let originalDataExplorer: Explorer; - - beforeEach(() => { - junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); - gitHubOAuthService = new GitHubOAuthService(junoClient); - originalDataExplorer = window.dataExplorer; - window.dataExplorer = { - ...originalDataExplorer, - logConsoleData: (data): void => - data.type === ConsoleDataType.Error ? console.error(data.message) : console.error(data.message) - } as Explorer; - window.dataExplorer.notebookManager = new NotebookManager(); - window.dataExplorer.notebookManager.junoClient = junoClient; - window.dataExplorer.notebookManager.gitHubOAuthService = gitHubOAuthService; - }); - - afterEach(() => { - jest.resetAllMocks(); - window.dataExplorer = originalDataExplorer; - originalDataExplorer = undefined; - gitHubOAuthService = undefined; - junoClient = undefined; - }); - - it("logout deletes app authorization and resets token", async () => { - const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent }); - junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback; - - await gitHubOAuthService.logout(); - expect(deleteAppAuthorizationCallback).toBeCalled(); - expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined(); - }); - - it("resetToken resets token", () => { - gitHubOAuthService.resetToken(); - expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined(); - }); - - it("startOAuth resets OAuth state", async () => { - let url: string; - const windowOpenCallback = jest.fn().mockImplementation((value: string) => { - url = value; - }); - window.open = windowOpenCallback; - - await gitHubOAuthService.startOAuth("scope"); - expect(windowOpenCallback).toBeCalled(); - - const initialParams = new URLSearchParams(new URL(url).search); - expect(initialParams.get("state")).toBeDefined(); - - await gitHubOAuthService.startOAuth("another scope"); - expect(windowOpenCallback).toBeCalled(); - - const newParams = new URLSearchParams(new URL(url).search); - expect(newParams.get("state")).toBeDefined(); - expect(newParams.get("state")).not.toEqual(initialParams.get("state")); - }); - - it("finishOAuth is called whenever GitHubConnector is started", async () => { - const finishOAuthCallback = jest.fn().mockImplementation(); - gitHubOAuthService.finishOAuth = finishOAuthCallback; - - const params: IGitHubConnectorParams = { - state: "state", - code: "code" - }; - const searchParams = new URLSearchParams({ ...params }); - - const gitHubConnector = new GitHubConnector(); - gitHubConnector.start(searchParams, window); - - // GitHubConnector uses Window.postMessage and there's no good way to know when the message has received - await new Promise(resolve => setTimeout(resolve, 100)); - - expect(finishOAuthCallback).toBeCalledWith(params); - }); - - it("finishOAuth updates token", async () => { - const data = { key: "value" }; - const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data }); - junoClient.getGitHubToken = getGitHubTokenCallback; - - const initialToken = gitHubOAuthService.getTokenObservable()(); - const state = await gitHubOAuthService.startOAuth("scope"); - - const params: IGitHubConnectorParams = { - state, - code: "code" - }; - await gitHubOAuthService.finishOAuth(params); - const updatedToken = gitHubOAuthService.getTokenObservable()(); - - expect(getGitHubTokenCallback).toBeCalledWith("code"); - expect(initialToken).not.toEqual(updatedToken); - }); - - it("finishOAuth updates token to error if state doesn't match", async () => { - await gitHubOAuthService.startOAuth("scope"); - - const params: IGitHubConnectorParams = { - state: "state", - code: "code" - }; - await gitHubOAuthService.finishOAuth(params); - - expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined(); - }); - - it("finishOAuth updates token to error if unable to fetch token", async () => { - const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound }); - junoClient.getGitHubToken = getGitHubTokenCallback; - - const state = await gitHubOAuthService.startOAuth("scope"); - - const params: IGitHubConnectorParams = { - state, - code: "code" - }; - await gitHubOAuthService.finishOAuth(params); - - expect(getGitHubTokenCallback).toBeCalledWith("code"); - expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined(); - }); - - it("isLoggedIn returns false if resetToken is called", () => { - gitHubOAuthService.resetToken(); - expect(gitHubOAuthService.isLoggedIn()).toBeFalsy(); - }); - - it("isLoggedIn returns false if logout is called", async () => { - const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent }); - junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback; - - await gitHubOAuthService.logout(); - expect(gitHubOAuthService.isLoggedIn()).toBeFalsy(); - }); -}); +import ko from "knockout"; +import { HttpStatusCodes } from "../Common/Constants"; +import * as DataModels from "../Contracts/DataModels"; +import { JunoClient } from "../Juno/JunoClient"; +import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector"; +import { GitHubOAuthService } from "./GitHubOAuthService"; +import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import NotebookManager from "../Explorer/Notebook/NotebookManager"; +import Explorer from "../Explorer/Explorer"; + +const sampleDatabaseAccount: DataModels.DatabaseAccount = { + id: "id", + name: "name", + location: "location", + type: "type", + kind: "kind", + tags: [], + properties: { + documentEndpoint: "documentEndpoint", + gremlinEndpoint: "gremlinEndpoint", + tableEndpoint: "tableEndpoint", + cassandraEndpoint: "cassandraEndpoint", + }, +}; + +describe("GitHubOAuthService", () => { + let junoClient: JunoClient; + let gitHubOAuthService: GitHubOAuthService; + let originalDataExplorer: Explorer; + + beforeEach(() => { + junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); + gitHubOAuthService = new GitHubOAuthService(junoClient); + originalDataExplorer = window.dataExplorer; + window.dataExplorer = { + ...originalDataExplorer, + logConsoleData: (data): void => + data.type === ConsoleDataType.Error ? console.error(data.message) : console.error(data.message), + } as Explorer; + window.dataExplorer.notebookManager = new NotebookManager(); + window.dataExplorer.notebookManager.junoClient = junoClient; + window.dataExplorer.notebookManager.gitHubOAuthService = gitHubOAuthService; + }); + + afterEach(() => { + jest.resetAllMocks(); + window.dataExplorer = originalDataExplorer; + originalDataExplorer = undefined; + gitHubOAuthService = undefined; + junoClient = undefined; + }); + + it("logout deletes app authorization and resets token", async () => { + const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent }); + junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback; + + await gitHubOAuthService.logout(); + expect(deleteAppAuthorizationCallback).toBeCalled(); + expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined(); + }); + + it("resetToken resets token", () => { + gitHubOAuthService.resetToken(); + expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined(); + }); + + it("startOAuth resets OAuth state", async () => { + let url: string; + const windowOpenCallback = jest.fn().mockImplementation((value: string) => { + url = value; + }); + window.open = windowOpenCallback; + + await gitHubOAuthService.startOAuth("scope"); + expect(windowOpenCallback).toBeCalled(); + + const initialParams = new URLSearchParams(new URL(url).search); + expect(initialParams.get("state")).toBeDefined(); + + await gitHubOAuthService.startOAuth("another scope"); + expect(windowOpenCallback).toBeCalled(); + + const newParams = new URLSearchParams(new URL(url).search); + expect(newParams.get("state")).toBeDefined(); + expect(newParams.get("state")).not.toEqual(initialParams.get("state")); + }); + + it("finishOAuth is called whenever GitHubConnector is started", async () => { + const finishOAuthCallback = jest.fn().mockImplementation(); + gitHubOAuthService.finishOAuth = finishOAuthCallback; + + const params: IGitHubConnectorParams = { + state: "state", + code: "code", + }; + const searchParams = new URLSearchParams({ ...params }); + + const gitHubConnector = new GitHubConnector(); + gitHubConnector.start(searchParams, window); + + // GitHubConnector uses Window.postMessage and there's no good way to know when the message has received + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(finishOAuthCallback).toBeCalledWith(params); + }); + + it("finishOAuth updates token", async () => { + const data = { key: "value" }; + const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data }); + junoClient.getGitHubToken = getGitHubTokenCallback; + + const initialToken = gitHubOAuthService.getTokenObservable()(); + const state = await gitHubOAuthService.startOAuth("scope"); + + const params: IGitHubConnectorParams = { + state, + code: "code", + }; + await gitHubOAuthService.finishOAuth(params); + const updatedToken = gitHubOAuthService.getTokenObservable()(); + + expect(getGitHubTokenCallback).toBeCalledWith("code"); + expect(initialToken).not.toEqual(updatedToken); + }); + + it("finishOAuth updates token to error if state doesn't match", async () => { + await gitHubOAuthService.startOAuth("scope"); + + const params: IGitHubConnectorParams = { + state: "state", + code: "code", + }; + await gitHubOAuthService.finishOAuth(params); + + expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined(); + }); + + it("finishOAuth updates token to error if unable to fetch token", async () => { + const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound }); + junoClient.getGitHubToken = getGitHubTokenCallback; + + const state = await gitHubOAuthService.startOAuth("scope"); + + const params: IGitHubConnectorParams = { + state, + code: "code", + }; + await gitHubOAuthService.finishOAuth(params); + + expect(getGitHubTokenCallback).toBeCalledWith("code"); + expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined(); + }); + + it("isLoggedIn returns false if resetToken is called", () => { + gitHubOAuthService.resetToken(); + expect(gitHubOAuthService.isLoggedIn()).toBeFalsy(); + }); + + it("isLoggedIn returns false if logout is called", async () => { + const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent }); + junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback; + + await gitHubOAuthService.logout(); + expect(gitHubOAuthService.isLoggedIn()).toBeFalsy(); + }); +}); diff --git a/src/GitHub/GitHubOAuthService.ts b/src/GitHub/GitHubOAuthService.ts index 80a6d2c42..15d0c2d38 100644 --- a/src/GitHub/GitHubOAuthService.ts +++ b/src/GitHub/GitHubOAuthService.ts @@ -1,125 +1,125 @@ -import ko from "knockout"; -import { HttpStatusCodes } from "../Common/Constants"; -import { configContext } from "../ConfigContext"; -import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { JunoClient } from "../Juno/JunoClient"; -import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; -import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; -import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector"; -import { handleError } from "../Common/ErrorHandlingUtils"; - -window.addEventListener("message", (event: MessageEvent) => { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - const msg = event.data; - if (msg.type === GitHubConnectorMsgType) { - const params = msg.data as IGitHubConnectorParams; - window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params); - } -}); - -export interface IGitHubOAuthToken { - // API properties - access_token?: string; - scope?: string; - token_type?: string; - error?: string; - error_description?: string; -} - -export class GitHubOAuthService { - private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize"; - - private state: string; - private token: ko.Observable; - - constructor(private junoClient: JunoClient) { - this.token = ko.observable(); - } - - public async startOAuth(scope: string): Promise { - // If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization. - // Otherwise OAuth app still retains the "public & private repos" permissions. - if ( - this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key && - scope === AuthorizeAccessComponent.Scopes.Public.key - ) { - const logoutSuccessful = await this.logout(); - if (!logoutSuccessful) { - return undefined; - } - } - - const params = { - scope, - client_id: configContext.GITHUB_CLIENT_ID, - redirect_uri: new URL("./connectToGitHub.html", window.location.href).href, - state: this.resetState() - }; - - window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`); - return params.state; - } - - public async finishOAuth(params: IGitHubConnectorParams) { - try { - this.validateState(params.state); - const response = await this.junoClient.getGitHubToken(params.code); - - if (response.status === HttpStatusCodes.OK && !response.data.error) { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully connected to GitHub"); - this.token(response.data); - } else { - let errorMsg = response.data.error; - if (response.data.error_description) { - errorMsg = `${errorMsg}: ${response.data.error_description}`; - } - throw new Error(errorMsg); - } - } catch (error) { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect to GitHub: ${error}`); - this.token({ error }); - } - } - - public getTokenObservable(): ko.Observable { - return this.token; - } - - public async logout(): Promise { - try { - const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token); - if (response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`); - } - - this.resetToken(); - return true; - } catch (error) { - handleError(error, "GitHubOAuthService/logout", "Failed to delete app authorization"); - return false; - } - } - - public isLoggedIn(): boolean { - return !!this.token()?.access_token; - } - - private resetState(): string { - this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString(); - return this.state; - } - - public resetToken() { - this.token(undefined); - } - - private validateState(state: string) { - if (state !== this.state) { - throw new Error("State didn't match. Possibility of cross-site request forgery attack."); - } - } -} +import ko from "knockout"; +import { HttpStatusCodes } from "../Common/Constants"; +import { configContext } from "../ConfigContext"; +import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; +import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { JunoClient } from "../Juno/JunoClient"; +import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; +import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; +import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector"; +import { handleError } from "../Common/ErrorHandlingUtils"; + +window.addEventListener("message", (event: MessageEvent) => { + if (isInvalidParentFrameOrigin(event)) { + return; + } + + const msg = event.data; + if (msg.type === GitHubConnectorMsgType) { + const params = msg.data as IGitHubConnectorParams; + window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params); + } +}); + +export interface IGitHubOAuthToken { + // API properties + access_token?: string; + scope?: string; + token_type?: string; + error?: string; + error_description?: string; +} + +export class GitHubOAuthService { + private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize"; + + private state: string; + private token: ko.Observable; + + constructor(private junoClient: JunoClient) { + this.token = ko.observable(); + } + + public async startOAuth(scope: string): Promise { + // If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization. + // Otherwise OAuth app still retains the "public & private repos" permissions. + if ( + this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key && + scope === AuthorizeAccessComponent.Scopes.Public.key + ) { + const logoutSuccessful = await this.logout(); + if (!logoutSuccessful) { + return undefined; + } + } + + const params = { + scope, + client_id: configContext.GITHUB_CLIENT_ID, + redirect_uri: new URL("./connectToGitHub.html", window.location.href).href, + state: this.resetState(), + }; + + window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`); + return params.state; + } + + public async finishOAuth(params: IGitHubConnectorParams) { + try { + this.validateState(params.state); + const response = await this.junoClient.getGitHubToken(params.code); + + if (response.status === HttpStatusCodes.OK && !response.data.error) { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully connected to GitHub"); + this.token(response.data); + } else { + let errorMsg = response.data.error; + if (response.data.error_description) { + errorMsg = `${errorMsg}: ${response.data.error_description}`; + } + throw new Error(errorMsg); + } + } catch (error) { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect to GitHub: ${error}`); + this.token({ error }); + } + } + + public getTokenObservable(): ko.Observable { + return this.token; + } + + public async logout(): Promise { + try { + const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token); + if (response.status !== HttpStatusCodes.NoContent) { + throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`); + } + + this.resetToken(); + return true; + } catch (error) { + handleError(error, "GitHubOAuthService/logout", "Failed to delete app authorization"); + return false; + } + } + + public isLoggedIn(): boolean { + return !!this.token()?.access_token; + } + + private resetState(): string { + this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString(); + return this.state; + } + + public resetToken() { + this.token(undefined); + } + + private validateState(state: string) { + if (state !== this.state) { + throw new Error("State didn't match. Possibility of cross-site request forgery attack."); + } + } +} diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 7fe0988c7..bfaa1b47a 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -1,143 +1,143 @@ -import { useBoolean } from "@uifabric/react-hooks"; -import { initializeIcons } from "office-ui-fabric-react"; -import * as React from "react"; -import { render } from "react-dom"; -import ChevronRight from "../images/chevron-right.svg"; -import "../less/hostedexplorer.less"; -import { AuthType } from "./AuthType"; -import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; -import { DatabaseAccount } from "./Contracts/DataModels"; -import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel"; -import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; -import "./Explorer/Menus/NavBar/MeControlComponent.less"; -import { useTokenMetadata } from "./hooks/usePortalAccessToken"; -import { MeControl } from "./Platform/Hosted/Components/MeControl"; -import "./Platform/Hosted/ConnectScreen.less"; -import "./Shared/appInsights"; -import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; -import { useAADAuth } from "./hooks/useAADAuth"; -import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; -import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; -import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; - -initializeIcons(); - -const App: React.FunctionComponent = () => { - // For handling encrypted portal tokens sent via query paramter - const params = new URLSearchParams(window.location.search); - const [encryptedToken, setEncryptedToken] = React.useState(params && params.get("key")); - const encryptedTokenMetadata = useTokenMetadata(encryptedToken); - - // For showing/hiding panel - const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); - - const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); - const [databaseAccount, setDatabaseAccount] = React.useState(); - const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); - const [connectionString, setConnectionString] = React.useState(); - - const ref = React.useRef(); - - React.useEffect(() => { - // If ref.current is undefined no iframe has been rendered - if (ref.current) { - // In hosted mode, we can set global properties directly on the child iframe. - // This is not possible in the portal where the iframes have different origins - const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame; - // AAD authenticated uses ALWAYS using AAD authType - if (isLoggedIn) { - frameWindow.hostedConfig = { - authType: AuthType.AAD, - databaseAccount, - authorizationToken: armToken - }; - } else if (authType === AuthType.EncryptedToken) { - frameWindow.hostedConfig = { - authType: AuthType.EncryptedToken, - encryptedToken, - encryptedTokenMetadata - }; - } else if (authType === AuthType.ConnectionString) { - frameWindow.hostedConfig = { - authType: AuthType.ConnectionString, - encryptedToken, - encryptedTokenMetadata, - masterKey: extractMasterKeyfromConnectionString(connectionString) - }; - } else if (authType === AuthType.ResourceToken) { - frameWindow.hostedConfig = { - authType: AuthType.ResourceToken, - resourceToken: connectionString - }; - } - } - }); - - const showExplorer = - (isLoggedIn && databaseAccount) || - (encryptedTokenMetadata && encryptedTokenMetadata) || - (authType === AuthType.ResourceToken && connectionString); - - return ( - <> -
-
-
- window.open("https://portal.azure.com", "_blank")} - tabIndex={0} - title="Go to Azure Portal" - > - Microsoft Azure - - Cosmos DB - {(isLoggedIn || encryptedTokenMetadata?.accountName) && ( - account separator - )} - {isLoggedIn && ( - - - - )} - {!isLoggedIn && encryptedTokenMetadata?.accountName && ( - - {encryptedTokenMetadata?.accountName} - - )} -
- -
- {isLoggedIn ? ( - - ) : ( - - )} -
-
-
- {showExplorer && ( - // Ideally we would import and render data explorer like any other React component, however - // because it still has a significant amount of Knockout code, this would lead to memory leaks. - // Knockout does not have a way to tear down all of its binding and listeners with a single method. - // It's possible this can be changed once all knockout code has been removed. - - )} - {!isLoggedIn && !encryptedTokenMetadata && ( - - )} - {isLoggedIn && } - - ); -}; - -render(, document.getElementById("App")); +import { useBoolean } from "@uifabric/react-hooks"; +import { initializeIcons } from "office-ui-fabric-react"; +import * as React from "react"; +import { render } from "react-dom"; +import ChevronRight from "../images/chevron-right.svg"; +import "../less/hostedexplorer.less"; +import { AuthType } from "./AuthType"; +import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; +import { DatabaseAccount } from "./Contracts/DataModels"; +import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel"; +import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; +import "./Explorer/Menus/NavBar/MeControlComponent.less"; +import { useTokenMetadata } from "./hooks/usePortalAccessToken"; +import { MeControl } from "./Platform/Hosted/Components/MeControl"; +import "./Platform/Hosted/ConnectScreen.less"; +import "./Shared/appInsights"; +import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; +import { useAADAuth } from "./hooks/useAADAuth"; +import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; +import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; +import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; + +initializeIcons(); + +const App: React.FunctionComponent = () => { + // For handling encrypted portal tokens sent via query paramter + const params = new URLSearchParams(window.location.search); + const [encryptedToken, setEncryptedToken] = React.useState(params && params.get("key")); + const encryptedTokenMetadata = useTokenMetadata(encryptedToken); + + // For showing/hiding panel + const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); + + const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); + const [databaseAccount, setDatabaseAccount] = React.useState(); + const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); + const [connectionString, setConnectionString] = React.useState(); + + const ref = React.useRef(); + + React.useEffect(() => { + // If ref.current is undefined no iframe has been rendered + if (ref.current) { + // In hosted mode, we can set global properties directly on the child iframe. + // This is not possible in the portal where the iframes have different origins + const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame; + // AAD authenticated uses ALWAYS using AAD authType + if (isLoggedIn) { + frameWindow.hostedConfig = { + authType: AuthType.AAD, + databaseAccount, + authorizationToken: armToken, + }; + } else if (authType === AuthType.EncryptedToken) { + frameWindow.hostedConfig = { + authType: AuthType.EncryptedToken, + encryptedToken, + encryptedTokenMetadata, + }; + } else if (authType === AuthType.ConnectionString) { + frameWindow.hostedConfig = { + authType: AuthType.ConnectionString, + encryptedToken, + encryptedTokenMetadata, + masterKey: extractMasterKeyfromConnectionString(connectionString), + }; + } else if (authType === AuthType.ResourceToken) { + frameWindow.hostedConfig = { + authType: AuthType.ResourceToken, + resourceToken: connectionString, + }; + } + } + }); + + const showExplorer = + (isLoggedIn && databaseAccount) || + (encryptedTokenMetadata && encryptedTokenMetadata) || + (authType === AuthType.ResourceToken && connectionString); + + return ( + <> +
+
+
+ window.open("https://portal.azure.com", "_blank")} + tabIndex={0} + title="Go to Azure Portal" + > + Microsoft Azure + + Cosmos DB + {(isLoggedIn || encryptedTokenMetadata?.accountName) && ( + account separator + )} + {isLoggedIn && ( + + + + )} + {!isLoggedIn && encryptedTokenMetadata?.accountName && ( + + {encryptedTokenMetadata?.accountName} + + )} +
+ +
+ {isLoggedIn ? ( + + ) : ( + + )} +
+
+
+ {showExplorer && ( + // Ideally we would import and render data explorer like any other React component, however + // because it still has a significant amount of Knockout code, this would lead to memory leaks. + // Knockout does not have a way to tear down all of its binding and listeners with a single method. + // It's possible this can be changed once all knockout code has been removed. + + )} + {!isLoggedIn && !encryptedTokenMetadata && ( + + )} + {isLoggedIn && } + + ); +}; + +render(, document.getElementById("App")); diff --git a/src/Index.ts b/src/Index.ts index a86e0a63b..9eb33943c 100644 --- a/src/Index.ts +++ b/src/Index.ts @@ -1,23 +1,23 @@ -import "../less/index.less"; -import "./Libs/jquery"; - -import * as ko from "knockout"; - -class Index { - public navigationSelection: ko.Observable; - - constructor() { - this.navigationSelection = ko.observable("quickstart"); - } - - public quickstart_click() { - this.navigationSelection("quickstart"); - } - - public explorer_click() { - this.navigationSelection("explorer"); - } -} - -var index = new Index(); -ko.applyBindings(index); +import "../less/index.less"; +import "./Libs/jquery"; + +import * as ko from "knockout"; + +class Index { + public navigationSelection: ko.Observable; + + constructor() { + this.navigationSelection = ko.observable("quickstart"); + } + + public quickstart_click() { + this.navigationSelection("quickstart"); + } + + public explorer_click() { + this.navigationSelection("explorer"); + } +} + +var index = new Index(); +ko.applyBindings(index); diff --git a/src/Juno/JunoClient.test.ts b/src/Juno/JunoClient.test.ts index ccb9f761a..2255df7ce 100644 --- a/src/Juno/JunoClient.test.ts +++ b/src/Juno/JunoClient.test.ts @@ -1,386 +1,386 @@ -import ko from "knockout"; -import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; -import { IPinnedRepo, JunoClient, IPublishNotebookRequest } from "./JunoClient"; -import { configContext } from "../ConfigContext"; -import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; -import { DatabaseAccount } from "../Contracts/DataModels"; -import { updateUserContext, userContext } from "../UserContext"; - -const sampleSubscriptionId = "subscriptionId"; - -const sampleDatabaseAccount: DatabaseAccount = { - id: "id", - name: "name", - location: "location", - type: "type", - kind: "kind", - tags: [], - properties: { - documentEndpoint: "documentEndpoint", - gremlinEndpoint: "gremlinEndpoint", - tableEndpoint: "tableEndpoint", - cassandraEndpoint: "cassandraEndpoint" - } -}; - -const samplePinnedRepos: IPinnedRepo[] = [ - { - owner: "owner", - name: "name", - private: false, - branches: [ - { - name: "name" - } - ] - } -]; - -describe("Pinned repos", () => { - const junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); - - beforeEach(() => { - window.fetch = jest.fn().mockImplementation(() => { - return { - status: HttpStatusCodes.OK, - text: () => JSON.stringify(samplePinnedRepos) - }; - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("updatePinnedRepos invokes pinned repos subscribers", async () => { - const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {}); - - junoClient.subscribeToPinnedRepos(callback); - const response = await junoClient.updatePinnedRepos(samplePinnedRepos); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(callback).toBeCalledWith(samplePinnedRepos); - }); - - it("getPinnedRepos invokes pinned repos subscribers", async () => { - const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {}); - - junoClient.subscribeToPinnedRepos(callback); - const response = await junoClient.getPinnedRepos("scope"); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(callback).toBeCalledWith(samplePinnedRepos); - }); -}); - -describe("GitHub", () => { - const junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("getGitHubToken", async () => { - let fetchUrl: string; - window.fetch = jest.fn().mockImplementation((url: string) => { - fetchUrl = url; - - return { - status: HttpStatusCodes.OK, - text: () => JSON.stringify({ access_token: "token" }) - }; - }); - - const response = await junoClient.getGitHubToken("code"); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(response.data.access_token).toBeDefined(); - expect(window.fetch).toBeCalled(); - - const fetchUrlParams = new URLSearchParams(new URL(fetchUrl).search); - let fetchUrlParamsCount = 0; - fetchUrlParams.forEach(() => fetchUrlParamsCount++); - - expect(fetchUrlParamsCount).toBe(2); - expect(fetchUrlParams.get("code")).toBeDefined(); - expect(fetchUrlParams.get("client_id")).toBeDefined(); - }); - - it("deleteAppauthorization", async () => { - let fetchUrl: string; - window.fetch = jest.fn().mockImplementation((url: string) => { - fetchUrl = url; - - return { - status: HttpStatusCodes.NoContent, - text: () => undefined as string - }; - }); - - const response = await junoClient.deleteAppAuthorization("token"); - - expect(response.status).toBe(HttpStatusCodes.NoContent); - expect(window.fetch).toBeCalled(); - - const fetchUrlParams = new URLSearchParams(new URL(fetchUrl).search); - let fetchUrlParamsCount = 0; - fetchUrlParams.forEach(() => fetchUrlParamsCount++); - - expect(fetchUrlParamsCount).toBe(2); - expect(fetchUrlParams.get("access_token")).toBeDefined(); - expect(fetchUrlParams.get("client_id")).toBeDefined(); - }); -}); - -describe("Gallery", () => { - const junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); - const originalSubscriptionId = userContext.subscriptionId; - - beforeAll(() => { - updateUserContext({ subscriptionId: sampleSubscriptionId }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - afterAll(() => { - updateUserContext({ subscriptionId: originalSubscriptionId }); - }); - - it("getSampleNotebooks", async () => { - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.getSampleNotebooks(); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/samples`, undefined); - }); - - it("getPublicNotebooks", async () => { - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.getPublicNotebooks(); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/public`, undefined); - }); - - it("getNotebook", async () => { - const id = "id"; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.getNotebookInfo(id); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`); - }); - - it("getNotebookContent", async () => { - const id = "id"; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - text: () => undefined as any - }); - - const response = await junoClient.getNotebookContent(id); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/content`); - }); - - it("increaseNotebookViews", async () => { - const id = "id"; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.increaseNotebookViews(id); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/views`, { - method: "PATCH" - }); - }); - - it("increaseNotebookDownloadCount", async () => { - const id = "id"; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.increaseNotebookDownloadCount(id); - - const authorizationHeader = getAuthorizationHeader(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/downloads`, - { - method: "PATCH", - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - } - } - ); - }); - - it("favoriteNotebook", async () => { - const id = "id"; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.favoriteNotebook(id); - - const authorizationHeader = getAuthorizationHeader(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/favorite`, - { - method: "PATCH", - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - } - } - ); - }); - - it("unfavoriteNotebook", async () => { - const id = "id"; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.unfavoriteNotebook(id); - - const authorizationHeader = getAuthorizationHeader(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/unfavorite`, { - method: "PATCH", - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - } - }); - }); - - it("getFavoriteNotebooks", async () => { - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.getFavoriteNotebooks(); - - const authorizationHeader = getAuthorizationHeader(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, { - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - } - }); - }); - - it("getPublishedNotebooks", async () => { - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.getPublishedNotebooks(); - - const authorizationHeader = getAuthorizationHeader(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/gallery/published`, - { - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - } - } - ); - }); - - it("deleteNotebook", async () => { - const id = "id"; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.deleteNotebook(id); - - const authorizationHeader = getAuthorizationHeader(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`, { - method: "DELETE", - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - } - }); - }); - - it("publishNotebook", async () => { - const name = "name"; - const description = "description"; - const tags = ["tag"]; - const author = "author"; - const thumbnailUrl = "thumbnailUrl"; - const content = `{ "key": "value" }`; - const addLinkToNotebookViewer = false; - window.fetch = jest.fn().mockReturnValue({ - status: HttpStatusCodes.OK, - json: () => undefined as any - }); - - const response = await junoClient.publishNotebook( - name, - description, - tags, - author, - thumbnailUrl, - content, - addLinkToNotebookViewer - ); - - const authorizationHeader = getAuthorizationHeader(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/${sampleDatabaseAccount.name}/gallery`, - { - method: "PUT", - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - }, - body: JSON.stringify({ - name, - description, - tags, - author, - thumbnailUrl, - content: JSON.parse(content), - addLinkToNotebookViewer - } as IPublishNotebookRequest) - } - ); - }); -}); +import ko from "knockout"; +import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; +import { IPinnedRepo, JunoClient, IPublishNotebookRequest } from "./JunoClient"; +import { configContext } from "../ConfigContext"; +import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; +import { DatabaseAccount } from "../Contracts/DataModels"; +import { updateUserContext, userContext } from "../UserContext"; + +const sampleSubscriptionId = "subscriptionId"; + +const sampleDatabaseAccount: DatabaseAccount = { + id: "id", + name: "name", + location: "location", + type: "type", + kind: "kind", + tags: [], + properties: { + documentEndpoint: "documentEndpoint", + gremlinEndpoint: "gremlinEndpoint", + tableEndpoint: "tableEndpoint", + cassandraEndpoint: "cassandraEndpoint", + }, +}; + +const samplePinnedRepos: IPinnedRepo[] = [ + { + owner: "owner", + name: "name", + private: false, + branches: [ + { + name: "name", + }, + ], + }, +]; + +describe("Pinned repos", () => { + const junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); + + beforeEach(() => { + window.fetch = jest.fn().mockImplementation(() => { + return { + status: HttpStatusCodes.OK, + text: () => JSON.stringify(samplePinnedRepos), + }; + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("updatePinnedRepos invokes pinned repos subscribers", async () => { + const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {}); + + junoClient.subscribeToPinnedRepos(callback); + const response = await junoClient.updatePinnedRepos(samplePinnedRepos); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(callback).toBeCalledWith(samplePinnedRepos); + }); + + it("getPinnedRepos invokes pinned repos subscribers", async () => { + const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {}); + + junoClient.subscribeToPinnedRepos(callback); + const response = await junoClient.getPinnedRepos("scope"); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(callback).toBeCalledWith(samplePinnedRepos); + }); +}); + +describe("GitHub", () => { + const junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("getGitHubToken", async () => { + let fetchUrl: string; + window.fetch = jest.fn().mockImplementation((url: string) => { + fetchUrl = url; + + return { + status: HttpStatusCodes.OK, + text: () => JSON.stringify({ access_token: "token" }), + }; + }); + + const response = await junoClient.getGitHubToken("code"); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(response.data.access_token).toBeDefined(); + expect(window.fetch).toBeCalled(); + + const fetchUrlParams = new URLSearchParams(new URL(fetchUrl).search); + let fetchUrlParamsCount = 0; + fetchUrlParams.forEach(() => fetchUrlParamsCount++); + + expect(fetchUrlParamsCount).toBe(2); + expect(fetchUrlParams.get("code")).toBeDefined(); + expect(fetchUrlParams.get("client_id")).toBeDefined(); + }); + + it("deleteAppauthorization", async () => { + let fetchUrl: string; + window.fetch = jest.fn().mockImplementation((url: string) => { + fetchUrl = url; + + return { + status: HttpStatusCodes.NoContent, + text: () => undefined as string, + }; + }); + + const response = await junoClient.deleteAppAuthorization("token"); + + expect(response.status).toBe(HttpStatusCodes.NoContent); + expect(window.fetch).toBeCalled(); + + const fetchUrlParams = new URLSearchParams(new URL(fetchUrl).search); + let fetchUrlParamsCount = 0; + fetchUrlParams.forEach(() => fetchUrlParamsCount++); + + expect(fetchUrlParamsCount).toBe(2); + expect(fetchUrlParams.get("access_token")).toBeDefined(); + expect(fetchUrlParams.get("client_id")).toBeDefined(); + }); +}); + +describe("Gallery", () => { + const junoClient = new JunoClient(ko.observable(sampleDatabaseAccount)); + const originalSubscriptionId = userContext.subscriptionId; + + beforeAll(() => { + updateUserContext({ subscriptionId: sampleSubscriptionId }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + updateUserContext({ subscriptionId: originalSubscriptionId }); + }); + + it("getSampleNotebooks", async () => { + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.getSampleNotebooks(); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/samples`, undefined); + }); + + it("getPublicNotebooks", async () => { + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.getPublicNotebooks(); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/public`, undefined); + }); + + it("getNotebook", async () => { + const id = "id"; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.getNotebookInfo(id); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`); + }); + + it("getNotebookContent", async () => { + const id = "id"; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + text: () => undefined as any, + }); + + const response = await junoClient.getNotebookContent(id); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/content`); + }); + + it("increaseNotebookViews", async () => { + const id = "id"; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.increaseNotebookViews(id); + + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/views`, { + method: "PATCH", + }); + }); + + it("increaseNotebookDownloadCount", async () => { + const id = "id"; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.increaseNotebookDownloadCount(id); + + const authorizationHeader = getAuthorizationHeader(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith( + `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/downloads`, + { + method: "PATCH", + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }, + } + ); + }); + + it("favoriteNotebook", async () => { + const id = "id"; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.favoriteNotebook(id); + + const authorizationHeader = getAuthorizationHeader(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith( + `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/favorite`, + { + method: "PATCH", + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }, + } + ); + }); + + it("unfavoriteNotebook", async () => { + const id = "id"; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.unfavoriteNotebook(id); + + const authorizationHeader = getAuthorizationHeader(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/unfavorite`, { + method: "PATCH", + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }, + }); + }); + + it("getFavoriteNotebooks", async () => { + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.getFavoriteNotebooks(); + + const authorizationHeader = getAuthorizationHeader(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, { + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }, + }); + }); + + it("getPublishedNotebooks", async () => { + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.getPublishedNotebooks(); + + const authorizationHeader = getAuthorizationHeader(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith( + `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/gallery/published`, + { + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }, + } + ); + }); + + it("deleteNotebook", async () => { + const id = "id"; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.deleteNotebook(id); + + const authorizationHeader = getAuthorizationHeader(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`, { + method: "DELETE", + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }, + }); + }); + + it("publishNotebook", async () => { + const name = "name"; + const description = "description"; + const tags = ["tag"]; + const author = "author"; + const thumbnailUrl = "thumbnailUrl"; + const content = `{ "key": "value" }`; + const addLinkToNotebookViewer = false; + window.fetch = jest.fn().mockReturnValue({ + status: HttpStatusCodes.OK, + json: () => undefined as any, + }); + + const response = await junoClient.publishNotebook( + name, + description, + tags, + author, + thumbnailUrl, + content, + addLinkToNotebookViewer + ); + + const authorizationHeader = getAuthorizationHeader(); + expect(response.status).toBe(HttpStatusCodes.OK); + expect(window.fetch).toBeCalledWith( + `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/${sampleDatabaseAccount.name}/gallery`, + { + method: "PUT", + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }, + body: JSON.stringify({ + name, + description, + tags, + author, + thumbnailUrl, + content: JSON.parse(content), + addLinkToNotebookViewer, + } as IPublishNotebookRequest), + } + ); + }); +}); diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index bbc1954cc..557fe29d0 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -1,528 +1,528 @@ -import ko from "knockout"; -import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; -import { configContext } from "../ConfigContext"; -import * as DataModels from "../Contracts/DataModels"; -import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; -import { IGitHubResponse } from "../GitHub/GitHubClient"; -import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService"; -import { userContext } from "../UserContext"; -import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; -import { number } from "prop-types"; - -export interface IJunoResponse { - status: number; - data: T; -} - -export interface IPinnedRepo { - owner: string; - name: string; - private: boolean; - branches: IPinnedBranch[]; -} - -export interface IPinnedBranch { - name: string; -} - -export interface IGalleryItem { - id: string; - name: string; - description: string; - gitSha: string; - tags: string[]; - author: string; - thumbnailUrl: string; - created: string; - isSample: boolean; - downloads: number; - favorites: number; - views: number; - newCellId: string; - policyViolations: string[]; - pendingScanJobIds: string[]; -} - -export interface IPublicGalleryData { - metadata: IPublicGalleryMetaData; - notebooksData: IGalleryItem[]; -} - -export interface IPublicGalleryMetaData { - acceptedCodeOfConduct: boolean; -} - -export interface IUserGallery { - favorites: string[]; - published: string[]; -} - -// Only exported for unit test -export interface IPublishNotebookRequest { - name: string; - description: string; - tags: string[]; - author: string; - thumbnailUrl: string; - content: any; - addLinkToNotebookViewer: boolean; -} - -export class JunoClient { - private cachedPinnedRepos: ko.Observable; - - constructor(private databaseAccount?: ko.Observable) { - this.cachedPinnedRepos = ko.observable([]); - } - - public subscribeToPinnedRepos(callback: ko.SubscriptionCallback): ko.Subscription { - return this.cachedPinnedRepos.subscribe(callback); - } - - public async getPinnedRepos(scope: string): Promise> { - const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, { - headers: JunoClient.getHeaders() - }); - - let pinnedRepos: IPinnedRepo[]; - if (response.status === HttpStatusCodes.OK) { - pinnedRepos = JSON.parse(await response.text()); - - // In case we're restricted to public only scope, we return only public repos - if (scope === AuthorizeAccessComponent.Scopes.Public.key) { - pinnedRepos = pinnedRepos.filter(repo => !repo.private); - } - - this.cachedPinnedRepos(pinnedRepos); - } - - return { - status: response.status, - data: pinnedRepos - }; - } - - public async updatePinnedRepos(repos: IPinnedRepo[]): Promise> { - const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, { - method: "PUT", - body: JSON.stringify(repos), - headers: JunoClient.getHeaders() - }); - - if (response.status === HttpStatusCodes.OK) { - this.cachedPinnedRepos(repos); - } - - return { - status: response.status, - data: undefined - }; - } - - public async deleteGitHubInfo(): Promise> { - const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github`, { - method: "DELETE", - headers: JunoClient.getHeaders() - }); - - return { - status: response.status, - data: undefined - }; - } - - public async getGitHubToken(code: string): Promise> { - const githubParams = JunoClient.getGitHubClientParams(); - githubParams.append("code", code); - - const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, { - headers: JunoClient.getHeaders() - }); - - let data: IGitHubOAuthToken; - const body = await response.text(); - if (body) { - data = JSON.parse(body); - } else { - data = { - error: response.statusText - }; - } - - return { - status: response.status, - data - }; - } - - public async deleteAppAuthorization(token: string): Promise> { - const githubParams = JunoClient.getGitHubClientParams(); - githubParams.append("access_token", token); - - const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, { - method: "DELETE", - headers: JunoClient.getHeaders() - }); - - return { - status: response.status, - data: await response.text() - }; - } - - public async getSampleNotebooks(): Promise> { - return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/samples`); - } - - public async getPublicNotebooks(): Promise> { - return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`); - } - - public async getPublicGalleryData(): Promise> { - const url = `${this.getNotebooksAccountUrl()}/gallery/public`; - const response = await window.fetch(url, { - method: "PATCH", - headers: JunoClient.getHeaders() - }); - - let data: IPublicGalleryData; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async acceptCodeOfConduct(): Promise> { - const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`; - const response = await window.fetch(url, { - method: "PATCH", - headers: JunoClient.getHeaders() - }); - - let data: boolean; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async isCodeOfConductAccepted(): Promise> { - const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`; - const response = await window.fetch(url, { - method: "PATCH", - headers: JunoClient.getHeaders() - }); - - let data: boolean; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async getNotebookInfo(id: string): Promise> { - const response = await window.fetch(this.getNotebookInfoUrl(id)); - - let data: IGalleryItem; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async getNotebookContent(id: string): Promise> { - const response = await window.fetch(this.getNotebookContentUrl(id)); - - let data: string; - if (response.status === HttpStatusCodes.OK) { - data = await response.text(); - } - - return { - status: response.status, - data - }; - } - - public async increaseNotebookViews(id: string): Promise> { - const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/views`, { - method: "PATCH" - }); - - let data: IGalleryItem; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async increaseNotebookDownloadCount(id: string): Promise> { - const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/downloads`, { - method: "PATCH", - headers: JunoClient.getHeaders() - }); - - let data: IGalleryItem; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async favoriteNotebook(id: string): Promise> { - const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/favorite`, { - method: "PATCH", - headers: JunoClient.getHeaders() - }); - - let data: IGalleryItem; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async unfavoriteNotebook(id: string): Promise> { - const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/unfavorite`, { - method: "PATCH", - headers: JunoClient.getHeaders() - }); - - let data: IGalleryItem; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async getFavoriteNotebooks(): Promise> { - return await this.getNotebooks(`${this.getNotebooksUrl()}/gallery/favorites`, { - headers: JunoClient.getHeaders() - }); - } - - public async getPublishedNotebooks(): Promise> { - return await this.getNotebooks(`${this.getNotebooksUrl()}/${this.getSubscriptionId()}/gallery/published`, { - headers: JunoClient.getHeaders() - }); - } - - public async deleteNotebook(id: string): Promise> { - const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}`, { - method: "DELETE", - headers: JunoClient.getHeaders() - }); - - let data: IGalleryItem; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async publishNotebook( - name: string, - description: string, - tags: string[], - author: string, - thumbnailUrl: string, - content: string, - isLinkInjectionEnabled: boolean - ): Promise> { - const response = await window.fetch( - `${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}/gallery`, - { - method: "PUT", - headers: JunoClient.getHeaders(), - body: JSON.stringify({ - name, - description, - tags, - author, - thumbnailUrl, - content: JSON.parse(content), - addLinkToNotebookViewer: isLinkInjectionEnabled - } as IPublishNotebookRequest) - } - ); - - let data: IGalleryItem; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } else { - throw new Error(`HTTP status ${response.status} thrown. ${(await response.json()).Message}`); - } - - return { - status: response.status, - data - }; - } - - public getNotebookContentUrl(id: string): string { - return `${this.getNotebooksUrl()}/gallery/${id}/content`; - } - - public getNotebookInfoUrl(id: string): string { - return `${this.getNotebooksUrl()}/gallery/${id}`; - } - - public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise> { - const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, { - method: "POST", - body: JSON.stringify({ - notebookId, - abuseCategory, - notes - }), - headers: { - [HttpHeaders.contentType]: "application/json" - } - }); - - let data: boolean; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async requestSchema( - schemaRequest: DataModels.ISchemaRequest - ): Promise> { - const response = await window.fetch(`${this.getAnalyticsUrl()}/${schemaRequest.accountName}/schema/request`, { - method: "POST", - body: JSON.stringify(schemaRequest), - headers: JunoClient.getHeaders() - }); - - let data: DataModels.ISchemaRequest; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - public async getSchema( - accountName: string, - databaseName: string, - containerName: string - ): Promise> { - const response = await window.fetch( - `${this.getAnalyticsUrl()}/${accountName}/schema/${databaseName}/${containerName}`, - { - method: "GET", - headers: JunoClient.getHeaders() - } - ); - - let data: DataModels.ISchema; - - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise> { - const response = await window.fetch(input, init); - - let data: IGalleryItem[]; - if (response.status === HttpStatusCodes.OK) { - data = await response.json(); - } - - return { - status: response.status, - data - }; - } - - private getNotebooksUrl(): string { - return `${configContext.JUNO_ENDPOINT}/api/notebooks`; - } - - private getAccount(): string { - return this.databaseAccount().name; - } - - private getSubscriptionId(): string { - return userContext.subscriptionId; - } - - private getNotebooksAccountUrl(): string { - return `${this.getNotebooksUrl()}/${this.getAccount()}`; - } - - private getAnalyticsUrl(): string { - return `${configContext.JUNO_ENDPOINT}/api/analytics`; - } - - private static getHeaders(): HeadersInit { - const authorizationHeader = getAuthorizationHeader(); - return { - [authorizationHeader.header]: authorizationHeader.token, - [HttpHeaders.contentType]: "application/json" - }; - } - - private static getGitHubClientParams(): URLSearchParams { - const githubParams = new URLSearchParams({ - client_id: configContext.GITHUB_CLIENT_ID - }); - - if (configContext.GITHUB_CLIENT_SECRET) { - githubParams.append("client_secret", configContext.GITHUB_CLIENT_SECRET); - } - - return githubParams; - } -} +import ko from "knockout"; +import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; +import { configContext } from "../ConfigContext"; +import * as DataModels from "../Contracts/DataModels"; +import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; +import { IGitHubResponse } from "../GitHub/GitHubClient"; +import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService"; +import { userContext } from "../UserContext"; +import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; +import { number } from "prop-types"; + +export interface IJunoResponse { + status: number; + data: T; +} + +export interface IPinnedRepo { + owner: string; + name: string; + private: boolean; + branches: IPinnedBranch[]; +} + +export interface IPinnedBranch { + name: string; +} + +export interface IGalleryItem { + id: string; + name: string; + description: string; + gitSha: string; + tags: string[]; + author: string; + thumbnailUrl: string; + created: string; + isSample: boolean; + downloads: number; + favorites: number; + views: number; + newCellId: string; + policyViolations: string[]; + pendingScanJobIds: string[]; +} + +export interface IPublicGalleryData { + metadata: IPublicGalleryMetaData; + notebooksData: IGalleryItem[]; +} + +export interface IPublicGalleryMetaData { + acceptedCodeOfConduct: boolean; +} + +export interface IUserGallery { + favorites: string[]; + published: string[]; +} + +// Only exported for unit test +export interface IPublishNotebookRequest { + name: string; + description: string; + tags: string[]; + author: string; + thumbnailUrl: string; + content: any; + addLinkToNotebookViewer: boolean; +} + +export class JunoClient { + private cachedPinnedRepos: ko.Observable; + + constructor(private databaseAccount?: ko.Observable) { + this.cachedPinnedRepos = ko.observable([]); + } + + public subscribeToPinnedRepos(callback: ko.SubscriptionCallback): ko.Subscription { + return this.cachedPinnedRepos.subscribe(callback); + } + + public async getPinnedRepos(scope: string): Promise> { + const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, { + headers: JunoClient.getHeaders(), + }); + + let pinnedRepos: IPinnedRepo[]; + if (response.status === HttpStatusCodes.OK) { + pinnedRepos = JSON.parse(await response.text()); + + // In case we're restricted to public only scope, we return only public repos + if (scope === AuthorizeAccessComponent.Scopes.Public.key) { + pinnedRepos = pinnedRepos.filter((repo) => !repo.private); + } + + this.cachedPinnedRepos(pinnedRepos); + } + + return { + status: response.status, + data: pinnedRepos, + }; + } + + public async updatePinnedRepos(repos: IPinnedRepo[]): Promise> { + const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, { + method: "PUT", + body: JSON.stringify(repos), + headers: JunoClient.getHeaders(), + }); + + if (response.status === HttpStatusCodes.OK) { + this.cachedPinnedRepos(repos); + } + + return { + status: response.status, + data: undefined, + }; + } + + public async deleteGitHubInfo(): Promise> { + const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github`, { + method: "DELETE", + headers: JunoClient.getHeaders(), + }); + + return { + status: response.status, + data: undefined, + }; + } + + public async getGitHubToken(code: string): Promise> { + const githubParams = JunoClient.getGitHubClientParams(); + githubParams.append("code", code); + + const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, { + headers: JunoClient.getHeaders(), + }); + + let data: IGitHubOAuthToken; + const body = await response.text(); + if (body) { + data = JSON.parse(body); + } else { + data = { + error: response.statusText, + }; + } + + return { + status: response.status, + data, + }; + } + + public async deleteAppAuthorization(token: string): Promise> { + const githubParams = JunoClient.getGitHubClientParams(); + githubParams.append("access_token", token); + + const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, { + method: "DELETE", + headers: JunoClient.getHeaders(), + }); + + return { + status: response.status, + data: await response.text(), + }; + } + + public async getSampleNotebooks(): Promise> { + return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/samples`); + } + + public async getPublicNotebooks(): Promise> { + return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`); + } + + public async getPublicGalleryData(): Promise> { + const url = `${this.getNotebooksAccountUrl()}/gallery/public`; + const response = await window.fetch(url, { + method: "PATCH", + headers: JunoClient.getHeaders(), + }); + + let data: IPublicGalleryData; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async acceptCodeOfConduct(): Promise> { + const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`; + const response = await window.fetch(url, { + method: "PATCH", + headers: JunoClient.getHeaders(), + }); + + let data: boolean; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async isCodeOfConductAccepted(): Promise> { + const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`; + const response = await window.fetch(url, { + method: "PATCH", + headers: JunoClient.getHeaders(), + }); + + let data: boolean; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async getNotebookInfo(id: string): Promise> { + const response = await window.fetch(this.getNotebookInfoUrl(id)); + + let data: IGalleryItem; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async getNotebookContent(id: string): Promise> { + const response = await window.fetch(this.getNotebookContentUrl(id)); + + let data: string; + if (response.status === HttpStatusCodes.OK) { + data = await response.text(); + } + + return { + status: response.status, + data, + }; + } + + public async increaseNotebookViews(id: string): Promise> { + const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/views`, { + method: "PATCH", + }); + + let data: IGalleryItem; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async increaseNotebookDownloadCount(id: string): Promise> { + const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/downloads`, { + method: "PATCH", + headers: JunoClient.getHeaders(), + }); + + let data: IGalleryItem; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async favoriteNotebook(id: string): Promise> { + const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/favorite`, { + method: "PATCH", + headers: JunoClient.getHeaders(), + }); + + let data: IGalleryItem; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async unfavoriteNotebook(id: string): Promise> { + const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/unfavorite`, { + method: "PATCH", + headers: JunoClient.getHeaders(), + }); + + let data: IGalleryItem; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async getFavoriteNotebooks(): Promise> { + return await this.getNotebooks(`${this.getNotebooksUrl()}/gallery/favorites`, { + headers: JunoClient.getHeaders(), + }); + } + + public async getPublishedNotebooks(): Promise> { + return await this.getNotebooks(`${this.getNotebooksUrl()}/${this.getSubscriptionId()}/gallery/published`, { + headers: JunoClient.getHeaders(), + }); + } + + public async deleteNotebook(id: string): Promise> { + const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}`, { + method: "DELETE", + headers: JunoClient.getHeaders(), + }); + + let data: IGalleryItem; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async publishNotebook( + name: string, + description: string, + tags: string[], + author: string, + thumbnailUrl: string, + content: string, + isLinkInjectionEnabled: boolean + ): Promise> { + const response = await window.fetch( + `${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}/gallery`, + { + method: "PUT", + headers: JunoClient.getHeaders(), + body: JSON.stringify({ + name, + description, + tags, + author, + thumbnailUrl, + content: JSON.parse(content), + addLinkToNotebookViewer: isLinkInjectionEnabled, + } as IPublishNotebookRequest), + } + ); + + let data: IGalleryItem; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } else { + throw new Error(`HTTP status ${response.status} thrown. ${(await response.json()).Message}`); + } + + return { + status: response.status, + data, + }; + } + + public getNotebookContentUrl(id: string): string { + return `${this.getNotebooksUrl()}/gallery/${id}/content`; + } + + public getNotebookInfoUrl(id: string): string { + return `${this.getNotebooksUrl()}/gallery/${id}`; + } + + public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise> { + const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, { + method: "POST", + body: JSON.stringify({ + notebookId, + abuseCategory, + notes, + }), + headers: { + [HttpHeaders.contentType]: "application/json", + }, + }); + + let data: boolean; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async requestSchema( + schemaRequest: DataModels.ISchemaRequest + ): Promise> { + const response = await window.fetch(`${this.getAnalyticsUrl()}/${schemaRequest.accountName}/schema/request`, { + method: "POST", + body: JSON.stringify(schemaRequest), + headers: JunoClient.getHeaders(), + }); + + let data: DataModels.ISchemaRequest; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + public async getSchema( + accountName: string, + databaseName: string, + containerName: string + ): Promise> { + const response = await window.fetch( + `${this.getAnalyticsUrl()}/${accountName}/schema/${databaseName}/${containerName}`, + { + method: "GET", + headers: JunoClient.getHeaders(), + } + ); + + let data: DataModels.ISchema; + + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise> { + const response = await window.fetch(input, init); + + let data: IGalleryItem[]; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data, + }; + } + + private getNotebooksUrl(): string { + return `${configContext.JUNO_ENDPOINT}/api/notebooks`; + } + + private getAccount(): string { + return this.databaseAccount().name; + } + + private getSubscriptionId(): string { + return userContext.subscriptionId; + } + + private getNotebooksAccountUrl(): string { + return `${this.getNotebooksUrl()}/${this.getAccount()}`; + } + + private getAnalyticsUrl(): string { + return `${configContext.JUNO_ENDPOINT}/api/analytics`; + } + + private static getHeaders(): HeadersInit { + const authorizationHeader = getAuthorizationHeader(); + return { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }; + } + + private static getGitHubClientParams(): URLSearchParams { + const githubParams = new URLSearchParams({ + client_id: configContext.GITHUB_CLIENT_ID, + }); + + if (configContext.GITHUB_CLIENT_SECRET) { + githubParams.append("client_secret", configContext.GITHUB_CLIENT_SECRET); + } + + return githubParams; + } +} diff --git a/src/Main.tsx b/src/Main.tsx index 2e2540d72..d2f78bec1 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -1,551 +1,551 @@ -// CSS Dependencies -import "bootstrap/dist/css/bootstrap.css"; -import "../less/documentDB.less"; -import "../less/tree.less"; -import "../less/forms.less"; -import "../less/menus.less"; -import "../less/infobox.less"; -import "../less/messagebox.less"; -import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; -import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; -import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; -import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; -import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; -import "./Explorer/Controls/DynamicList/DynamicListComponent.less"; -import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; -import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; -import "../less/TableStyles/queryBuilder.less"; -import "../externals/jquery.dataTables.min.css"; -import "../less/TableStyles/fulldatatables.less"; -import "../less/TableStyles/EntityEditor.less"; -import "../less/TableStyles/CustomizeColumns.less"; -import "../less/resourceTree.less"; -import "../externals/jquery.typeahead.min.css"; -import "../externals/jquery-ui.min.css"; -import "../externals/jquery-ui.structure.min.css"; -import "../externals/jquery-ui.theme.min.css"; -import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less"; -import "./Explorer/Panes/GraphNewVertexPane.less"; -import "./Explorer/Tabs/QueryTab.less"; -import "./Explorer/Controls/TreeComponent/treeComponent.less"; -import "./Explorer/Controls/Accordion/AccordionComponent.less"; -import "./Explorer/SplashScreen/SplashScreenComponent.less"; -import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; - -// Image Dependencies -import "../images/CosmosDB_rgb_ui_lighttheme.ico"; -import "../images/favicon.ico"; - -import "./Shared/appInsights"; -import "babel-polyfill"; -import "es6-symbol/implement"; -import "webcrypto-liner/build/webcrypto-liner.shim.min"; -import "./Libs/jquery"; -import "bootstrap/dist/js/npm"; -import "../externals/jquery.typeahead.min.js"; -import "../externals/jquery-ui.min.js"; -import "promise-polyfill/src/polyfill"; -import "abort-controller/polyfill"; -import "whatwg-fetch"; -import "es6-object-assign/auto"; -import "promise.prototype.finally/auto"; -import "object.entries/auto"; -import "./Libs/is-integer-polyfill"; -import "url-polyfill/url-polyfill.min"; - -initializeIcons(); - -import { AuthType } from "./AuthType"; - -import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; -import { applyExplorerBindings } from "./applyExplorerBindings"; -import { configContext, initializeConfiguration, Platform } from "./ConfigContext"; -import Explorer from "./Explorer/Explorer"; -import React, { useEffect } from "react"; -import ReactDOM from "react-dom"; -import copyImage from "../images/Copy.svg"; -import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; -import refreshImg from "../images/refresh-cosmos.svg"; -import arrowLeftImg from "../images/imgarrowlefticon.svg"; -import { KOCommentEnd, KOCommentIfStart } from "./koComment"; -import { updateUserContext } from "./UserContext"; -import { CollectionCreation } from "./Shared/Constants"; -import { extractFeatures } from "./Platform/Hosted/extractFeatures"; -import { emulatorAccount } from "./Platform/Emulator/emulatorAccount"; -import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; -import { - getDatabaseAccountKindFromExperience, - getDatabaseAccountPropertiesFromMetadata -} from "./Platform/Hosted/HostedUtils"; -import { DefaultExperienceUtility } from "./Shared/DefaultExperienceUtility"; -import { parseResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils"; -import { AccountKind, DefaultAccountExperience, ServerIds } from "./Common/Constants"; -import { listKeys } from "./Utils/arm/generatedClients/2020-04-01/databaseAccounts"; -import { SelfServeType } from "./SelfServe/SelfServeUtils"; - -const App: React.FunctionComponent = () => { - useEffect(() => { - initializeConfiguration().then(async config => { - let explorer: Explorer; - if (config.platform === Platform.Hosted) { - const win = (window as unknown) as HostedExplorerChildFrame; - explorer = new Explorer(); - explorer.selfServeType(SelfServeType.none); - if (win.hostedConfig.authType === AuthType.EncryptedToken) { - // TODO: Remove window.authType - window.authType = AuthType.EncryptedToken; - // Impossible to tell if this is a try cosmos sub using an encrypted token - explorer.isTryCosmosDBSubscription(false); - updateUserContext({ - accessToken: encodeURIComponent(win.hostedConfig.encryptedToken) - }); - - const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( - win.hostedConfig.encryptedTokenMetadata.apiKind - ); - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: "", - // id: Main._databaseAccountId, - name: win.hostedConfig.encryptedTokenMetadata.accountName, - kind: getDatabaseAccountKindFromExperience(apiExperience), - properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), - tags: [] - }, - subscriptionId: undefined, - resourceGroup: undefined, - masterKey: undefined, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() - }); - explorer.isAccountReady(true); - } else if (win.hostedConfig.authType === AuthType.ResourceToken) { - window.authType = AuthType.ResourceToken; - // Resource tokens can only be used with SQL API - const apiExperience: string = DefaultAccountExperience.DocumentDB; - const parsedResourceToken = parseResourceTokenConnectionString(win.hostedConfig.resourceToken); - updateUserContext({ - resourceToken: parsedResourceToken.resourceToken, - endpoint: parsedResourceToken.accountEndpoint - }); - explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); - explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); - if (parsedResourceToken.partitionKey) { - explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); - } - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: "", - name: parsedResourceToken.accountEndpoint, - kind: AccountKind.GlobalDocumentDB, - properties: { documentEndpoint: parsedResourceToken.accountEndpoint }, - tags: { defaultExperience: apiExperience } - }, - subscriptionId: undefined, - resourceGroup: undefined, - masterKey: undefined, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), - isAuthWithresourceToken: true - }); - explorer.isAccountReady(true); - explorer.isRefreshingExplorer(false); - } else if (win.hostedConfig.authType === AuthType.ConnectionString) { - // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login - window.authType = AuthType.EncryptedToken; - // Impossible to tell if this is a try cosmos sub using an encrypted token - explorer.isTryCosmosDBSubscription(false); - updateUserContext({ - accessToken: encodeURIComponent(win.hostedConfig.encryptedToken) - }); - - const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( - win.hostedConfig.encryptedTokenMetadata.apiKind - ); - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: "", - // id: Main._databaseAccountId, - name: win.hostedConfig.encryptedTokenMetadata.accountName, - kind: getDatabaseAccountKindFromExperience(apiExperience), - properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), - tags: [] - }, - subscriptionId: undefined, - resourceGroup: undefined, - masterKey: win.hostedConfig.masterKey, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() - }); - explorer.isAccountReady(true); - } else if (win.hostedConfig.authType === AuthType.AAD) { - window.authType = AuthType.AAD; - const account = win.hostedConfig.databaseAccount; - const accountResourceId = account.id; - const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; - const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; - updateUserContext({ - authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, - databaseAccount: win.hostedConfig.databaseAccount - }); - const keys = await listKeys(subscriptionId, resourceGroup, account.name); - explorer.initDataExplorerWithFrameInputs({ - databaseAccount: account, - subscriptionId, - resourceGroup, - masterKey: keys.primaryMasterKey, - hasWriteAccess: true, //TODO: 425017 - support read access - authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, - features: extractFeatures(), - csmEndpoint: undefined, - dnsSuffix: undefined, - serverId: ServerIds.productionPortal, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() - }); - explorer.isAccountReady(true); - } - } else if (config.platform === Platform.Emulator) { - window.authType = AuthType.MasterKey; - explorer = new Explorer(); - explorer.selfServeType(SelfServeType.none); - explorer.databaseAccount(emulatorAccount); - explorer.isAccountReady(true); - } else if (config.platform === Platform.Portal) { - explorer = new Explorer(); - - // In development mode, try to load the iframe message from session storage. - // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { - const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); - if (initMessage) { - const message = JSON.parse(initMessage); - console.warn("Loaded cached portal iframe message from session storage"); - console.dir(message); - explorer.initDataExplorerWithFrameInputs(message); - } - } - - window.addEventListener("message", explorer.handleMessage.bind(explorer), false); - } - applyExplorerBindings(explorer); - }); - }, []); - - return ( -
-
-
- {/* Main Command Bar - Start */} -
- {/* Main Command Bar - End */} - {/* Share url flyout - Start */} -
-
-
- - Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read - only access urls below to share with others. For security purposes, the URLs grant time-bound access to - the account. When access expires, you can reconnect, using a valid connection string for the account. - -
-
-
- - - Read-Write - -
-
- - - Read - -
-
-
- - - Copy link - - -
-
-
-
- {/* Share url flyout - End */} - {/* Collections Tree and Tabs - Begin */} -
- {/* Collections Tree - Start */} -
-
- {/* Collections Tree Expanded - Start */} -
- {/* Collections Window - - Start */} -
- {/* Collections Window Title/Command Bar - Start */} -
-
- -
- - - - - Hide - -
-
-
-
-
-
- {/* Collections Window - End */} -
- {/* Collections Tree Expanded - End */} - {/* Collections Tree Collapsed - Start */} -
-
-
    -
  • - - Expand - - - - -
  • -
-
-
- {/* Collections Tree Collapsed - End */} -
- {/* Splitter - Start */} -
- {/* Splitter - End */} -
- {/* Collections Tree - End */} -
- -
- -
-
-
- {/* Collections Tree and Tabs - End */} - - {/* Global loader - Start */} - -
-
-
-
-

- Azure Cosmos DB -

-

- Welcome to Azure Cosmos DB -

- -
-
-
- {/* Global loader - End */} -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - -
- - -
- - {/* Global access token expiration dialog - Start */} -
-
-

Please reconnect to the account using the connection string.

-
-
- {/* Global access token expiration dialog - End */} - {/* Context switch prompt - Start */} -
-
-

- Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current - Data Explorer tabs will be closed. -

-

Proceed anyway?

-
-
-
-
-
- ); -}; - -ReactDOM.render(, document.body); +// CSS Dependencies +import "bootstrap/dist/css/bootstrap.css"; +import "../less/documentDB.less"; +import "../less/tree.less"; +import "../less/forms.less"; +import "../less/menus.less"; +import "../less/infobox.less"; +import "../less/messagebox.less"; +import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; +import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; +import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; +import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; +import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; +import "./Explorer/Controls/DynamicList/DynamicListComponent.less"; +import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; +import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; +import "../less/TableStyles/queryBuilder.less"; +import "../externals/jquery.dataTables.min.css"; +import "../less/TableStyles/fulldatatables.less"; +import "../less/TableStyles/EntityEditor.less"; +import "../less/TableStyles/CustomizeColumns.less"; +import "../less/resourceTree.less"; +import "../externals/jquery.typeahead.min.css"; +import "../externals/jquery-ui.min.css"; +import "../externals/jquery-ui.structure.min.css"; +import "../externals/jquery-ui.theme.min.css"; +import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less"; +import "./Explorer/Panes/GraphNewVertexPane.less"; +import "./Explorer/Tabs/QueryTab.less"; +import "./Explorer/Controls/TreeComponent/treeComponent.less"; +import "./Explorer/Controls/Accordion/AccordionComponent.less"; +import "./Explorer/SplashScreen/SplashScreenComponent.less"; +import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; + +// Image Dependencies +import "../images/CosmosDB_rgb_ui_lighttheme.ico"; +import "../images/favicon.ico"; + +import "./Shared/appInsights"; +import "babel-polyfill"; +import "es6-symbol/implement"; +import "webcrypto-liner/build/webcrypto-liner.shim.min"; +import "./Libs/jquery"; +import "bootstrap/dist/js/npm"; +import "../externals/jquery.typeahead.min.js"; +import "../externals/jquery-ui.min.js"; +import "promise-polyfill/src/polyfill"; +import "abort-controller/polyfill"; +import "whatwg-fetch"; +import "es6-object-assign/auto"; +import "promise.prototype.finally/auto"; +import "object.entries/auto"; +import "./Libs/is-integer-polyfill"; +import "url-polyfill/url-polyfill.min"; + +initializeIcons(); + +import { AuthType } from "./AuthType"; + +import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; +import { applyExplorerBindings } from "./applyExplorerBindings"; +import { configContext, initializeConfiguration, Platform } from "./ConfigContext"; +import Explorer from "./Explorer/Explorer"; +import React, { useEffect } from "react"; +import ReactDOM from "react-dom"; +import copyImage from "../images/Copy.svg"; +import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; +import refreshImg from "../images/refresh-cosmos.svg"; +import arrowLeftImg from "../images/imgarrowlefticon.svg"; +import { KOCommentEnd, KOCommentIfStart } from "./koComment"; +import { updateUserContext } from "./UserContext"; +import { CollectionCreation } from "./Shared/Constants"; +import { extractFeatures } from "./Platform/Hosted/extractFeatures"; +import { emulatorAccount } from "./Platform/Emulator/emulatorAccount"; +import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; +import { + getDatabaseAccountKindFromExperience, + getDatabaseAccountPropertiesFromMetadata, +} from "./Platform/Hosted/HostedUtils"; +import { DefaultExperienceUtility } from "./Shared/DefaultExperienceUtility"; +import { parseResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils"; +import { AccountKind, DefaultAccountExperience, ServerIds } from "./Common/Constants"; +import { listKeys } from "./Utils/arm/generatedClients/2020-04-01/databaseAccounts"; +import { SelfServeType } from "./SelfServe/SelfServeUtils"; + +const App: React.FunctionComponent = () => { + useEffect(() => { + initializeConfiguration().then(async (config) => { + let explorer: Explorer; + if (config.platform === Platform.Hosted) { + const win = (window as unknown) as HostedExplorerChildFrame; + explorer = new Explorer(); + explorer.selfServeType(SelfServeType.none); + if (win.hostedConfig.authType === AuthType.EncryptedToken) { + // TODO: Remove window.authType + window.authType = AuthType.EncryptedToken; + // Impossible to tell if this is a try cosmos sub using an encrypted token + explorer.isTryCosmosDBSubscription(false); + updateUserContext({ + accessToken: encodeURIComponent(win.hostedConfig.encryptedToken), + }); + + const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( + win.hostedConfig.encryptedTokenMetadata.apiKind + ); + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: { + id: "", + // id: Main._databaseAccountId, + name: win.hostedConfig.encryptedTokenMetadata.accountName, + kind: getDatabaseAccountKindFromExperience(apiExperience), + properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), + tags: [], + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: undefined, + hasWriteAccess: true, // TODO: we should embed this information in the token ideally + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + }); + explorer.isAccountReady(true); + } else if (win.hostedConfig.authType === AuthType.ResourceToken) { + window.authType = AuthType.ResourceToken; + // Resource tokens can only be used with SQL API + const apiExperience: string = DefaultAccountExperience.DocumentDB; + const parsedResourceToken = parseResourceTokenConnectionString(win.hostedConfig.resourceToken); + updateUserContext({ + resourceToken: parsedResourceToken.resourceToken, + endpoint: parsedResourceToken.accountEndpoint, + }); + explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); + explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); + if (parsedResourceToken.partitionKey) { + explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); + } + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: { + id: "", + name: parsedResourceToken.accountEndpoint, + kind: AccountKind.GlobalDocumentDB, + properties: { documentEndpoint: parsedResourceToken.accountEndpoint }, + tags: { defaultExperience: apiExperience }, + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: undefined, + hasWriteAccess: true, // TODO: we should embed this information in the token ideally + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + isAuthWithresourceToken: true, + }); + explorer.isAccountReady(true); + explorer.isRefreshingExplorer(false); + } else if (win.hostedConfig.authType === AuthType.ConnectionString) { + // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login + window.authType = AuthType.EncryptedToken; + // Impossible to tell if this is a try cosmos sub using an encrypted token + explorer.isTryCosmosDBSubscription(false); + updateUserContext({ + accessToken: encodeURIComponent(win.hostedConfig.encryptedToken), + }); + + const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( + win.hostedConfig.encryptedTokenMetadata.apiKind + ); + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: { + id: "", + // id: Main._databaseAccountId, + name: win.hostedConfig.encryptedTokenMetadata.accountName, + kind: getDatabaseAccountKindFromExperience(apiExperience), + properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), + tags: [], + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: win.hostedConfig.masterKey, + hasWriteAccess: true, // TODO: we should embed this information in the token ideally + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + }); + explorer.isAccountReady(true); + } else if (win.hostedConfig.authType === AuthType.AAD) { + window.authType = AuthType.AAD; + const account = win.hostedConfig.databaseAccount; + const accountResourceId = account.id; + const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; + const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; + updateUserContext({ + authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, + databaseAccount: win.hostedConfig.databaseAccount, + }); + const keys = await listKeys(subscriptionId, resourceGroup, account.name); + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: account, + subscriptionId, + resourceGroup, + masterKey: keys.primaryMasterKey, + hasWriteAccess: true, //TODO: 425017 - support read access + authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + }); + explorer.isAccountReady(true); + } + } else if (config.platform === Platform.Emulator) { + window.authType = AuthType.MasterKey; + explorer = new Explorer(); + explorer.selfServeType(SelfServeType.none); + explorer.databaseAccount(emulatorAccount); + explorer.isAccountReady(true); + } else if (config.platform === Platform.Portal) { + explorer = new Explorer(); + + // In development mode, try to load the iframe message from session storage. + // This allows webpack hot reload to funciton properly + if (process.env.NODE_ENV === "development") { + const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); + if (initMessage) { + const message = JSON.parse(initMessage); + console.warn("Loaded cached portal iframe message from session storage"); + console.dir(message); + explorer.initDataExplorerWithFrameInputs(message); + } + } + + window.addEventListener("message", explorer.handleMessage.bind(explorer), false); + } + applyExplorerBindings(explorer); + }); + }, []); + + return ( +
+
+
+ {/* Main Command Bar - Start */} +
+ {/* Main Command Bar - End */} + {/* Share url flyout - Start */} +
+
+
+ + Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read + only access urls below to share with others. For security purposes, the URLs grant time-bound access to + the account. When access expires, you can reconnect, using a valid connection string for the account. + +
+
+
+ + + Read-Write + +
+
+ + + Read + +
+
+
+ + + Copy link + + +
+
+
+
+ {/* Share url flyout - End */} + {/* Collections Tree and Tabs - Begin */} +
+ {/* Collections Tree - Start */} +
+
+ {/* Collections Tree Expanded - Start */} +
+ {/* Collections Window - - Start */} +
+ {/* Collections Window Title/Command Bar - Start */} +
+
+ +
+ + + + + Hide + +
+
+
+
+
+
+ {/* Collections Window - End */} +
+ {/* Collections Tree Expanded - End */} + {/* Collections Tree Collapsed - Start */} +
+
+
    +
  • + + Expand + + + + +
  • +
+
+
+ {/* Collections Tree Collapsed - End */} +
+ {/* Splitter - Start */} +
+ {/* Splitter - End */} +
+ {/* Collections Tree - End */} +
+
+
+ +
+
+
+ {/* Collections Tree and Tabs - End */} + + {/* Global loader - Start */} + +
+
+
+
+

+ Azure Cosmos DB +

+

+ Welcome to Azure Cosmos DB +

+ +
+
+
+ {/* Global loader - End */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ + +
+ + {/* Global access token expiration dialog - Start */} +
+
+

Please reconnect to the account using the connection string.

+
+
+ {/* Global access token expiration dialog - End */} + {/* Context switch prompt - Start */} +
+
+

+ Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current + Data Explorer tabs will be closed. +

+

Proceed anyway?

+
+
+
+
+
+ ); +}; + +ReactDOM.render(, document.body); diff --git a/src/NotebookViewer/NotebookViewer.tsx b/src/NotebookViewer/NotebookViewer.tsx index e9fca19bb..050da4903 100644 --- a/src/NotebookViewer/NotebookViewer.tsx +++ b/src/NotebookViewer/NotebookViewer.tsx @@ -5,7 +5,7 @@ import * as ReactDOM from "react-dom"; import { initializeConfiguration, configContext } from "../ConfigContext"; import { NotebookViewerComponent, - NotebookViewerComponentProps + NotebookViewerComponentProps, } from "../Explorer/Controls/NotebookViewer/NotebookViewerComponent"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import * as GalleryUtils from "../Utils/GalleryUtils"; @@ -59,7 +59,7 @@ const render = ( hideInputs, hidePrompts, onBackClick: onBackClick, - onTagClick: undefined + onTagClick: undefined, }; if (galleryItem) { diff --git a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts b/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts index 6b3d36f3f..171cb5cf4 100644 --- a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts +++ b/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts @@ -1,98 +1,98 @@ -import { ArmApiVersions } from "../Common/Constants"; -import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient"; -import * as Logger from "../Common/Logger"; -import { - NotebookWorkspace, - NotebookWorkspaceConnectionInfo, - NotebookWorkspaceFeedResponse -} from "../Contracts/DataModels"; -import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; - -export class NotebookWorkspaceManager { - private resourceProviderClientFactory: IResourceProviderClientFactory; - - constructor() { - this.resourceProviderClientFactory = new ResourceProviderClientFactory(); - } - - public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise { - const uri = `${cosmosdbResourceId}/notebookWorkspaces`; - try { - const response = (await this.rpClient(uri).getAsync( - uri, - ArmApiVersions.documentDB - )) as NotebookWorkspaceFeedResponse; - return response && response.value; - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspacesAsync"); - throw error; - } - } - - public async getNotebookWorkspaceAsync( - cosmosdbResourceId: string, - notebookWorkspaceId: string - ): Promise { - const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`; - try { - return (await this.rpClient(uri).getAsync(uri, ArmApiVersions.documentDB)) as NotebookWorkspace; - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspaceAsync"); - throw error; - } - } - - public async createNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise { - const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`; - try { - await this.rpClient(uri).putAsync(uri, ArmApiVersions.documentDB, { name: notebookWorkspaceId }); - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/createNotebookWorkspaceAsync"); - throw error; - } - } - - public async deleteNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise { - const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`; - try { - await this.rpClient(uri).deleteAsync(uri, ArmApiVersions.documentDB); - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/deleteNotebookWorkspaceAsync"); - throw error; - } - } - - public async getNotebookConnectionInfoAsync( - cosmosdbResourceId: string, - notebookWorkspaceId: string - ): Promise { - const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}/listConnectionInfo`; - try { - return await this.rpClient(uri).postAsync( - uri, - ArmApiVersions.documentDB, - undefined - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookConnectionInfoAsync"); - throw error; - } - } - - public async startNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise { - const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}/start`; - try { - return await this.rpClient(uri).postAsync(uri, ArmApiVersions.documentDB, undefined, { - skipResourceValidation: true - }); - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/startNotebookWorkspaceAsync"); - throw error; - } - } - - private rpClient(uri: string): IResourceProviderClient { - return this.resourceProviderClientFactory.getOrCreate(uri); - } -} +import { ArmApiVersions } from "../Common/Constants"; +import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient"; +import * as Logger from "../Common/Logger"; +import { + NotebookWorkspace, + NotebookWorkspaceConnectionInfo, + NotebookWorkspaceFeedResponse, +} from "../Contracts/DataModels"; +import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; + +export class NotebookWorkspaceManager { + private resourceProviderClientFactory: IResourceProviderClientFactory; + + constructor() { + this.resourceProviderClientFactory = new ResourceProviderClientFactory(); + } + + public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise { + const uri = `${cosmosdbResourceId}/notebookWorkspaces`; + try { + const response = (await this.rpClient(uri).getAsync( + uri, + ArmApiVersions.documentDB + )) as NotebookWorkspaceFeedResponse; + return response && response.value; + } catch (error) { + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspacesAsync"); + throw error; + } + } + + public async getNotebookWorkspaceAsync( + cosmosdbResourceId: string, + notebookWorkspaceId: string + ): Promise { + const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`; + try { + return (await this.rpClient(uri).getAsync(uri, ArmApiVersions.documentDB)) as NotebookWorkspace; + } catch (error) { + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspaceAsync"); + throw error; + } + } + + public async createNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise { + const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`; + try { + await this.rpClient(uri).putAsync(uri, ArmApiVersions.documentDB, { name: notebookWorkspaceId }); + } catch (error) { + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/createNotebookWorkspaceAsync"); + throw error; + } + } + + public async deleteNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise { + const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`; + try { + await this.rpClient(uri).deleteAsync(uri, ArmApiVersions.documentDB); + } catch (error) { + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/deleteNotebookWorkspaceAsync"); + throw error; + } + } + + public async getNotebookConnectionInfoAsync( + cosmosdbResourceId: string, + notebookWorkspaceId: string + ): Promise { + const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}/listConnectionInfo`; + try { + return await this.rpClient(uri).postAsync( + uri, + ArmApiVersions.documentDB, + undefined + ); + } catch (error) { + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookConnectionInfoAsync"); + throw error; + } + } + + public async startNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise { + const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}/start`; + try { + return await this.rpClient(uri).postAsync(uri, ArmApiVersions.documentDB, undefined, { + skipResourceValidation: true, + }); + } catch (error) { + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/startNotebookWorkspaceAsync"); + throw error; + } + } + + private rpClient(uri: string): IResourceProviderClient { + return this.resourceProviderClientFactory.getOrCreate(uri); + } +} diff --git a/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts b/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts index 6f2e2bca1..cdb5705dc 100644 --- a/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts +++ b/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts @@ -1,50 +1,50 @@ -import { IResourceProviderClient } from "../ResourceProvider/IResourceProviderClient"; -import { NotebookWorkspace } from "../Contracts/DataModels"; - -export class NotebookWorkspaceSettingsProviderClient implements IResourceProviderClient { - public async deleteAsync(url: string, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } - - public async postAsync(url: string, body: any, apiVersion: string): Promise { - return Promise.resolve({ - notebookServerEndpoint: "http://localhost:8888", - username: "", - password: "" - }); - } - - public async getAsync(url: string, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } - - public async putAsync(url: string, body: any, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } - - public async patchAsync(url: string, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } -} - -export class NotebookWorkspaceResourceProviderClient implements IResourceProviderClient { - public async deleteAsync(url: string, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } - - public async postAsync(url: string, body: any, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } - - public async getAsync(url: string, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } - - public async putAsync(url: string, body: any, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } - - public async patchAsync(url: string, body: any, apiVersion: string): Promise { - throw new Error("Not yet implemented"); - } -} +import { IResourceProviderClient } from "../ResourceProvider/IResourceProviderClient"; +import { NotebookWorkspace } from "../Contracts/DataModels"; + +export class NotebookWorkspaceSettingsProviderClient implements IResourceProviderClient { + public async deleteAsync(url: string, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } + + public async postAsync(url: string, body: any, apiVersion: string): Promise { + return Promise.resolve({ + notebookServerEndpoint: "http://localhost:8888", + username: "", + password: "", + }); + } + + public async getAsync(url: string, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } + + public async putAsync(url: string, body: any, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } + + public async patchAsync(url: string, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } +} + +export class NotebookWorkspaceResourceProviderClient implements IResourceProviderClient { + public async deleteAsync(url: string, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } + + public async postAsync(url: string, body: any, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } + + public async getAsync(url: string, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } + + public async putAsync(url: string, body: any, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } + + public async patchAsync(url: string, body: any, apiVersion: string): Promise { + throw new Error("Not yet implemented"); + } +} diff --git a/src/Platform/Emulator/emulatorAccount.tsx b/src/Platform/Emulator/emulatorAccount.tsx index 1ff4437c8..5042a53a6 100644 --- a/src/Platform/Emulator/emulatorAccount.tsx +++ b/src/Platform/Emulator/emulatorAccount.tsx @@ -7,12 +7,12 @@ export const emulatorAccount = { type: "", kind: AccountKind.DocumentDB, tags: { - [TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB + [TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB, }, properties: { documentEndpoint: "", tableEndpoint: "", gremlinEndpoint: "", - cassandraEndpoint: "" - } + cassandraEndpoint: "", + }, }; diff --git a/src/Platform/Hosted/Authorization.ts b/src/Platform/Hosted/Authorization.ts index 06531d932..2aaac31ad 100644 --- a/src/Platform/Hosted/Authorization.ts +++ b/src/Platform/Hosted/Authorization.ts @@ -17,7 +17,7 @@ export default class AuthHeadersUtil { type: "POST", headers: headers, contentType: "application/json", - cache: false + cache: false, }); } @@ -37,7 +37,7 @@ export default class AuthHeadersUtil { type: "POST", headers: headers, contentType: "application/json", - cache: false + cache: false, }); } diff --git a/src/Platform/Hosted/Components/AccountSwitcher.test.tsx b/src/Platform/Hosted/Components/AccountSwitcher.test.tsx index 51f1fc687..51e3c47f2 100644 --- a/src/Platform/Hosted/Components/AccountSwitcher.test.tsx +++ b/src/Platform/Hosted/Components/AccountSwitcher.test.tsx @@ -13,7 +13,7 @@ it("calls setAccount from parent component", () => { const setDatabaseAccount = jest.fn(); const subscriptions = [ { subscriptionId: "testSub1", displayName: "Test Sub 1" }, - { subscriptionId: "testSub2", displayName: "Test Sub 2" } + { subscriptionId: "testSub2", displayName: "Test Sub 2" }, ] as Subscription[]; (useSubscriptions as jest.Mock).mockReturnValue(subscriptions); const accounts = [{ name: "testAccount1" }, { name: "testAccount2" }] as DatabaseAccount[]; diff --git a/src/Platform/Hosted/Components/AccountSwitcher.tsx b/src/Platform/Hosted/Components/AccountSwitcher.tsx index da28c5eac..b3c8ddd26 100644 --- a/src/Platform/Hosted/Components/AccountSwitcher.tsx +++ b/src/Platform/Hosted/Components/AccountSwitcher.tsx @@ -20,27 +20,27 @@ const buttonStyles: IButtonStyles = { paddingLeft: 10, marginRight: 5, backgroundColor: StyleConstants.BaseDark, - color: StyleConstants.BaseLight + color: StyleConstants.BaseLight, }, rootHovered: { backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight + color: StyleConstants.BaseLight, }, rootFocused: { backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight + color: StyleConstants.BaseLight, }, rootPressed: { backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight + color: StyleConstants.BaseLight, }, rootExpanded: { backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight + color: StyleConstants.BaseLight, }, textContainer: { - flexGrow: "initial" - } + flexGrow: "initial", + }, }; interface Props { @@ -53,12 +53,12 @@ export const AccountSwitcher: FunctionComponent = ({ armToken, setDatabas const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(() => localStorage.getItem("cachedSubscriptionId") ); - const selectedSubscription = subscriptions?.find(sub => sub.subscriptionId === selectedSubscriptionId); + const selectedSubscription = subscriptions?.find((sub) => sub.subscriptionId === selectedSubscriptionId); const accounts = useDatabaseAccounts(selectedSubscription?.subscriptionId, armToken); const [selectedAccountName, setSelectedAccountName] = useState(() => localStorage.getItem("cachedDatabaseAccountName") ); - const selectedAccount = accounts?.find(account => account.name === selectedAccountName); + const selectedAccount = accounts?.find((account) => account.name === selectedAccountName); useEffect(() => { if (selectedAccountName) { @@ -83,14 +83,14 @@ export const AccountSwitcher: FunctionComponent = ({ armToken, setDatabas const items: IContextualMenuItem[] = [ { key: "switchSubscription", - onRender: () => + onRender: () => , }, { key: "switchAccount", onRender: (_, dismissMenu) => ( - ) - } + ), + }, ]; return ( @@ -99,7 +99,7 @@ export const AccountSwitcher: FunctionComponent = ({ armToken, setDatabas menuProps={{ directionalHintFixed: true, className: "accountSwitchContextualMenu", - items + items, }} styles={buttonStyles} className="accountSwitchButton" diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index bd9c99275..35fc60d9b 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -19,7 +19,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ login, setAuthType, connectionString, - setConnectionString + setConnectionString, }: Props) => { const [isFormVisible, { setTrue: showForm }] = useBoolean(false); @@ -34,7 +34,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ {isFormVisible ? (
{ + onSubmit={async (event) => { event.preventDefault(); if (isResourceTokenConnectionString(connectionString)) { @@ -63,7 +63,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ required placeholder="Please enter a connection string" value={connectionString} - onChange={event => { + onChange={(event) => { setConnectionString(event.target.value); }} /> diff --git a/src/Platform/Hosted/Components/DirectoryPickerPanel.test.tsx b/src/Platform/Hosted/Components/DirectoryPickerPanel.test.tsx index a734bef71..711093443 100644 --- a/src/Platform/Hosted/Components/DirectoryPickerPanel.test.tsx +++ b/src/Platform/Hosted/Components/DirectoryPickerPanel.test.tsx @@ -12,7 +12,7 @@ it("switches tenant for user", () => { const dismissPanel = jest.fn(); const directories = [ { displayName: "test1", tenantId: "test1-id" }, - { displayName: "test2", tenantId: "test2-id" } + { displayName: "test2", tenantId: "test2-id" }, ] as Tenant[]; (useDirectories as jest.Mock).mockReturnValue(directories); diff --git a/src/Platform/Hosted/Components/DirectoryPickerPanel.tsx b/src/Platform/Hosted/Components/DirectoryPickerPanel.tsx index 41ddc9688..128a5e52d 100644 --- a/src/Platform/Hosted/Components/DirectoryPickerPanel.tsx +++ b/src/Platform/Hosted/Components/DirectoryPickerPanel.tsx @@ -15,7 +15,7 @@ export const DirectoryPickerPanel: React.FunctionComponent = ({ dismissPanel, armToken, tenantId, - switchTenant + switchTenant, }: Props) => { const directories = useDirectories(armToken); return ( @@ -27,7 +27,7 @@ export const DirectoryPickerPanel: React.FunctionComponent = ({ closeButtonAriaLabel="Close" > ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))} + options={directories.map((dir) => ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))} selectedKey={tenantId} onChange={(event, option) => { switchTenant(option.key); diff --git a/src/Platform/Hosted/Components/MeControl.tsx b/src/Platform/Hosted/Components/MeControl.tsx index 43677cc71..6740b8b61 100644 --- a/src/Platform/Hosted/Components/MeControl.tsx +++ b/src/Platform/Hosted/Components/MeControl.tsx @@ -4,7 +4,7 @@ import { DirectionalHint, Persona, PersonaInitialsColor, - PersonaSize + PersonaSize, } from "office-ui-fabric-react"; import * as React from "react"; import { Account } from "msal"; @@ -30,26 +30,26 @@ export const MeControl: React.FunctionComponent = ({ openPanel, logout, a directionalHintFixed: true, directionalHint: DirectionalHint.bottomRightEdge, calloutProps: { - minPagePadding: 0 + minPagePadding: 0, }, items: [ { key: "SwitchDirectory", text: "Switch Directory", - onClick: openPanel + onClick: openPanel, }, { key: "SignOut", text: "Sign Out", - onClick: logout - } - ] + onClick: logout, + }, + ], }} styles={{ rootHovered: { backgroundColor: "#393939" }, rootFocused: { backgroundColor: "#393939" }, rootPressed: { backgroundColor: "#393939" }, - rootExpanded: { backgroundColor: "#393939" } + rootExpanded: { backgroundColor: "#393939" }, }} > = ({ login }: Props) = styles={{ rootHovered: { backgroundColor: "#393939", color: "#fff" }, rootFocused: { backgroundColor: "#393939", color: "#fff" }, - rootPressed: { backgroundColor: "#393939", color: "#fff" } + rootPressed: { backgroundColor: "#393939", color: "#fff" }, }} /> ); diff --git a/src/Platform/Hosted/Components/SwitchAccount.tsx b/src/Platform/Hosted/Components/SwitchAccount.tsx index c3d98a6a8..204f64b78 100644 --- a/src/Platform/Hosted/Components/SwitchAccount.tsx +++ b/src/Platform/Hosted/Components/SwitchAccount.tsx @@ -14,16 +14,16 @@ export const SwitchAccount: FunctionComponent = ({ accounts, setSelectedAccountName, selectedAccount, - dismissMenu + dismissMenu, }: Props) => { return ( ({ + options={accounts?.map((account) => ({ key: account.name, text: account.name, - data: account + data: account, }))} onChange={(_, option) => { setSelectedAccountName(String(option.key)); @@ -32,7 +32,7 @@ export const SwitchAccount: FunctionComponent = ({ defaultSelectedKey={selectedAccount?.name} placeholder={accounts && accounts.length === 0 ? "No Accounts Found" : "Select an Account"} styles={{ - callout: "accountSwitchAccountDropdownMenu" + callout: "accountSwitchAccountDropdownMenu", }} /> ); diff --git a/src/Platform/Hosted/Components/SwitchSubscription.tsx b/src/Platform/Hosted/Components/SwitchSubscription.tsx index e8ae29e94..f6b404bda 100644 --- a/src/Platform/Hosted/Components/SwitchSubscription.tsx +++ b/src/Platform/Hosted/Components/SwitchSubscription.tsx @@ -12,17 +12,17 @@ interface Props { export const SwitchSubscription: FunctionComponent = ({ subscriptions, setSelectedSubscriptionId, - selectedSubscription + selectedSubscription, }: Props) => { return ( { + options={subscriptions?.map((sub) => { return { key: sub.subscriptionId, text: sub.displayName, - data: sub + data: sub, }; })} onChange={(_, option) => { @@ -31,7 +31,7 @@ export const SwitchSubscription: FunctionComponent = ({ defaultSelectedKey={selectedSubscription?.subscriptionId} placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"} styles={{ - callout: "accountSwitchSubscriptionDropdownMenu" + callout: "accountSwitchSubscriptionDropdownMenu", }} /> ); diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts index da534c045..8762171e7 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts @@ -1,73 +1,73 @@ -import * as DataModels from "../../../Contracts/DataModels"; -import { parseConnectionString } from "./ConnectionStringParser"; - -describe("ConnectionStringParser", () => { - const mockAccountName: string = "Test"; - const mockMasterKey: string = "some-key"; - - it("should parse a valid sql account connection string", () => { - const metadata = parseConnectionString( - `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};` - ); - - expect(metadata.accountName).toBe(mockAccountName); - expect(metadata.apiKind).toBe(DataModels.ApiKind.SQL); - }); - - it("should parse a valid mongo account connection string", () => { - const metadata = parseConnectionString( - `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255` - ); - - expect(metadata.accountName).toBe(mockAccountName); - expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDB); - }); - - it("should parse a valid compute mongo account connection string", () => { - const metadata = parseConnectionString( - `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255` - ); - - expect(metadata.accountName).toBe(mockAccountName); - expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDBCompute); - }); - - it("should parse a valid graph account connection string", () => { - const metadata = parseConnectionString( - `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;` - ); - - expect(metadata.accountName).toBe(mockAccountName); - expect(metadata.apiKind).toBe(DataModels.ApiKind.Graph); - }); - - it("should parse a valid table account connection string", () => { - const metadata = parseConnectionString( - `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;` - ); - - expect(metadata.accountName).toBe(mockAccountName); - expect(metadata.apiKind).toBe(DataModels.ApiKind.Table); - }); - - it("should parse a valid cassandra account connection string", () => { - const metadata = parseConnectionString( - `AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};` - ); - - expect(metadata.accountName).toBe(mockAccountName); - expect(metadata.apiKind).toBe(DataModels.ApiKind.Cassandra); - }); - - it("should fail to parse an invalid connection string", () => { - const metadata = parseConnectionString("some-rogue-connection-string"); - - expect(metadata).toBe(undefined); - }); - - it("should fail to parse an empty connection string", () => { - const metadata = parseConnectionString(""); - - expect(metadata).toBe(undefined); - }); -}); +import * as DataModels from "../../../Contracts/DataModels"; +import { parseConnectionString } from "./ConnectionStringParser"; + +describe("ConnectionStringParser", () => { + const mockAccountName: string = "Test"; + const mockMasterKey: string = "some-key"; + + it("should parse a valid sql account connection string", () => { + const metadata = parseConnectionString( + `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};` + ); + + expect(metadata.accountName).toBe(mockAccountName); + expect(metadata.apiKind).toBe(DataModels.ApiKind.SQL); + }); + + it("should parse a valid mongo account connection string", () => { + const metadata = parseConnectionString( + `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255` + ); + + expect(metadata.accountName).toBe(mockAccountName); + expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDB); + }); + + it("should parse a valid compute mongo account connection string", () => { + const metadata = parseConnectionString( + `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255` + ); + + expect(metadata.accountName).toBe(mockAccountName); + expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDBCompute); + }); + + it("should parse a valid graph account connection string", () => { + const metadata = parseConnectionString( + `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;` + ); + + expect(metadata.accountName).toBe(mockAccountName); + expect(metadata.apiKind).toBe(DataModels.ApiKind.Graph); + }); + + it("should parse a valid table account connection string", () => { + const metadata = parseConnectionString( + `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;` + ); + + expect(metadata.accountName).toBe(mockAccountName); + expect(metadata.apiKind).toBe(DataModels.ApiKind.Table); + }); + + it("should parse a valid cassandra account connection string", () => { + const metadata = parseConnectionString( + `AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};` + ); + + expect(metadata.accountName).toBe(mockAccountName); + expect(metadata.apiKind).toBe(DataModels.ApiKind.Cassandra); + }); + + it("should fail to parse an invalid connection string", () => { + const metadata = parseConnectionString("some-rogue-connection-string"); + + expect(metadata).toBe(undefined); + }); + + it("should fail to parse an empty connection string", () => { + const metadata = parseConnectionString(""); + + expect(metadata).toBe(undefined); + }); +}); diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts index 861f724d1..6c8db5f96 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts @@ -1,48 +1,48 @@ -import * as Constants from "../../../Common/Constants"; -import { AccessInputMetadata, ApiKind } from "../../../Contracts/DataModels"; - -export function parseConnectionString(connectionString: string): AccessInputMetadata { - if (connectionString) { - try { - const accessInput = {} as AccessInputMetadata; - const connectionStringParts = connectionString.split(";"); - - connectionStringParts.forEach((connectionStringPart: string) => { - if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) { - accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1]; - accessInput.apiKind = ApiKind.SQL; - } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) { - const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo); - accessInput.accountName = matches && matches.length > 1 && matches[2]; - accessInput.apiKind = ApiKind.MongoDB; - } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) { - const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute); - accessInput.accountName = matches && matches.length > 1 && matches[2]; - accessInput.apiKind = ApiKind.MongoDBCompute; - } else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) { - Constants.EndpointsRegex.cassandra.forEach(regex => { - if (RegExp(regex).test(connectionStringPart)) { - accessInput.accountName = connectionStringPart.match(regex)[1]; - accessInput.apiKind = ApiKind.Cassandra; - } - }); - } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) { - accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1]; - accessInput.apiKind = ApiKind.Table; - } else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) { - accessInput.apiKind = ApiKind.Graph; - } - }); - - if (Object.keys(accessInput).length === 0) { - return undefined; - } - - return accessInput; - } catch (error) { - return undefined; - } - } - - return undefined; -} +import * as Constants from "../../../Common/Constants"; +import { AccessInputMetadata, ApiKind } from "../../../Contracts/DataModels"; + +export function parseConnectionString(connectionString: string): AccessInputMetadata { + if (connectionString) { + try { + const accessInput = {} as AccessInputMetadata; + const connectionStringParts = connectionString.split(";"); + + connectionStringParts.forEach((connectionStringPart: string) => { + if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) { + accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1]; + accessInput.apiKind = ApiKind.SQL; + } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) { + const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo); + accessInput.accountName = matches && matches.length > 1 && matches[2]; + accessInput.apiKind = ApiKind.MongoDB; + } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) { + const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute); + accessInput.accountName = matches && matches.length > 1 && matches[2]; + accessInput.apiKind = ApiKind.MongoDBCompute; + } else if (Constants.EndpointsRegex.cassandra.some((regex) => RegExp(regex).test(connectionStringPart))) { + Constants.EndpointsRegex.cassandra.forEach((regex) => { + if (RegExp(regex).test(connectionStringPart)) { + accessInput.accountName = connectionStringPart.match(regex)[1]; + accessInput.apiKind = ApiKind.Cassandra; + } + }); + } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) { + accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1]; + accessInput.apiKind = ApiKind.Table; + } else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) { + accessInput.apiKind = ApiKind.Graph; + } + }); + + if (Object.keys(accessInput).length === 0) { + return undefined; + } + + return accessInput; + } catch (error) { + return undefined; + } + } + + return undefined; +} diff --git a/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts b/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts index 12cc90706..61ceb206b 100644 --- a/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts +++ b/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts @@ -11,7 +11,7 @@ describe("parseResourceTokenConnectionString", () => { collectionId: "fakeCollectionId", databaseId: "fakeDatabaseId", partitionKey: undefined, - resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;" + resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;", }); }); @@ -25,7 +25,7 @@ describe("parseResourceTokenConnectionString", () => { collectionId: "fakeCollectionId", databaseId: "fakeDatabaseId", partitionKey: "fakePartitionKey", - resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;" + resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;", }); }); }); diff --git a/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts index 8fb6f51c7..b55014c23 100644 --- a/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts +++ b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts @@ -34,7 +34,7 @@ export function parseResourceTokenConnectionString(connectionString: string): Pa collectionId, databaseId, partitionKey, - resourceToken + resourceToken, }; } diff --git a/src/Platform/Hosted/HostedUtils.test.ts b/src/Platform/Hosted/HostedUtils.test.ts index 3ec2ba99a..1b8ddd666 100644 --- a/src/Platform/Hosted/HostedUtils.test.ts +++ b/src/Platform/Hosted/HostedUtils.test.ts @@ -9,11 +9,11 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => { apiKind: 5, documentEndpoint: "https://compute-batch2.documents.azure.com:443/", expiryTimestamp: "1234", - mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/" + mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/", }; expect(getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({ mongoEndpoint: mongoComputeAccount.mongoEndpoint, - documentEndpoint: mongoComputeAccount.documentEndpoint + documentEndpoint: mongoComputeAccount.documentEndpoint, }); }); @@ -23,10 +23,10 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => { apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255", apiKind: 1, documentEndpoint: "https://compute-batch2.documents.azure.com:443/", - expiryTimestamp: "1234" + expiryTimestamp: "1234", }; expect(getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({ - documentEndpoint: mongoAccount.documentEndpoint + documentEndpoint: mongoAccount.documentEndpoint, }); }); }); diff --git a/src/Platform/Hosted/HostedUtils.ts b/src/Platform/Hosted/HostedUtils.ts index 4cf09904e..85ef302f3 100644 --- a/src/Platform/Hosted/HostedUtils.ts +++ b/src/Platform/Hosted/HostedUtils.ts @@ -9,22 +9,22 @@ export function getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMe if (apiExperience === DefaultAccountExperience.Cassandra) { properties = Object.assign(properties, { cassandraEndpoint: metadata.apiEndpoint, - capabilities: [{ name: CapabilityNames.EnableCassandra }] + capabilities: [{ name: CapabilityNames.EnableCassandra }], }); } else if (apiExperience === DefaultAccountExperience.Table) { properties = Object.assign(properties, { tableEndpoint: metadata.apiEndpoint, - capabilities: [{ name: CapabilityNames.EnableTable }] + capabilities: [{ name: CapabilityNames.EnableTable }], }); } else if (apiExperience === DefaultAccountExperience.Graph) { properties = Object.assign(properties, { gremlinEndpoint: metadata.apiEndpoint, - capabilities: [{ name: CapabilityNames.EnableGremlin }] + capabilities: [{ name: CapabilityNames.EnableGremlin }], }); } else if (apiExperience === DefaultAccountExperience.MongoDB) { if (metadata.apiKind === ApiKind.MongoDBCompute) { properties = Object.assign(properties, { - mongoEndpoint: metadata.mongoEndpoint + mongoEndpoint: metadata.mongoEndpoint, }); } } diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts index 3567134d2..c0d326fae 100644 --- a/src/Platform/Hosted/extractFeatures.test.ts +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -11,7 +11,7 @@ describe("extractFeatures", () => { expect(features).toEqual({ notebookserverurl: "https://localhost:10001/12345/notebook", notebookservertoken: "token", - enablenotebooks: "true" + enablenotebooks: "true", }); }); }); diff --git a/src/ReactDevTools.ts b/src/ReactDevTools.ts index 19512901f..09947f934 100644 --- a/src/ReactDevTools.ts +++ b/src/ReactDevTools.ts @@ -1,3 +1,3 @@ -if (window.parent !== window) { - (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; -} +if (window.parent !== window) { + (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; +} diff --git a/src/ResourceProvider/ResourceProviderClient.ts b/src/ResourceProvider/ResourceProviderClient.ts index b0828374f..4c13cb9c3 100644 --- a/src/ResourceProvider/ResourceProviderClient.ts +++ b/src/ResourceProvider/ResourceProviderClient.ts @@ -1,217 +1,217 @@ -import * as ViewModels from "../Contracts/ViewModels"; -import { HttpStatusCodes } from "../Common/Constants"; -import { IResourceProviderClient, IResourceProviderRequestOptions } from "./IResourceProviderClient"; -import { OperationStatus } from "../Contracts/DataModels"; -import { TokenProviderFactory } from "../TokenProviders/TokenProviderFactory"; -import UrlUtility from "../Common/UrlUtility"; - -export class ResourceProviderClient implements IResourceProviderClient { - private httpClient: HttpClient; - - constructor(private armEndpoint: string) { - this.httpClient = new HttpClient(); - } - - public async getAsync( - url: string, - apiVersion: string, - queryString?: string, - requestOptions?: IResourceProviderRequestOptions - ): Promise { - let uri = `${this.armEndpoint}${url}?api-version=${apiVersion}`; - if (queryString) { - uri += `&${queryString}`; - } - return await this.httpClient.getAsync( - uri, - Object.assign({}, { skipResourceValidation: false }, requestOptions) - ); - } - - public async postAsync( - url: string, - apiVersion: string, - body: any, - requestOptions?: IResourceProviderRequestOptions - ): Promise { - const fullUrl = UrlUtility.createUri(this.armEndpoint, url); - return await this.httpClient.postAsync( - `${fullUrl}?api-version=${apiVersion}`, - body, - Object.assign({}, { skipResourceValidation: false }, requestOptions) - ); - } - - public async putAsync( - url: string, - apiVersion: string, - body: any, - requestOptions?: IResourceProviderRequestOptions - ): Promise { - const fullUrl = UrlUtility.createUri(this.armEndpoint, url); - return await this.httpClient.putAsync( - `${fullUrl}?api-version=${apiVersion}`, - body, - Object.assign({}, { skipResourceValidation: false }, requestOptions) - ); - } - - public async patchAsync( - url: string, - apiVersion: string, - body: any, - requestOptions?: IResourceProviderRequestOptions - ): Promise { - const fullUrl = UrlUtility.createUri(this.armEndpoint, url); - return await this.httpClient.patchAsync( - `${fullUrl}?api-version=${apiVersion}`, - body, - Object.assign({}, { skipResourceValidation: false }, requestOptions) - ); - } - - public async deleteAsync( - url: string, - apiVersion: string, - requestOptions?: IResourceProviderRequestOptions - ): Promise { - const fullUrl = UrlUtility.createUri(this.armEndpoint, url); - return await this.httpClient.deleteAsync( - `${fullUrl}?api-version=${apiVersion}`, - Object.assign({}, { skipResourceValidation: true }, requestOptions) - ); - } -} - -class HttpClient { - private static readonly SUCCEEDED_STATUS = "Succeeded"; - private static readonly FAILED_STATUS = "Failed"; - private static readonly CANCELED_STATUS = "Canceled"; - private static readonly AZURE_ASYNC_OPERATION_HEADER = "azure-asyncoperation"; - private static readonly RETRY_AFTER_HEADER = "Retry-After"; - private static readonly DEFAULT_THROTTLE_WAIT_TIME_SECONDS = 5; - - private tokenProvider: ViewModels.TokenProvider; - - constructor() { - this.tokenProvider = TokenProviderFactory.create(); - } - - public async getAsync(url: string, requestOptions: IResourceProviderRequestOptions): Promise { - const args: RequestInit = { method: "GET" }; - const response = await this.httpRequest(new Request(url, args), requestOptions); - return (await response.json()) as T; - } - - public async postAsync(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise { - body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body; - const args: RequestInit = { method: "POST", headers: { "Content-Type": "application/json" }, body }; - const response = await this.httpRequest(new Request(url, args), requestOptions); - return await response.json(); - } - - public async putAsync(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise { - body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body; - const args: RequestInit = { method: "PUT", headers: { "Content-Type": "application/json" }, body }; - const response = await this.httpRequest(new Request(url, args), requestOptions); - return (await response.json()) as T; - } - - public async patchAsync(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise { - body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body; - const args: RequestInit = { method: "PATCH", headers: { "Content-Type": "application/json" }, body }; - const response = await this.httpRequest(new Request(url, args), requestOptions); - return (await response.json()) as T; - } - - public async deleteAsync(url: string, requestOptions: IResourceProviderRequestOptions): Promise { - const args: RequestInit = { method: "DELETE" }; - await this.httpRequest(new Request(url, args), requestOptions); - return null; - } - - public async httpRequest( - request: RequestInfo, - requestOptions: IResourceProviderRequestOptions, - numRetries: number = 12 - ): Promise { - const authHeader = await this.tokenProvider.getAuthHeader(); - authHeader && - authHeader.forEach((value: string, header: string) => { - (request as Request).headers.append(header, value); - }); - const response = await fetch(request); - - if (response.status === HttpStatusCodes.Accepted) { - const operationStatusUrl: string = - response.headers && response.headers.get(HttpClient.AZURE_ASYNC_OPERATION_HEADER); - const resource = await this.pollOperationAndGetResultAsync(request, operationStatusUrl, requestOptions); - return new Response(resource && JSON.stringify(resource)); - } - - if (response.status === HttpStatusCodes.TooManyRequests && numRetries > 0) { - // retry on throttles - let waitTimeInSeconds = response.headers.has(HttpClient.RETRY_AFTER_HEADER) - ? parseInt(response.headers.get(HttpClient.RETRY_AFTER_HEADER)) - : HttpClient.DEFAULT_THROTTLE_WAIT_TIME_SECONDS; - - return new Promise((resolve: (value: Response) => void, reject: (error: any) => void) => { - setTimeout(async () => { - try { - const response = await this.httpRequest(request, requestOptions, numRetries - 1); - resolve(response); - } catch (error) { - reject(error); - throw error; - } - }, waitTimeInSeconds * 1000); - }); - } - - if (response.ok) { - // RP sometimes returns HTTP 200 for async operations instead of HTTP 202 (e.g., on PATCH operations), so we need to check - const operationStatusUrl: string = - response.headers && response.headers.get(HttpClient.AZURE_ASYNC_OPERATION_HEADER); - - if (operationStatusUrl) { - const resource = await this.pollOperationAndGetResultAsync(request, operationStatusUrl, requestOptions); - return new Response(resource && JSON.stringify(resource)); - } - - return response; - } - - return Promise.reject({ code: response.status, message: await response.text() }); - } - - private async pollOperationAndGetResultAsync( - originalRequest: RequestInfo, - operationStatusUrl: string, - requestOptions: IResourceProviderRequestOptions - ): Promise { - const getOperationResult = async (resolve: (value: T) => void, reject: (error: any) => void) => { - const operationStatus: OperationStatus = await this.getAsync(operationStatusUrl, requestOptions); - if (!operationStatus) { - return reject("Could not retrieve operation status"); - } else if (operationStatus.status === HttpClient.SUCCEEDED_STATUS) { - let result; - if (requestOptions?.skipResourceValidation === false) { - result = await this.getAsync((originalRequest as Request).url, requestOptions); - } - return resolve(result); - } else if ( - operationStatus.status === HttpClient.CANCELED_STATUS || - operationStatus.status === HttpClient.FAILED_STATUS - ) { - const errorMessage = operationStatus.error - ? JSON.stringify(operationStatus.error) - : "Operation could not be completed"; - return reject(errorMessage); - } - // TODO: add exponential backup and timeout threshold - setTimeout(getOperationResult, 1000, resolve, reject); - }; - - return new Promise(getOperationResult); - } -} +import * as ViewModels from "../Contracts/ViewModels"; +import { HttpStatusCodes } from "../Common/Constants"; +import { IResourceProviderClient, IResourceProviderRequestOptions } from "./IResourceProviderClient"; +import { OperationStatus } from "../Contracts/DataModels"; +import { TokenProviderFactory } from "../TokenProviders/TokenProviderFactory"; +import UrlUtility from "../Common/UrlUtility"; + +export class ResourceProviderClient implements IResourceProviderClient { + private httpClient: HttpClient; + + constructor(private armEndpoint: string) { + this.httpClient = new HttpClient(); + } + + public async getAsync( + url: string, + apiVersion: string, + queryString?: string, + requestOptions?: IResourceProviderRequestOptions + ): Promise { + let uri = `${this.armEndpoint}${url}?api-version=${apiVersion}`; + if (queryString) { + uri += `&${queryString}`; + } + return await this.httpClient.getAsync( + uri, + Object.assign({}, { skipResourceValidation: false }, requestOptions) + ); + } + + public async postAsync( + url: string, + apiVersion: string, + body: any, + requestOptions?: IResourceProviderRequestOptions + ): Promise { + const fullUrl = UrlUtility.createUri(this.armEndpoint, url); + return await this.httpClient.postAsync( + `${fullUrl}?api-version=${apiVersion}`, + body, + Object.assign({}, { skipResourceValidation: false }, requestOptions) + ); + } + + public async putAsync( + url: string, + apiVersion: string, + body: any, + requestOptions?: IResourceProviderRequestOptions + ): Promise { + const fullUrl = UrlUtility.createUri(this.armEndpoint, url); + return await this.httpClient.putAsync( + `${fullUrl}?api-version=${apiVersion}`, + body, + Object.assign({}, { skipResourceValidation: false }, requestOptions) + ); + } + + public async patchAsync( + url: string, + apiVersion: string, + body: any, + requestOptions?: IResourceProviderRequestOptions + ): Promise { + const fullUrl = UrlUtility.createUri(this.armEndpoint, url); + return await this.httpClient.patchAsync( + `${fullUrl}?api-version=${apiVersion}`, + body, + Object.assign({}, { skipResourceValidation: false }, requestOptions) + ); + } + + public async deleteAsync( + url: string, + apiVersion: string, + requestOptions?: IResourceProviderRequestOptions + ): Promise { + const fullUrl = UrlUtility.createUri(this.armEndpoint, url); + return await this.httpClient.deleteAsync( + `${fullUrl}?api-version=${apiVersion}`, + Object.assign({}, { skipResourceValidation: true }, requestOptions) + ); + } +} + +class HttpClient { + private static readonly SUCCEEDED_STATUS = "Succeeded"; + private static readonly FAILED_STATUS = "Failed"; + private static readonly CANCELED_STATUS = "Canceled"; + private static readonly AZURE_ASYNC_OPERATION_HEADER = "azure-asyncoperation"; + private static readonly RETRY_AFTER_HEADER = "Retry-After"; + private static readonly DEFAULT_THROTTLE_WAIT_TIME_SECONDS = 5; + + private tokenProvider: ViewModels.TokenProvider; + + constructor() { + this.tokenProvider = TokenProviderFactory.create(); + } + + public async getAsync(url: string, requestOptions: IResourceProviderRequestOptions): Promise { + const args: RequestInit = { method: "GET" }; + const response = await this.httpRequest(new Request(url, args), requestOptions); + return (await response.json()) as T; + } + + public async postAsync(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise { + body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body; + const args: RequestInit = { method: "POST", headers: { "Content-Type": "application/json" }, body }; + const response = await this.httpRequest(new Request(url, args), requestOptions); + return await response.json(); + } + + public async putAsync(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise { + body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body; + const args: RequestInit = { method: "PUT", headers: { "Content-Type": "application/json" }, body }; + const response = await this.httpRequest(new Request(url, args), requestOptions); + return (await response.json()) as T; + } + + public async patchAsync(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise { + body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body; + const args: RequestInit = { method: "PATCH", headers: { "Content-Type": "application/json" }, body }; + const response = await this.httpRequest(new Request(url, args), requestOptions); + return (await response.json()) as T; + } + + public async deleteAsync(url: string, requestOptions: IResourceProviderRequestOptions): Promise { + const args: RequestInit = { method: "DELETE" }; + await this.httpRequest(new Request(url, args), requestOptions); + return null; + } + + public async httpRequest( + request: RequestInfo, + requestOptions: IResourceProviderRequestOptions, + numRetries: number = 12 + ): Promise { + const authHeader = await this.tokenProvider.getAuthHeader(); + authHeader && + authHeader.forEach((value: string, header: string) => { + (request as Request).headers.append(header, value); + }); + const response = await fetch(request); + + if (response.status === HttpStatusCodes.Accepted) { + const operationStatusUrl: string = + response.headers && response.headers.get(HttpClient.AZURE_ASYNC_OPERATION_HEADER); + const resource = await this.pollOperationAndGetResultAsync(request, operationStatusUrl, requestOptions); + return new Response(resource && JSON.stringify(resource)); + } + + if (response.status === HttpStatusCodes.TooManyRequests && numRetries > 0) { + // retry on throttles + let waitTimeInSeconds = response.headers.has(HttpClient.RETRY_AFTER_HEADER) + ? parseInt(response.headers.get(HttpClient.RETRY_AFTER_HEADER)) + : HttpClient.DEFAULT_THROTTLE_WAIT_TIME_SECONDS; + + return new Promise((resolve: (value: Response) => void, reject: (error: any) => void) => { + setTimeout(async () => { + try { + const response = await this.httpRequest(request, requestOptions, numRetries - 1); + resolve(response); + } catch (error) { + reject(error); + throw error; + } + }, waitTimeInSeconds * 1000); + }); + } + + if (response.ok) { + // RP sometimes returns HTTP 200 for async operations instead of HTTP 202 (e.g., on PATCH operations), so we need to check + const operationStatusUrl: string = + response.headers && response.headers.get(HttpClient.AZURE_ASYNC_OPERATION_HEADER); + + if (operationStatusUrl) { + const resource = await this.pollOperationAndGetResultAsync(request, operationStatusUrl, requestOptions); + return new Response(resource && JSON.stringify(resource)); + } + + return response; + } + + return Promise.reject({ code: response.status, message: await response.text() }); + } + + private async pollOperationAndGetResultAsync( + originalRequest: RequestInfo, + operationStatusUrl: string, + requestOptions: IResourceProviderRequestOptions + ): Promise { + const getOperationResult = async (resolve: (value: T) => void, reject: (error: any) => void) => { + const operationStatus: OperationStatus = await this.getAsync(operationStatusUrl, requestOptions); + if (!operationStatus) { + return reject("Could not retrieve operation status"); + } else if (operationStatus.status === HttpClient.SUCCEEDED_STATUS) { + let result; + if (requestOptions?.skipResourceValidation === false) { + result = await this.getAsync((originalRequest as Request).url, requestOptions); + } + return resolve(result); + } else if ( + operationStatus.status === HttpClient.CANCELED_STATUS || + operationStatus.status === HttpClient.FAILED_STATUS + ) { + const errorMessage = operationStatus.error + ? JSON.stringify(operationStatus.error) + : "Operation could not be completed"; + return reject(errorMessage); + } + // TODO: add exponential backup and timeout threshold + setTimeout(getOperationResult, 1000, resolve, reject); + }; + + return new Promise(getOperationResult); + } +} diff --git a/src/ResourceProvider/ResourceProviderClientFactory.ts b/src/ResourceProvider/ResourceProviderClientFactory.ts index 07b71a34e..4ecc128d1 100644 --- a/src/ResourceProvider/ResourceProviderClientFactory.ts +++ b/src/ResourceProvider/ResourceProviderClientFactory.ts @@ -1,22 +1,22 @@ -import { configContext } from "../ConfigContext"; -import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient"; -import { ResourceProviderClient } from "./ResourceProviderClient"; - -export class ResourceProviderClientFactory implements IResourceProviderClientFactory { - private armEndpoint: string; - private cachedClients: { [url: string]: IResourceProviderClient } = {}; - - constructor() { - this.armEndpoint = configContext.ARM_ENDPOINT; - } - - public getOrCreate(url: string): IResourceProviderClient { - if (!url) { - throw new Error("No resource provider client factory params specified"); - } - if (!this.cachedClients[url]) { - this.cachedClients[url] = new ResourceProviderClient(this.armEndpoint); - } - return this.cachedClients[url]; - } -} +import { configContext } from "../ConfigContext"; +import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient"; +import { ResourceProviderClient } from "./ResourceProviderClient"; + +export class ResourceProviderClientFactory implements IResourceProviderClientFactory { + private armEndpoint: string; + private cachedClients: { [url: string]: IResourceProviderClient } = {}; + + constructor() { + this.armEndpoint = configContext.ARM_ENDPOINT; + } + + public getOrCreate(url: string): IResourceProviderClient { + if (!url) { + throw new Error("No resource provider client factory params specified"); + } + if (!this.cachedClients[url]) { + this.cachedClients[url] = new ResourceProviderClient(this.armEndpoint); + } + return this.cachedClients[url]; + } +} diff --git a/src/RouteHandlers/RouteHandler.ts b/src/RouteHandlers/RouteHandler.ts index ba512ae04..ade05e7df 100644 --- a/src/RouteHandlers/RouteHandler.ts +++ b/src/RouteHandlers/RouteHandler.ts @@ -1,35 +1,35 @@ -import { MessageTypes } from "../Contracts/ExplorerContracts"; -import { sendMessage } from "../Common/MessageHandler"; -import { TabRouteHandler } from "./TabRouteHandler"; - -export class RouteHandler { - private static _instance: RouteHandler; - private _tabRouteHandler: TabRouteHandler; - - private constructor() { - this._tabRouteHandler = new TabRouteHandler(); - } - - public static getInstance(): RouteHandler { - if (!RouteHandler._instance) { - RouteHandler._instance = new RouteHandler(); - } - - return RouteHandler._instance; - } - - public initHandler(): void { - this._tabRouteHandler.initRouteHandler(); - } - - public parseHash(hash: string): void { - this._tabRouteHandler.parseHash(hash); - } - - public updateRouteHashLocation(hash: string): void { - sendMessage({ - type: MessageTypes.UpdateLocationHash, - locationHash: hash - }); - } -} +import { MessageTypes } from "../Contracts/ExplorerContracts"; +import { sendMessage } from "../Common/MessageHandler"; +import { TabRouteHandler } from "./TabRouteHandler"; + +export class RouteHandler { + private static _instance: RouteHandler; + private _tabRouteHandler: TabRouteHandler; + + private constructor() { + this._tabRouteHandler = new TabRouteHandler(); + } + + public static getInstance(): RouteHandler { + if (!RouteHandler._instance) { + RouteHandler._instance = new RouteHandler(); + } + + return RouteHandler._instance; + } + + public initHandler(): void { + this._tabRouteHandler.initRouteHandler(); + } + + public parseHash(hash: string): void { + this._tabRouteHandler.parseHash(hash); + } + + public updateRouteHashLocation(hash: string): void { + sendMessage({ + type: MessageTypes.UpdateLocationHash, + locationHash: hash, + }); + } +} diff --git a/src/RouteHandlers/TabRouteHandler.test.ts b/src/RouteHandlers/TabRouteHandler.test.ts index cbddd968f..0d2031efd 100644 --- a/src/RouteHandlers/TabRouteHandler.test.ts +++ b/src/RouteHandlers/TabRouteHandler.test.ts @@ -1,102 +1,102 @@ -import crossroads from "crossroads"; -import hasher from "hasher"; - -import * as Constants from "../Common/Constants"; -import Explorer from "../Explorer/Explorer"; -import { TabRouteHandler } from "./TabRouteHandler"; - -describe("TabRouteHandler", () => { - let tabRouteHandler: TabRouteHandler; - - beforeAll(() => { - (window).dataExplorer = new Explorer(); // create a mock to avoid null refs - }); - - beforeEach(() => { - tabRouteHandler = new TabRouteHandler(); - }); - - afterEach(() => { - crossroads.removeAllRoutes(); - }); - - describe("Route Handling", () => { - let routedSpy: jasmine.Spy; - - beforeEach(() => { - routedSpy = jasmine.createSpy("Routed spy"); - const onMatch = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => { - routedSpy(request, data); - }; - tabRouteHandler.initRouteHandler(onMatch); - }); - - function validateRouteWithParams(route: string, routeParams: string[]): void { - hasher.setHash(route); - expect(routedSpy).toHaveBeenCalledWith( - route, - jasmine.objectContaining({ params: jasmine.arrayContaining(routeParams) }) - ); - } - - it("should include a route for documents tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/documents`, ["1", "2"]); - }); - - it("should include a route for entities tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/entities`, ["1", "2"]); - }); - - it("should include a route for graphs tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/graphs`, ["1", "2"]); - }); - - it("should include a route for mongo documents tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/mongoDocuments`, ["1", "2"]); - }); - - it("should include a route for mongo shell", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/mongoShell`, ["1", "2"]); - }); - - it("should include a route for query tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/query`, ["1", "2"]); - }); - - it("should include a route for database settings tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.databasesWithId("1")}/settings`, ["1"]); - }); - - it("should include a route for settings tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/settings`, ["1", "2"]); - }); - - it("should include a route for new stored procedure tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/sproc`, ["1", "2"]); - }); - - it("should include a route for stored procedure tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/sprocs/3`, ["1", "2", "3"]); - }); - - it("should include a route for new trigger tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/trigger`, ["1", "2"]); - }); - - it("should include a route for trigger tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/triggers/3`, [ - "1", - "2", - "3" - ]); - }); - - it("should include a route for new UDF tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/udf`, ["1", "2"]); - }); - - it("should include a route for UDF tab", () => { - validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/udfs/3`, ["1", "2", "3"]); - }); - }); -}); +import crossroads from "crossroads"; +import hasher from "hasher"; + +import * as Constants from "../Common/Constants"; +import Explorer from "../Explorer/Explorer"; +import { TabRouteHandler } from "./TabRouteHandler"; + +describe("TabRouteHandler", () => { + let tabRouteHandler: TabRouteHandler; + + beforeAll(() => { + (window).dataExplorer = new Explorer(); // create a mock to avoid null refs + }); + + beforeEach(() => { + tabRouteHandler = new TabRouteHandler(); + }); + + afterEach(() => { + crossroads.removeAllRoutes(); + }); + + describe("Route Handling", () => { + let routedSpy: jasmine.Spy; + + beforeEach(() => { + routedSpy = jasmine.createSpy("Routed spy"); + const onMatch = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => { + routedSpy(request, data); + }; + tabRouteHandler.initRouteHandler(onMatch); + }); + + function validateRouteWithParams(route: string, routeParams: string[]): void { + hasher.setHash(route); + expect(routedSpy).toHaveBeenCalledWith( + route, + jasmine.objectContaining({ params: jasmine.arrayContaining(routeParams) }) + ); + } + + it("should include a route for documents tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/documents`, ["1", "2"]); + }); + + it("should include a route for entities tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/entities`, ["1", "2"]); + }); + + it("should include a route for graphs tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/graphs`, ["1", "2"]); + }); + + it("should include a route for mongo documents tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/mongoDocuments`, ["1", "2"]); + }); + + it("should include a route for mongo shell", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/mongoShell`, ["1", "2"]); + }); + + it("should include a route for query tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/query`, ["1", "2"]); + }); + + it("should include a route for database settings tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.databasesWithId("1")}/settings`, ["1"]); + }); + + it("should include a route for settings tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/settings`, ["1", "2"]); + }); + + it("should include a route for new stored procedure tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/sproc`, ["1", "2"]); + }); + + it("should include a route for stored procedure tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/sprocs/3`, ["1", "2", "3"]); + }); + + it("should include a route for new trigger tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/trigger`, ["1", "2"]); + }); + + it("should include a route for trigger tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/triggers/3`, [ + "1", + "2", + "3", + ]); + }); + + it("should include a route for new UDF tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/udf`, ["1", "2"]); + }); + + it("should include a route for UDF tab", () => { + validateRouteWithParams(`${Constants.HashRoutePrefixes.collectionsWithIds("1", "2")}/udfs/3`, ["1", "2", "3"]); + }); + }); +}); diff --git a/src/RouteHandlers/TabRouteHandler.ts b/src/RouteHandlers/TabRouteHandler.ts index b79e1f69a..2632d93d3 100644 --- a/src/RouteHandlers/TabRouteHandler.ts +++ b/src/RouteHandlers/TabRouteHandler.ts @@ -1,418 +1,418 @@ -import * as _ from "underscore"; -import * as Constants from "../Common/Constants"; -import * as ViewModels from "../Contracts/ViewModels"; - -import crossroads from "crossroads"; -import hasher from "hasher"; -import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase"; -import TabsBase from "../Explorer/Tabs/TabsBase"; - -export class TabRouteHandler { - private _tabRouter: any; - - constructor() {} - - public initRouteHandler( - onMatch?: (request: string, data: { route: any; params: string[]; isFirst: boolean }) => void - ): void { - this._initRouter(); - const parseHash = (newHash: string, oldHash: string) => this._tabRouter.parse(newHash); - const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {}; - this._tabRouter.routed.add(onMatch || defaultRoutedCallback); - hasher.initialized.add(parseHash); - hasher.changed.add(parseHash); - hasher.init(); - } - - public parseHash(hash: string): void { - this._tabRouter.parse(hash); - } - - private _initRouter() { - this._tabRouter = crossroads.create(); - this._setupTabRoutesForRouter(); - } - - private _setupTabRoutesForRouter(): void { - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/documents`, - (db_id: string, coll_id: string) => { - this._openDocumentsTabForResource(db_id, coll_id); - } - ); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/entities`, - (db_id: string, coll_id: string) => { - this._openEntitiesTabForResource(db_id, coll_id); - } - ); - - this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/graphs`, (db_id: string, coll_id: string) => { - this._openGraphTabForResource(db_id, coll_id); - }); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/mongoDocuments`, - (db_id: string, coll_id: string) => { - this._openMongoDocumentsTabForResource(db_id, coll_id); - } - ); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/mongoQuery`, - (db_id: string, coll_id: string) => { - this._openMongoQueryTabForResource(db_id, coll_id); - } - ); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/mongoShell`, - (db_id: string, coll_id: string) => { - this._openMongoShellTabForResource(db_id, coll_id); - } - ); - - this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/query`, (db_id: string, coll_id: string) => { - this._openQueryTabForResource(db_id, coll_id); - }); - - this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.databases}/settings`, (db_id: string) => { - this._openDatabaseSettingsTabForResource(db_id); - }); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/settings`, - (db_id: string, coll_id: string) => { - this._openSettingsTabForResource(db_id, coll_id); - } - ); - - this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/sproc`, (db_id: string, coll_id: string) => { - this._openNewSprocTabForResource(db_id, coll_id); - }); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/sprocs/{sproc_id}`, - (db_id: string, coll_id: string, sproc_id: string) => { - this._openSprocTabForResource(db_id, coll_id, sproc_id); - } - ); - - this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/trigger`, (db_id: string, coll_id: string) => { - this._openNewTriggerTabForResource(db_id, coll_id); - }); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/triggers/{trigger_id}`, - (db_id: string, coll_id: string, trigger_id: string) => { - this._openTriggerTabForResource(db_id, coll_id, trigger_id); - } - ); - - this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/udf`, (db_id: string, coll_id: string) => { - this._openNewUserDefinedFunctionTabForResource(db_id, coll_id); - }); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/udfs/{udf_id}`, - (db_id: string, coll_id: string, udf_id: string) => { - this._openUserDefinedFunctionTabForResource(db_id, coll_id, udf_id); - } - ); - - this._tabRouter.addRoute( - `${Constants.HashRoutePrefixes.collections}/conflicts`, - (db_id: string, coll_id: string) => { - this._openConflictsTabForResource(db_id, coll_id); - } - ); - } - - private _openDocumentsTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && - collection.container && - collection.container.isPreferredApiDocumentDB() && - collection.onDocumentDBDocumentsClick(); - }); - } - - private _openEntitiesTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && - collection.container && - (collection.container.isPreferredApiTable() || collection.container.isPreferredApiCassandra()) && - collection.onTableEntitiesClick(); - }); - } - - private _openGraphTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && - collection.container && - collection.container.isPreferredApiGraph() && - collection.onGraphDocumentsClick(); - }); - } - - private _openMongoDocumentsTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && - collection.container && - collection.container.isPreferredApiMongoDB() && - collection.onMongoDBDocumentsClick(); - }); - } - - private _openQueryTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - const matchingTab: TabsBase = this._findMatchingTabByTabKind( - databaseId, - collectionId, - ViewModels.CollectionTabKind.Query - ); - if (!!matchingTab) { - matchingTab.onTabClick(); - } else { - collection && collection.onNewQueryClick(collection, null); - } - }); - } - - private _openMongoQueryTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - const matchingTab: TabsBase = this._findMatchingTabByTabKind( - databaseId, - collectionId, - ViewModels.CollectionTabKind.Query - ); - if (!!matchingTab) { - matchingTab.onTabClick(); - } else { - collection && - collection.container && - collection.container.isPreferredApiMongoDB() && - collection.onNewMongoQueryClick(collection, null); - } - }); - } - - private _openMongoShellTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - const matchingTab: TabsBase = this._findMatchingTabByTabKind( - databaseId, - collectionId, - ViewModels.CollectionTabKind.MongoShell - ); - if (!!matchingTab) { - matchingTab.onTabClick(); - } else { - collection && - collection.container && - collection.container.isPreferredApiMongoDB() && - collection.onNewMongoShellClick(); - } - }); - } - - private _openDatabaseSettingsTabForResource(databaseId: string): void { - this._executeActionHelper(() => { - const explorer = window.dataExplorer; - const database: ViewModels.Database = _.find( - explorer.databases(), - (database: ViewModels.Database) => database.id() === databaseId - ); - database && database.onSettingsClick(); - }); - } - - private _openSettingsTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && collection.onSettingsClick(); - }); - } - - private _openNewSprocTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - const matchingTab: TabsBase = this._findMatchingTabByTabKind( - databaseId, - collectionId, - ViewModels.CollectionTabKind.StoredProcedures, - true - ); - if (!!matchingTab) { - matchingTab.onTabClick(); - } else { - collection && collection.onNewStoredProcedureClick(collection, null); - } - }); - } - - private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); - collection && - collection.expandCollection().then(() => { - const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId); - storedProcedure && storedProcedure.open(); - }); - }); - } - - private _openNewTriggerTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - const matchingTab: TabsBase = this._findMatchingTabByTabKind( - databaseId, - collectionId, - ViewModels.CollectionTabKind.Triggers, - true - ); - if (!!matchingTab) { - matchingTab.onTabClick(); - } else { - collection && collection.onNewTriggerClick(collection, null); - } - }); - } - - private _openTriggerTabForResource(databaseId: string, collectionId: string, triggerId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); - collection && - collection.expandCollection().then(() => { - const trigger = collection && collection.findTriggerWithId(triggerId); - trigger && trigger.open(); - }); - }); - } - - private _openNewUserDefinedFunctionTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - const matchingTab: TabsBase = this._findMatchingTabByTabKind( - databaseId, - collectionId, - ViewModels.CollectionTabKind.UserDefinedFunctions, - true - ); - if (!!matchingTab) { - matchingTab.onTabClick(); - } else { - collection && collection.onNewUserDefinedFunctionClick(collection, null); - } - }); - } - - private _openUserDefinedFunctionTabForResource(databaseId: string, collectionId: string, udfId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); - collection && - collection.expandCollection().then(() => { - const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId); - userDefinedFunction && userDefinedFunction.open(); - }); - }); - } - - private _openConflictsTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && collection.container && collection.onConflictsClick(); - }); - } - - private _findAndExpandMatchingCollectionForResource(databaseId: string, collectionId: string): ViewModels.Collection { - const matchedCollection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); - matchedCollection && matchedCollection.expandCollection(); - - return matchedCollection; - } - - private _findMatchingTabByTabKind( - databaseId: string, - collectionId: string, - tabKind: ViewModels.CollectionTabKind, - isNewScriptTab?: boolean - ): TabsBase { - const explorer = window.dataExplorer; - const matchingTabs: TabsBase[] = explorer.tabsManager.getTabs( - tabKind, - (tab: TabsBase) => - tab.collection && - tab.collection.databaseId === databaseId && - tab.collection.id() === collectionId && - (!isNewScriptTab || (tab as ScriptTabBase).isNew()) - ); - return matchingTabs && matchingTabs[0]; - } - - private _findMatchingCollectionForResource(databaseId: string, collectionId: string): ViewModels.Collection { - const explorer = window.dataExplorer; - const matchedDatabase: ViewModels.Database = explorer.findDatabaseWithId(databaseId); - const matchedCollection: ViewModels.Collection = - matchedDatabase && matchedDatabase.findCollectionWithId(collectionId); - - return matchedCollection; - } - - private _executeActionHelper(action: () => void): void { - const explorer = window.dataExplorer; - if (!!explorer && (explorer.isRefreshingExplorer() || !explorer.isAccountReady())) { - const refreshSubscription = explorer.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { - if (!isRefreshing) { - action(); - refreshSubscription.dispose(); - } - }); - } else { - action(); - } - } -} +import * as _ from "underscore"; +import * as Constants from "../Common/Constants"; +import * as ViewModels from "../Contracts/ViewModels"; + +import crossroads from "crossroads"; +import hasher from "hasher"; +import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase"; +import TabsBase from "../Explorer/Tabs/TabsBase"; + +export class TabRouteHandler { + private _tabRouter: any; + + constructor() {} + + public initRouteHandler( + onMatch?: (request: string, data: { route: any; params: string[]; isFirst: boolean }) => void + ): void { + this._initRouter(); + const parseHash = (newHash: string, oldHash: string) => this._tabRouter.parse(newHash); + const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {}; + this._tabRouter.routed.add(onMatch || defaultRoutedCallback); + hasher.initialized.add(parseHash); + hasher.changed.add(parseHash); + hasher.init(); + } + + public parseHash(hash: string): void { + this._tabRouter.parse(hash); + } + + private _initRouter() { + this._tabRouter = crossroads.create(); + this._setupTabRoutesForRouter(); + } + + private _setupTabRoutesForRouter(): void { + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/documents`, + (db_id: string, coll_id: string) => { + this._openDocumentsTabForResource(db_id, coll_id); + } + ); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/entities`, + (db_id: string, coll_id: string) => { + this._openEntitiesTabForResource(db_id, coll_id); + } + ); + + this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/graphs`, (db_id: string, coll_id: string) => { + this._openGraphTabForResource(db_id, coll_id); + }); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/mongoDocuments`, + (db_id: string, coll_id: string) => { + this._openMongoDocumentsTabForResource(db_id, coll_id); + } + ); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/mongoQuery`, + (db_id: string, coll_id: string) => { + this._openMongoQueryTabForResource(db_id, coll_id); + } + ); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/mongoShell`, + (db_id: string, coll_id: string) => { + this._openMongoShellTabForResource(db_id, coll_id); + } + ); + + this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/query`, (db_id: string, coll_id: string) => { + this._openQueryTabForResource(db_id, coll_id); + }); + + this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.databases}/settings`, (db_id: string) => { + this._openDatabaseSettingsTabForResource(db_id); + }); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/settings`, + (db_id: string, coll_id: string) => { + this._openSettingsTabForResource(db_id, coll_id); + } + ); + + this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/sproc`, (db_id: string, coll_id: string) => { + this._openNewSprocTabForResource(db_id, coll_id); + }); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/sprocs/{sproc_id}`, + (db_id: string, coll_id: string, sproc_id: string) => { + this._openSprocTabForResource(db_id, coll_id, sproc_id); + } + ); + + this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/trigger`, (db_id: string, coll_id: string) => { + this._openNewTriggerTabForResource(db_id, coll_id); + }); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/triggers/{trigger_id}`, + (db_id: string, coll_id: string, trigger_id: string) => { + this._openTriggerTabForResource(db_id, coll_id, trigger_id); + } + ); + + this._tabRouter.addRoute(`${Constants.HashRoutePrefixes.collections}/udf`, (db_id: string, coll_id: string) => { + this._openNewUserDefinedFunctionTabForResource(db_id, coll_id); + }); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/udfs/{udf_id}`, + (db_id: string, coll_id: string, udf_id: string) => { + this._openUserDefinedFunctionTabForResource(db_id, coll_id, udf_id); + } + ); + + this._tabRouter.addRoute( + `${Constants.HashRoutePrefixes.collections}/conflicts`, + (db_id: string, coll_id: string) => { + this._openConflictsTabForResource(db_id, coll_id); + } + ); + } + + private _openDocumentsTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + collection && + collection.container && + collection.container.isPreferredApiDocumentDB() && + collection.onDocumentDBDocumentsClick(); + }); + } + + private _openEntitiesTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + collection && + collection.container && + (collection.container.isPreferredApiTable() || collection.container.isPreferredApiCassandra()) && + collection.onTableEntitiesClick(); + }); + } + + private _openGraphTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + collection && + collection.container && + collection.container.isPreferredApiGraph() && + collection.onGraphDocumentsClick(); + }); + } + + private _openMongoDocumentsTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + collection && + collection.container && + collection.container.isPreferredApiMongoDB() && + collection.onMongoDBDocumentsClick(); + }); + } + + private _openQueryTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + const matchingTab: TabsBase = this._findMatchingTabByTabKind( + databaseId, + collectionId, + ViewModels.CollectionTabKind.Query + ); + if (!!matchingTab) { + matchingTab.onTabClick(); + } else { + collection && collection.onNewQueryClick(collection, null); + } + }); + } + + private _openMongoQueryTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + const matchingTab: TabsBase = this._findMatchingTabByTabKind( + databaseId, + collectionId, + ViewModels.CollectionTabKind.Query + ); + if (!!matchingTab) { + matchingTab.onTabClick(); + } else { + collection && + collection.container && + collection.container.isPreferredApiMongoDB() && + collection.onNewMongoQueryClick(collection, null); + } + }); + } + + private _openMongoShellTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + const matchingTab: TabsBase = this._findMatchingTabByTabKind( + databaseId, + collectionId, + ViewModels.CollectionTabKind.MongoShell + ); + if (!!matchingTab) { + matchingTab.onTabClick(); + } else { + collection && + collection.container && + collection.container.isPreferredApiMongoDB() && + collection.onNewMongoShellClick(); + } + }); + } + + private _openDatabaseSettingsTabForResource(databaseId: string): void { + this._executeActionHelper(() => { + const explorer = window.dataExplorer; + const database: ViewModels.Database = _.find( + explorer.databases(), + (database: ViewModels.Database) => database.id() === databaseId + ); + database && database.onSettingsClick(); + }); + } + + private _openSettingsTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + collection && collection.onSettingsClick(); + }); + } + + private _openNewSprocTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + const matchingTab: TabsBase = this._findMatchingTabByTabKind( + databaseId, + collectionId, + ViewModels.CollectionTabKind.StoredProcedures, + true + ); + if (!!matchingTab) { + matchingTab.onTabClick(); + } else { + collection && collection.onNewStoredProcedureClick(collection, null); + } + }); + } + + private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); + collection && + collection.expandCollection().then(() => { + const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId); + storedProcedure && storedProcedure.open(); + }); + }); + } + + private _openNewTriggerTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + const matchingTab: TabsBase = this._findMatchingTabByTabKind( + databaseId, + collectionId, + ViewModels.CollectionTabKind.Triggers, + true + ); + if (!!matchingTab) { + matchingTab.onTabClick(); + } else { + collection && collection.onNewTriggerClick(collection, null); + } + }); + } + + private _openTriggerTabForResource(databaseId: string, collectionId: string, triggerId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); + collection && + collection.expandCollection().then(() => { + const trigger = collection && collection.findTriggerWithId(triggerId); + trigger && trigger.open(); + }); + }); + } + + private _openNewUserDefinedFunctionTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + const matchingTab: TabsBase = this._findMatchingTabByTabKind( + databaseId, + collectionId, + ViewModels.CollectionTabKind.UserDefinedFunctions, + true + ); + if (!!matchingTab) { + matchingTab.onTabClick(); + } else { + collection && collection.onNewUserDefinedFunctionClick(collection, null); + } + }); + } + + private _openUserDefinedFunctionTabForResource(databaseId: string, collectionId: string, udfId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); + collection && + collection.expandCollection().then(() => { + const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId); + userDefinedFunction && userDefinedFunction.open(); + }); + }); + } + + private _openConflictsTabForResource(databaseId: string, collectionId: string): void { + this._executeActionHelper(() => { + const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( + databaseId, + collectionId + ); + collection && collection.container && collection.onConflictsClick(); + }); + } + + private _findAndExpandMatchingCollectionForResource(databaseId: string, collectionId: string): ViewModels.Collection { + const matchedCollection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); + matchedCollection && matchedCollection.expandCollection(); + + return matchedCollection; + } + + private _findMatchingTabByTabKind( + databaseId: string, + collectionId: string, + tabKind: ViewModels.CollectionTabKind, + isNewScriptTab?: boolean + ): TabsBase { + const explorer = window.dataExplorer; + const matchingTabs: TabsBase[] = explorer.tabsManager.getTabs( + tabKind, + (tab: TabsBase) => + tab.collection && + tab.collection.databaseId === databaseId && + tab.collection.id() === collectionId && + (!isNewScriptTab || (tab as ScriptTabBase).isNew()) + ); + return matchingTabs && matchingTabs[0]; + } + + private _findMatchingCollectionForResource(databaseId: string, collectionId: string): ViewModels.Collection { + const explorer = window.dataExplorer; + const matchedDatabase: ViewModels.Database = explorer.findDatabaseWithId(databaseId); + const matchedCollection: ViewModels.Collection = + matchedDatabase && matchedDatabase.findCollectionWithId(collectionId); + + return matchedCollection; + } + + private _executeActionHelper(action: () => void): void { + const explorer = window.dataExplorer; + if (!!explorer && (explorer.isRefreshingExplorer() || !explorer.isAccountReady())) { + const refreshSubscription = explorer.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { + if (!isRefreshing) { + action(); + refreshSubscription.dispose(); + } + }); + } else { + action(); + } + } +} diff --git a/src/SelfServe/ClassDecorators.tsx b/src/SelfServe/ClassDecorators.tsx index aeb9ad8e2..5659ab365 100644 --- a/src/SelfServe/ClassDecorators.tsx +++ b/src/SelfServe/ClassDecorators.tsx @@ -2,13 +2,13 @@ import { Info } from "../Explorer/Controls/SmartUi/SmartUiComponent"; import { addPropertyToMap, buildSmartUiDescriptor } from "./SelfServeUtils"; export const IsDisplayable = (): ClassDecorator => { - return target => { + return (target) => { buildSmartUiDescriptor(target.name, target.prototype); }; }; export const ClassInfo = (info: (() => Promise) | Info): ClassDecorator => { - return target => { + return (target) => { addPropertyToMap(target.prototype, "root", target.name, "info", info); }; }; diff --git a/src/SelfServe/Example/SelfServeExample.tsx b/src/SelfServe/Example/SelfServeExample.tsx index 739e49cda..1ff77400b 100644 --- a/src/SelfServe/Example/SelfServeExample.tsx +++ b/src/SelfServe/Example/SelfServeExample.tsx @@ -7,21 +7,21 @@ import { SessionStorageUtility } from "../../Shared/StorageUtility"; export enum Regions { NorthCentralUS = "NCUS", WestUS = "WUS", - EastUS2 = "EUS2" + EastUS2 = "EUS2", } export const regionDropdownItems: ChoiceItem[] = [ { label: "North Central US", key: Regions.NorthCentralUS }, { label: "West US", key: Regions.WestUS }, - { label: "East US 2", key: Regions.EastUS2 } + { label: "East US 2", key: Regions.EastUS2 }, ]; export const selfServeExampleInfo: Info = { - message: "This is a self serve class" + message: "This is a self serve class", }; export const regionDropdownInfo: Info = { - message: "More regions can be added in the future." + message: "More regions can be added in the future.", }; const onDbThroughputChange = (currentState: Map, newValue: InputType): Map => { @@ -124,13 +124,13 @@ export default class SelfServeExample extends SelfServeBaseClass { @Values({ label: "Enable Logging", trueLabel: "Enable", - falseLabel: "Disable" + falseLabel: "Disable", }) enableLogging: boolean; @Values({ label: "Account Name", - placeholder: "Enter the account name" + placeholder: "Enter the account name", }) accountName: string; @@ -152,7 +152,7 @@ export default class SelfServeExample extends SelfServeBaseClass { min: 400, max: initializeMaxThroughput, step: 100, - uiType: UiType.Slider + uiType: UiType.Slider, }) dbThroughput: number; @@ -161,7 +161,7 @@ export default class SelfServeExample extends SelfServeBaseClass { min: 400, max: initializeMaxThroughput, step: 100, - uiType: UiType.Spinner + uiType: UiType.Spinner, }) collectionThroughput: number; } diff --git a/src/SelfServe/SelfServeComponent.test.tsx b/src/SelfServe/SelfServeComponent.test.tsx index 32cf9ba73..b51e1a96b 100644 --- a/src/SelfServe/SelfServeComponent.test.tsx +++ b/src/SelfServe/SelfServeComponent.test.tsx @@ -7,7 +7,7 @@ describe("SelfServeComponent", () => { const defaultValues = new Map([ ["throughput", "450"], ["analyticalStore", "false"], - ["database", "db2"] + ["database", "db2"], ]); const initializeMock = jest.fn(async () => defaultValues); const onSubmitMock = jest.fn(async () => { @@ -24,8 +24,8 @@ describe("SelfServeComponent", () => { message: "Start at $24/mo per database", link: { href: "https://aka.ms/azure-cosmos-db-pricing", - text: "More Details" - } + text: "More Details", + }, }, children: [ { @@ -38,16 +38,16 @@ describe("SelfServeComponent", () => { max: 500, step: 10, defaultValue: 400, - uiType: UiType.Spinner - } + uiType: UiType.Spinner, + }, }, { id: "containerId", input: { label: "Container id", dataFieldName: "containerId", - type: "string" - } + type: "string", + }, }, { id: "analyticalStore", @@ -57,8 +57,8 @@ describe("SelfServeComponent", () => { falseLabel: "Disabled", defaultValue: true, dataFieldName: "analyticalStore", - type: "boolean" - } + type: "boolean", + }, }, { id: "database", @@ -69,13 +69,13 @@ describe("SelfServeComponent", () => { choices: [ { label: "Database 1", key: "db1" }, { label: "Database 2", key: "db2" }, - { label: "Database 3", key: "db3" } + { label: "Database 3", key: "db3" }, ], - defaultKey: "db2" - } - } - ] - } + defaultKey: "db2", + }, + }, + ], + }, }; const verifyDefaultsSet = (currentValues: Map): void => { @@ -88,7 +88,7 @@ describe("SelfServeComponent", () => { it("should render", async () => { const wrapper = shallow(); - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); expect(wrapper).toMatchSnapshot(); // initialize() should be called and defaults should be set when component is mounted diff --git a/src/SelfServe/SelfServeComponent.tsx b/src/SelfServe/SelfServeComponent.tsx index 7bb6208a1..5f4b48728 100644 --- a/src/SelfServe/SelfServeComponent.tsx +++ b/src/SelfServe/SelfServeComponent.tsx @@ -7,7 +7,7 @@ import { SmartUiComponent, UiType, SmartUiDescriptor, - Info + Info, } from "../Explorer/Controls/SmartUi/SmartUiComponent"; export interface BaseInput { @@ -80,7 +80,7 @@ export class SelfServeComponent extends React.Component { min: 1, max: 5, step: 1, - uiType: UiType.Slider - } + uiType: UiType.Slider, + }, ], [ "collThroughput", @@ -76,8 +76,8 @@ describe("SelfServeUtils", () => { min: 1, max: 5, step: 1, - uiType: UiType.Spinner - } + uiType: UiType.Spinner, + }, ], [ "invalidThroughput", @@ -90,8 +90,8 @@ describe("SelfServeUtils", () => { max: 5, step: 1, uiType: UiType.Spinner, - errorMessage: "label, truelabel and falselabel are required for boolean input" - } + errorMessage: "label, truelabel and falselabel are required for boolean input", + }, ], [ "collName", @@ -100,8 +100,8 @@ describe("SelfServeUtils", () => { dataFieldName: "collName", type: "string", label: "Coll Name", - placeholder: "placeholder text" - } + placeholder: "placeholder text", + }, ], [ "enableLogging", @@ -111,8 +111,8 @@ describe("SelfServeUtils", () => { type: "boolean", label: "Enable Logging", trueLabel: "Enable", - falseLabel: "Disable" - } + falseLabel: "Disable", + }, ], [ "invalidEnableLogging", @@ -121,8 +121,8 @@ describe("SelfServeUtils", () => { dataFieldName: "invalidEnableLogging", type: "boolean", label: "Invalid Enable Logging", - placeholder: "placeholder text" - } + placeholder: "placeholder text", + }, ], [ "regions", @@ -134,9 +134,9 @@ describe("SelfServeUtils", () => { choices: [ { label: "South West US", key: "SWUS" }, { label: "North Central US", key: "NCUS" }, - { label: "East US 2", key: "EUS2" } - ] - } + { label: "East US 2", key: "EUS2" }, + ], + }, ], [ "invalidRegions", @@ -145,9 +145,9 @@ describe("SelfServeUtils", () => { dataFieldName: "invalidRegions", type: "object", label: "Invalid Regions", - placeholder: "placeholder text" - } - ] + placeholder: "placeholder text", + }, + ], ]); const expectedDescriptor = { root: { @@ -163,9 +163,9 @@ describe("SelfServeUtils", () => { min: 1, max: 5, step: 1, - uiType: "Slider" + uiType: "Slider", }, - children: [] as Node[] + children: [] as Node[], }, { id: "collThroughput", @@ -177,9 +177,9 @@ describe("SelfServeUtils", () => { min: 1, max: 5, step: 1, - uiType: "Spinner" + uiType: "Spinner", }, - children: [] as Node[] + children: [] as Node[], }, { id: "invalidThroughput", @@ -192,9 +192,9 @@ describe("SelfServeUtils", () => { max: 5, step: 1, uiType: "Spinner", - errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidThroughput'." + errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidThroughput'.", }, - children: [] as Node[] + children: [] as Node[], }, { id: "collName", @@ -203,9 +203,9 @@ describe("SelfServeUtils", () => { dataFieldName: "collName", type: "string", label: "Coll Name", - placeholder: "placeholder text" + placeholder: "placeholder text", }, - children: [] as Node[] + children: [] as Node[], }, { id: "enableLogging", @@ -215,9 +215,9 @@ describe("SelfServeUtils", () => { type: "boolean", label: "Enable Logging", trueLabel: "Enable", - falseLabel: "Disable" + falseLabel: "Disable", }, - children: [] as Node[] + children: [] as Node[], }, { id: "invalidEnableLogging", @@ -227,9 +227,9 @@ describe("SelfServeUtils", () => { type: "boolean", label: "Invalid Enable Logging", placeholder: "placeholder text", - errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'." + errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.", }, - children: [] as Node[] + children: [] as Node[], }, { id: "regions", @@ -241,10 +241,10 @@ describe("SelfServeUtils", () => { choices: [ { label: "South West US", key: "SWUS" }, { label: "North Central US", key: "NCUS" }, - { label: "East US 2", key: "EUS2" } - ] + { label: "East US 2", key: "EUS2" }, + ], }, - children: [] as Node[] + children: [] as Node[], }, { id: "invalidRegions", @@ -254,11 +254,11 @@ describe("SelfServeUtils", () => { type: "object", label: "Invalid Regions", placeholder: "placeholder text", - errorMessage: "label and choices are required for Choice input 'invalidRegions'." + errorMessage: "label and choices are required for Choice input 'invalidRegions'.", }, - children: [] as Node[] - } - ] + children: [] as Node[], + }, + ], }, inputNames: [ "dbThroughput", @@ -268,8 +268,8 @@ describe("SelfServeUtils", () => { "enableLogging", "invalidEnableLogging", "regions", - "invalidRegions" - ] + "invalidRegions", + ], }; const descriptor = mapToSmartUiDescriptor(context); expect(descriptor).toEqual(expectedDescriptor); diff --git a/src/SelfServe/SelfServeUtils.tsx b/src/SelfServe/SelfServeUtils.tsx index e32eb69e9..722a6e2c9 100644 --- a/src/SelfServe/SelfServeUtils.tsx +++ b/src/SelfServe/SelfServeUtils.tsx @@ -7,7 +7,7 @@ import { NumberInput, StringInput, Node, - AnyInput + AnyInput, } from "./SelfServeComponent"; export enum SelfServeType { @@ -16,7 +16,7 @@ export enum SelfServeType { // Unsupported self serve type passed as feature flag invalid = "invalid", // Add your self serve types here - example = "example" + example = "example", } export abstract class SelfServeBaseClass { @@ -126,8 +126,8 @@ export const mapToSmartUiDescriptor = (context: Map): root: { id: "root", info: root?.info, - children: [] - } + children: [], + }, }; while (context.size > 0) { @@ -151,7 +151,7 @@ const addToDescriptor = ( id: value.id, info: value.info, input: getInput(value), - children: [] + children: [], } as Node; context.delete(key); root.children.push(element); diff --git a/src/Shared/AddCollectionUtility.test.ts b/src/Shared/AddCollectionUtility.test.ts index e539e4313..610784027 100644 --- a/src/Shared/AddCollectionUtility.test.ts +++ b/src/Shared/AddCollectionUtility.test.ts @@ -12,8 +12,8 @@ describe("getMaxThroughput", () => { unlimited: 400, unlimitedmax: 1000000, unlimitedmin: 400, - shared: 400 - } + shared: 400, + }, }; expect(getMaxThroughput(defaults, {} as Explorer)).toEqual(defaults.throughput.unlimited); @@ -27,12 +27,12 @@ describe("getMaxThroughput", () => { unlimited: { collectionThreshold: 3, lessThanOrEqualToThreshold: 400, - greatThanThreshold: 500 + greatThanThreshold: 500, }, unlimitedmax: 1000000, unlimitedmin: 400, - shared: 400 - } + shared: 400, + }, }; const mockCollection1 = { id: ko.observable("collection1") } as Collection; @@ -41,7 +41,7 @@ describe("getMaxThroughput", () => { const mockCollection4 = { id: ko.observable("collection4") } as Collection; const mockDatabase = {} as Database; const mockContainer = { - databases: ko.observableArray([mockDatabase]) + databases: ko.observableArray([mockDatabase]), } as Explorer; it("less than or equal to collection threshold", () => { @@ -56,7 +56,7 @@ describe("getMaxThroughput", () => { mockCollection1, mockCollection2, mockCollection3, - mockCollection4 + mockCollection4, ]); expect(getMaxThroughput(defaults, mockContainer)).toEqual(defaults.throughput.unlimited.greatThanThreshold); }); diff --git a/src/Shared/AddCollectionUtility.ts b/src/Shared/AddCollectionUtility.ts index e43a0b617..c81319a62 100644 --- a/src/Shared/AddCollectionUtility.ts +++ b/src/Shared/AddCollectionUtility.ts @@ -1,23 +1,23 @@ -import { any } from "underscore"; -import { CollectionCreationDefaults } from "../Contracts/ViewModels"; -import Explorer from "../Explorer/Explorer"; - -export const getMaxThroughput = (defaults: CollectionCreationDefaults, container: Explorer): number => { - const throughput = defaults.throughput.unlimited; - if (typeof throughput === "number") { - return throughput; - } else { - return _exceedsThreshold(throughput.collectionThreshold, container) - ? throughput.greatThanThreshold - : throughput.lessThanOrEqualToThreshold; - } -}; - -const _exceedsThreshold = (unlimitedThreshold: number, container: Explorer): boolean => { - const databases = (container && container.databases && container.databases()) || []; - return any( - databases, - database => - database && database.collections && database.collections() && database.collections().length > unlimitedThreshold - ); -}; +import { any } from "underscore"; +import { CollectionCreationDefaults } from "../Contracts/ViewModels"; +import Explorer from "../Explorer/Explorer"; + +export const getMaxThroughput = (defaults: CollectionCreationDefaults, container: Explorer): number => { + const throughput = defaults.throughput.unlimited; + if (typeof throughput === "number") { + return throughput; + } else { + return _exceedsThreshold(throughput.collectionThreshold, container) + ? throughput.greatThanThreshold + : throughput.lessThanOrEqualToThreshold; + } +}; + +const _exceedsThreshold = (unlimitedThreshold: number, container: Explorer): boolean => { + const databases = (container && container.databases && container.databases()) || []; + return any( + databases, + (database) => + database && database.collections && database.collections() && database.collections().length > unlimitedThreshold + ); +}; diff --git a/src/Shared/Constants.ts b/src/Shared/Constants.ts index 7aae90063..b242cb67f 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -1,239 +1,239 @@ -import { SubscriptionType } from "../Contracts/SubscriptionType"; - -export const hoursInAMonth = 730; -export class AutoscalePricing { - public static MonthlyPricing = { - default: { - singleMaster: { - Currency: "USD", - CurrencySign: "$", - Standard: { - StartingPrice: 24, - PricePerRU: 0.09, - PricePerGB: 0.25 - } - }, - multiMaster: { - Currency: "USD", - CurrencySign: "$", - Standard: { - StartingPrice: 24, - PricePerRU: 0.12, - PricePerGB: 0.25 - } - } - }, - mooncake: { - singleMaster: { - Currency: "RMB", - CurrencySign: "¥", - Standard: { - StartingPrice: 152, - PricePerRU: 0.57, - PricePerGB: 2.576 - } - }, - multiMaster: { - Currency: "RMB", - CurrencySign: "¥", - Standard: { - StartingPrice: 152, - PricePerRU: 0.76, - PricePerGB: 2.576 - } - } - } - }; - - public static HourlyPricing = { - default: { - singleMaster: { - Currency: "USD", - CurrencySign: "$", - Standard: { - StartingPrice: 24 / hoursInAMonth, - PricePerRU: 0.00012, - PricePerGB: 0.25 / hoursInAMonth - } - }, - multiMaster: { - Currency: "USD", - CurrencySign: "$", - Standard: { - StartingPrice: 24 / hoursInAMonth, - PricePerRU: 0.00016, - PricePerGB: 0.25 / hoursInAMonth - } - } - }, - mooncake: { - singleMaster: { - Currency: "RMB", - CurrencySign: "¥", - Standard: { - StartingPrice: AutoscalePricing.MonthlyPricing.mooncake.singleMaster.Standard.StartingPrice / hoursInAMonth, // per hour - PricePerRU: 0.000765, - PricePerGB: AutoscalePricing.MonthlyPricing.mooncake.singleMaster.Standard.PricePerGB / hoursInAMonth - } - }, - multiMaster: { - Currency: "RMB", - CurrencySign: "¥", - Standard: { - StartingPrice: AutoscalePricing.MonthlyPricing.mooncake.multiMaster.Standard.StartingPrice / hoursInAMonth, // per hour - PricePerRU: 0.00102, - PricePerGB: AutoscalePricing.MonthlyPricing.mooncake.multiMaster.Standard.PricePerGB / hoursInAMonth - } - } - } - }; -} - -export class OfferPricing { - public static MonthlyPricing = { - default: { - Currency: "USD", - CurrencySign: "$", - S1Price: 25, - S2Price: 50, - S3Price: 100, - Standard: { - StartingPrice: 24, - PricePerRU: 0.06, - PricePerGB: 0.25 - } - }, - mooncake: { - Currency: "RMB", - CurrencySign: "¥", - S1Price: 110.3, - S2Price: 220.6, - S3Price: 441.2, - Standard: { - StartingPrice: 152, - PricePerRU: 0.3794, - PricePerGB: 2.576 - } - } - }; - public static HourlyPricing = { - default: { - Currency: "USD", - CurrencySign: "$", - S1Price: 0.0336, - S2Price: 0.0672, - S3Price: 0.1344, - Standard: { - StartingPrice: 24 / hoursInAMonth, // per hour - PricePerRU: 0.00008, - PricePerGB: 0.25 / hoursInAMonth - } - }, - mooncake: { - Currency: "RMB", - CurrencySign: "¥", - S1Price: 0.15, - S2Price: 0.3, - S3Price: 0.6, - Standard: { - StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour - PricePerRU: 0.00051, - PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth - } - } - }; -} - -export class CollectionCreation { - // TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml - public static readonly MinRUPerPartitionBelow7Partitions: number = 400; - public static readonly MinRU7PartitionsTo25Partitions: number = 2500; - public static readonly MinRUPerPartitionAbove25Partitions: number = 100; - public static readonly MaxRUPerPartition: number = 10000; - public static readonly MinPartitionedCollectionRUs: number = 2500; - - public static readonly NumberOfPartitionsInFixedCollection: number = 1; - public static readonly NumberOfPartitionsInUnlimitedCollection: number = 10; - - public static storage10Gb: string = "10"; - public static storage100Gb: string = "100"; - - public static readonly DefaultCollectionRUs1000: number = 1000; - public static readonly DefaultCollectionRUs10K: number = 10000; - public static readonly DefaultCollectionRUs400: number = 400; - public static readonly DefaultCollectionRUs2000: number = 2000; - public static readonly DefaultCollectionRUs2500: number = 2500; - public static readonly DefaultCollectionRUs5000: number = 5000; - public static readonly DefaultCollectionRUs15000: number = 15000; - public static readonly DefaultCollectionRUs20000: number = 20000; - public static readonly DefaultCollectionRUs25000: number = 25000; - public static readonly DefaultCollectionRUs100K: number = 100000; - public static readonly DefaultCollectionRUs1Million: number = 1000000; - - public static readonly DefaultAddCollectionDefaultFlight: string = "0"; - public static readonly DefaultSubscriptionType: SubscriptionType = SubscriptionType.Free; - - public static readonly TablesAPIDefaultDatabase: string = "TablesDB"; -} - -export const CollectionCreationDefaults = { - storage: CollectionCreation.storage100Gb, - throughput: { - fixed: CollectionCreation.DefaultCollectionRUs400, - unlimited: CollectionCreation.DefaultCollectionRUs400, - unlimitedmax: CollectionCreation.DefaultCollectionRUs1Million, - unlimitedmin: CollectionCreation.DefaultCollectionRUs400, - shared: CollectionCreation.DefaultCollectionRUs400 - } -} as const; - -export class IndexingPolicies { - public static SharedDatabaseDefault = { - indexingMode: "consistent", - automatic: true, - includedPaths: [], - excludedPaths: [ - { - path: "/*" - } - ] - }; - - public static AllPropertiesIndexed = { - indexingMode: "consistent", - automatic: true, - includedPaths: [ - { - path: "/*", - indexes: [ - { - kind: "Range", - dataType: "Number", - precision: -1 - }, - { - kind: "Range", - dataType: "String", - precision: -1 - } - ] - } - ], - excludedPaths: [] - }; -} - -export class SubscriptionUtilMappings { - public static FreeTierSubscriptionIds: string[] = [ - "b8f2ff04-0a81-4cf9-95ef-5828d16981d2", - "39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea", - "41f6d14d-ece1-46e4-942c-02c00d67f7d6", - "11dc62e3-77dc-4ef5-a46b-480ec6caa8fe", - "199d0919-60bd-448e-b64d-8461a0fe9747", - "a57b6849-d443-44cf-a3b7-7dd07ead9401" - ]; -} - -export class AutopilotDocumentation { - public static Url: string = "https://aka.ms/cosmos-autoscale-info"; -} +import { SubscriptionType } from "../Contracts/SubscriptionType"; + +export const hoursInAMonth = 730; +export class AutoscalePricing { + public static MonthlyPricing = { + default: { + singleMaster: { + Currency: "USD", + CurrencySign: "$", + Standard: { + StartingPrice: 24, + PricePerRU: 0.09, + PricePerGB: 0.25, + }, + }, + multiMaster: { + Currency: "USD", + CurrencySign: "$", + Standard: { + StartingPrice: 24, + PricePerRU: 0.12, + PricePerGB: 0.25, + }, + }, + }, + mooncake: { + singleMaster: { + Currency: "RMB", + CurrencySign: "¥", + Standard: { + StartingPrice: 152, + PricePerRU: 0.57, + PricePerGB: 2.576, + }, + }, + multiMaster: { + Currency: "RMB", + CurrencySign: "¥", + Standard: { + StartingPrice: 152, + PricePerRU: 0.76, + PricePerGB: 2.576, + }, + }, + }, + }; + + public static HourlyPricing = { + default: { + singleMaster: { + Currency: "USD", + CurrencySign: "$", + Standard: { + StartingPrice: 24 / hoursInAMonth, + PricePerRU: 0.00012, + PricePerGB: 0.25 / hoursInAMonth, + }, + }, + multiMaster: { + Currency: "USD", + CurrencySign: "$", + Standard: { + StartingPrice: 24 / hoursInAMonth, + PricePerRU: 0.00016, + PricePerGB: 0.25 / hoursInAMonth, + }, + }, + }, + mooncake: { + singleMaster: { + Currency: "RMB", + CurrencySign: "¥", + Standard: { + StartingPrice: AutoscalePricing.MonthlyPricing.mooncake.singleMaster.Standard.StartingPrice / hoursInAMonth, // per hour + PricePerRU: 0.000765, + PricePerGB: AutoscalePricing.MonthlyPricing.mooncake.singleMaster.Standard.PricePerGB / hoursInAMonth, + }, + }, + multiMaster: { + Currency: "RMB", + CurrencySign: "¥", + Standard: { + StartingPrice: AutoscalePricing.MonthlyPricing.mooncake.multiMaster.Standard.StartingPrice / hoursInAMonth, // per hour + PricePerRU: 0.00102, + PricePerGB: AutoscalePricing.MonthlyPricing.mooncake.multiMaster.Standard.PricePerGB / hoursInAMonth, + }, + }, + }, + }; +} + +export class OfferPricing { + public static MonthlyPricing = { + default: { + Currency: "USD", + CurrencySign: "$", + S1Price: 25, + S2Price: 50, + S3Price: 100, + Standard: { + StartingPrice: 24, + PricePerRU: 0.06, + PricePerGB: 0.25, + }, + }, + mooncake: { + Currency: "RMB", + CurrencySign: "¥", + S1Price: 110.3, + S2Price: 220.6, + S3Price: 441.2, + Standard: { + StartingPrice: 152, + PricePerRU: 0.3794, + PricePerGB: 2.576, + }, + }, + }; + public static HourlyPricing = { + default: { + Currency: "USD", + CurrencySign: "$", + S1Price: 0.0336, + S2Price: 0.0672, + S3Price: 0.1344, + Standard: { + StartingPrice: 24 / hoursInAMonth, // per hour + PricePerRU: 0.00008, + PricePerGB: 0.25 / hoursInAMonth, + }, + }, + mooncake: { + Currency: "RMB", + CurrencySign: "¥", + S1Price: 0.15, + S2Price: 0.3, + S3Price: 0.6, + Standard: { + StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour + PricePerRU: 0.00051, + PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth, + }, + }, + }; +} + +export class CollectionCreation { + // TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml + public static readonly MinRUPerPartitionBelow7Partitions: number = 400; + public static readonly MinRU7PartitionsTo25Partitions: number = 2500; + public static readonly MinRUPerPartitionAbove25Partitions: number = 100; + public static readonly MaxRUPerPartition: number = 10000; + public static readonly MinPartitionedCollectionRUs: number = 2500; + + public static readonly NumberOfPartitionsInFixedCollection: number = 1; + public static readonly NumberOfPartitionsInUnlimitedCollection: number = 10; + + public static storage10Gb: string = "10"; + public static storage100Gb: string = "100"; + + public static readonly DefaultCollectionRUs1000: number = 1000; + public static readonly DefaultCollectionRUs10K: number = 10000; + public static readonly DefaultCollectionRUs400: number = 400; + public static readonly DefaultCollectionRUs2000: number = 2000; + public static readonly DefaultCollectionRUs2500: number = 2500; + public static readonly DefaultCollectionRUs5000: number = 5000; + public static readonly DefaultCollectionRUs15000: number = 15000; + public static readonly DefaultCollectionRUs20000: number = 20000; + public static readonly DefaultCollectionRUs25000: number = 25000; + public static readonly DefaultCollectionRUs100K: number = 100000; + public static readonly DefaultCollectionRUs1Million: number = 1000000; + + public static readonly DefaultAddCollectionDefaultFlight: string = "0"; + public static readonly DefaultSubscriptionType: SubscriptionType = SubscriptionType.Free; + + public static readonly TablesAPIDefaultDatabase: string = "TablesDB"; +} + +export const CollectionCreationDefaults = { + storage: CollectionCreation.storage100Gb, + throughput: { + fixed: CollectionCreation.DefaultCollectionRUs400, + unlimited: CollectionCreation.DefaultCollectionRUs400, + unlimitedmax: CollectionCreation.DefaultCollectionRUs1Million, + unlimitedmin: CollectionCreation.DefaultCollectionRUs400, + shared: CollectionCreation.DefaultCollectionRUs400, + }, +} as const; + +export class IndexingPolicies { + public static SharedDatabaseDefault = { + indexingMode: "consistent", + automatic: true, + includedPaths: [], + excludedPaths: [ + { + path: "/*", + }, + ], + }; + + public static AllPropertiesIndexed = { + indexingMode: "consistent", + automatic: true, + includedPaths: [ + { + path: "/*", + indexes: [ + { + kind: "Range", + dataType: "Number", + precision: -1, + }, + { + kind: "Range", + dataType: "String", + precision: -1, + }, + ], + }, + ], + excludedPaths: [], + }; +} + +export class SubscriptionUtilMappings { + public static FreeTierSubscriptionIds: string[] = [ + "b8f2ff04-0a81-4cf9-95ef-5828d16981d2", + "39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea", + "41f6d14d-ece1-46e4-942c-02c00d67f7d6", + "11dc62e3-77dc-4ef5-a46b-480ec6caa8fe", + "199d0919-60bd-448e-b64d-8461a0fe9747", + "a57b6849-d443-44cf-a3b7-7dd07ead9401", + ]; +} + +export class AutopilotDocumentation { + public static Url: string = "https://aka.ms/cosmos-autoscale-info"; +} diff --git a/src/Shared/DefaultExperienceUtility.test.ts b/src/Shared/DefaultExperienceUtility.test.ts index 372f54c02..ece1d6e88 100644 --- a/src/Shared/DefaultExperienceUtility.test.ts +++ b/src/Shared/DefaultExperienceUtility.test.ts @@ -83,7 +83,7 @@ describe("Default Experience Utility", () => { location: "somewhere", type: "DocumentDB", tags: { - defaultExperience: "Gremlin (graph)" + defaultExperience: "Gremlin (graph)", }, properties: { documentEndpoint: "", @@ -93,10 +93,10 @@ describe("Default Experience Utility", () => { capabilities: [ { name: Constants.CapabilityNames.EnableGremlin, - description: "something" - } - ] - } + description: "something", + }, + ], + }, }; const databaseAccountWithApiKind: DataModels.DatabaseAccount = { @@ -114,10 +114,10 @@ describe("Default Experience Utility", () => { capabilities: [ { name: Constants.CapabilityNames.EnableGremlin, - description: "something" - } - ] - } + description: "something", + }, + ], + }, }; describe("Disregard tags", () => { diff --git a/src/Shared/DefaultExperienceUtility.ts b/src/Shared/DefaultExperienceUtility.ts index 4007296b0..376cffcac 100644 --- a/src/Shared/DefaultExperienceUtility.ts +++ b/src/Shared/DefaultExperienceUtility.ts @@ -145,7 +145,7 @@ export class DefaultExperienceUtility { } private static _findCapability(capabilities: DataModels.Capability[], capabilityName: string): DataModels.Capability { - return _.find(capabilities, capability => { + return _.find(capabilities, (capability) => { return capability && capability.name && capability.name.toLowerCase() === capabilityName.toLowerCase(); }); } diff --git a/src/Shared/PriceEstimateCalculator.ts b/src/Shared/PriceEstimateCalculator.ts index d7c30d695..2e897ea66 100644 --- a/src/Shared/PriceEstimateCalculator.ts +++ b/src/Shared/PriceEstimateCalculator.ts @@ -1,43 +1,43 @@ -import * as Constants from "./Constants"; - -export function computeRUUsagePrice(serverId: string, requestUnits: number): string { - if (serverId === "mooncake") { - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; - return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; - } - - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; - return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; -} - -export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string { - if (serverId === "mooncake") { - let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerGB; - return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; - } - - let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerGB; - return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; -} - -export function computeDisplayUsageString(usageInKB: number): string { - let usageInMB = usageInKB / 1024, - usageInGB = usageInMB / 1024, - displayUsageString = - usageInGB > 0.1 - ? usageInGB.toFixed(2) + " GB" - : usageInMB > 0.1 - ? usageInMB.toFixed(2) + " MB" - : usageInKB.toFixed(2) + " KB"; - return displayUsageString; -} - -export function usageInGB(usageInKB: number): number { - let usageInMB = usageInKB / 1024, - usageInGB = usageInMB / 1024; - return Math.ceil(usageInGB); -} - -function calculateEstimateNumber(n: number): string { - return n >= 1 ? n.toFixed(2) : n.toPrecision(2); -} +import * as Constants from "./Constants"; + +export function computeRUUsagePrice(serverId: string, requestUnits: number): string { + if (serverId === "mooncake") { + let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; + return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; + } + + let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; + return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; +} + +export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string { + if (serverId === "mooncake") { + let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerGB; + return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; + } + + let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerGB; + return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; +} + +export function computeDisplayUsageString(usageInKB: number): string { + let usageInMB = usageInKB / 1024, + usageInGB = usageInMB / 1024, + displayUsageString = + usageInGB > 0.1 + ? usageInGB.toFixed(2) + " GB" + : usageInMB > 0.1 + ? usageInMB.toFixed(2) + " MB" + : usageInKB.toFixed(2) + " KB"; + return displayUsageString; +} + +export function usageInGB(usageInKB: number): number { + let usageInMB = usageInKB / 1024, + usageInGB = usageInMB / 1024; + return Math.ceil(usageInGB); +} + +function calculateEstimateNumber(n: number): string { + return n >= 1 ? n.toFixed(2) : n.toPrecision(2); +} diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index 64803e8f6..09cd64c89 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -79,5 +79,5 @@ export enum StorageKey { TenantId, MostRecentActivity, SetPartitionKeyUndefined, - GalleryCalloutDismissed + GalleryCalloutDismissed, } diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index c794fb1b0..061b8052d 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -1,136 +1,136 @@ -// Data Explorer specific actions. No need to keep this in sync with the one in Portal. -export enum Action { - CollapseTreeNode, - CreateCollection, - CreateDocument, - CreateStoredProcedure, - CreateTrigger, - CreateUDF, - DeleteCollection, - DeleteDatabase, - DeleteDocument, - ExpandTreeNode, - ExecuteQuery, - HasFeature, - GetVNETServices, - InitializeAccountLocationFromResourceGroup, - InitializeDataExplorer, - LoadDatabaseAccount, - LoadCollections, - LoadDatabases, - LoadOffers, - MongoShell, - ContextualPane, - ScaleThroughput, - ToggleAutoscaleSetting, - SelectItem, - Tab, - UpdateDocument, - UpdateSettings, - UpdateStoredProcedure, - UpdateTrigger, - UpdateUDF, - LoadResourceTree, - CreateDatabase, - ResolveConflict, - DeleteConflict, - SaveQuery, - SetupSavedQueries, - LoadSavedQuery, - DeleteSavedQuery, - ConnectEncryptionToken, - SignInAad, - SignOutAad, - FetchTenants, - FetchSubscriptions, - FetchAccounts, - GetAccountKeys, - LoadingStatus, - AccountSwitch, - SubscriptionSwitch, - TenantSwitch, - DefaultTenantSwitch, - ResetNotebookWorkspace, - CreateNotebookWorkspace, - NotebookErrorNotification, - CreateSparkCluster, - UpdateSparkCluster, - DeleteSparkCluster, - LibraryManage, - ClusterLibraryManage, - ModifyOptionForThroughputWithSharedDatabase, - EnableAzureSynapseLink, - CreateNewNotebook, - OpenSampleNotebook, - ExecuteCell, - ExecuteCellPromptBtn, - ExecuteAllCells, - NotebookEnabled, - NotebooksGitHubConnect, - NotebooksGitHubAuthorize, - NotebooksGitHubManualRepoAdd, - NotebooksGitHubManageRepo, - NotebooksGitHubCommit, - NotebooksGitHubDisconnect, - NotebooksFetched, - NotebooksKernelSpecName, - NotebooksExecuteCellFromMenu, - NotebooksClearOutputsFromMenu, - NotebooksInsertCodeCellAboveFromMenu, - NotebooksInsertCodeCellBelowFromMenu, - NotebooksInsertTextCellAboveFromMenu, - NotebooksInsertTextCellBelowFromMenu, - NotebooksMoveCellUpFromMenu, - NotebooksMoveCellDownFromMenu, - DeleteCellFromMenu, - OpenTerminal, - CreateMongoCollectionWithWildcardIndex, - ClickCommandBarButton, - RefreshResourceTreeMyNotebooks, - ClickResourceTreeNodeContextMenuItem, - DiscardSettings, - SettingsV2Updated, - SettingsV2Discarded, - MongoIndexUpdated -} - -export const ActionModifiers = { - Start: "start", - Success: "success", - Failed: "failed", - Mark: "mark", - Open: "open", - IFrameReady: "iframeready", - Close: "close", - Submit: "submit", - IndexAll: "index all properties", - NoIndex: "no indexing", - Cancel: "cancel", - ToggleAutoscaleOn: "autoscale on", - ToggleAutoscaleOff: "autoscale off" -} as const; - -export enum SourceBlade { - AddCollection, - AzureFunction, - BrowseCollectionBlade, - CassandraAccountCreateBlade, - CollectionSetting, - DatabaseAccountCreateBlade, - DataExplorer, - DeleteCollection, - DeleteDatabase, - DocumentExplorer, - FirewallVNETBlade, - Metrics, - NonDocumentDBAccountCreateBlade, - OverviewBlade, - QueryExplorer, - Quickstart, - ReaderWarning, - ResourceMenu, - RpcProvider, - ScaleCollection, - ScriptExplorer, - Keys -} +// Data Explorer specific actions. No need to keep this in sync with the one in Portal. +export enum Action { + CollapseTreeNode, + CreateCollection, + CreateDocument, + CreateStoredProcedure, + CreateTrigger, + CreateUDF, + DeleteCollection, + DeleteDatabase, + DeleteDocument, + ExpandTreeNode, + ExecuteQuery, + HasFeature, + GetVNETServices, + InitializeAccountLocationFromResourceGroup, + InitializeDataExplorer, + LoadDatabaseAccount, + LoadCollections, + LoadDatabases, + LoadOffers, + MongoShell, + ContextualPane, + ScaleThroughput, + ToggleAutoscaleSetting, + SelectItem, + Tab, + UpdateDocument, + UpdateSettings, + UpdateStoredProcedure, + UpdateTrigger, + UpdateUDF, + LoadResourceTree, + CreateDatabase, + ResolveConflict, + DeleteConflict, + SaveQuery, + SetupSavedQueries, + LoadSavedQuery, + DeleteSavedQuery, + ConnectEncryptionToken, + SignInAad, + SignOutAad, + FetchTenants, + FetchSubscriptions, + FetchAccounts, + GetAccountKeys, + LoadingStatus, + AccountSwitch, + SubscriptionSwitch, + TenantSwitch, + DefaultTenantSwitch, + ResetNotebookWorkspace, + CreateNotebookWorkspace, + NotebookErrorNotification, + CreateSparkCluster, + UpdateSparkCluster, + DeleteSparkCluster, + LibraryManage, + ClusterLibraryManage, + ModifyOptionForThroughputWithSharedDatabase, + EnableAzureSynapseLink, + CreateNewNotebook, + OpenSampleNotebook, + ExecuteCell, + ExecuteCellPromptBtn, + ExecuteAllCells, + NotebookEnabled, + NotebooksGitHubConnect, + NotebooksGitHubAuthorize, + NotebooksGitHubManualRepoAdd, + NotebooksGitHubManageRepo, + NotebooksGitHubCommit, + NotebooksGitHubDisconnect, + NotebooksFetched, + NotebooksKernelSpecName, + NotebooksExecuteCellFromMenu, + NotebooksClearOutputsFromMenu, + NotebooksInsertCodeCellAboveFromMenu, + NotebooksInsertCodeCellBelowFromMenu, + NotebooksInsertTextCellAboveFromMenu, + NotebooksInsertTextCellBelowFromMenu, + NotebooksMoveCellUpFromMenu, + NotebooksMoveCellDownFromMenu, + DeleteCellFromMenu, + OpenTerminal, + CreateMongoCollectionWithWildcardIndex, + ClickCommandBarButton, + RefreshResourceTreeMyNotebooks, + ClickResourceTreeNodeContextMenuItem, + DiscardSettings, + SettingsV2Updated, + SettingsV2Discarded, + MongoIndexUpdated, +} + +export const ActionModifiers = { + Start: "start", + Success: "success", + Failed: "failed", + Mark: "mark", + Open: "open", + IFrameReady: "iframeready", + Close: "close", + Submit: "submit", + IndexAll: "index all properties", + NoIndex: "no indexing", + Cancel: "cancel", + ToggleAutoscaleOn: "autoscale on", + ToggleAutoscaleOff: "autoscale off", +} as const; + +export enum SourceBlade { + AddCollection, + AzureFunction, + BrowseCollectionBlade, + CassandraAccountCreateBlade, + CollectionSetting, + DatabaseAccountCreateBlade, + DataExplorer, + DeleteCollection, + DeleteDatabase, + DocumentExplorer, + FirewallVNETBlade, + Metrics, + NonDocumentDBAccountCreateBlade, + OverviewBlade, + QueryExplorer, + Quickstart, + ReaderWarning, + ResourceMenu, + RpcProvider, + ScaleCollection, + ScriptExplorer, + Keys, +} diff --git a/src/Shared/Telemetry/TelemetryProcessor.ts b/src/Shared/Telemetry/TelemetryProcessor.ts index be484367f..fe7675b5c 100644 --- a/src/Shared/Telemetry/TelemetryProcessor.ts +++ b/src/Shared/Telemetry/TelemetryProcessor.ts @@ -18,8 +18,8 @@ export function trace(action: Action, actionModifier: string = ActionModifiers.M data: { action: Action[action], actionModifier: actionModifier, - data: JSON.stringify(data) - } + data: JSON.stringify(data), + }, }); appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data)); @@ -33,8 +33,8 @@ export function traceStart(action: Action, data?: TelemetryData): number { action: Action[action], actionModifier: ActionModifiers.Start, timestamp: timestamp, - data: JSON.stringify(data) - } + data: JSON.stringify(data), + }, }); appInsights.startTrackEvent(Action[action]); @@ -48,8 +48,8 @@ export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: n action: Action[action], actionModifier: ActionModifiers.Success, timestamp: timestamp || Date.now(), - data: JSON.stringify(data) - } + data: JSON.stringify(data), + }, }); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data)); @@ -62,8 +62,8 @@ export function traceFailure(action: Action, data?: TelemetryData, timestamp?: n action: Action[action], actionModifier: ActionModifiers.Failed, timestamp: timestamp || Date.now(), - data: JSON.stringify(data) - } + data: JSON.stringify(data), + }, }); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data)); @@ -76,8 +76,8 @@ export function traceCancel(action: Action, data?: TelemetryData, timestamp?: nu action: Action[action], actionModifier: ActionModifiers.Cancel, timestamp: timestamp || Date.now(), - data: JSON.stringify(data) - } + data: JSON.stringify(data), + }, }); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data)); @@ -91,8 +91,8 @@ export function traceOpen(action: Action, data?: TelemetryData, timestamp?: numb action: Action[action], actionModifier: ActionModifiers.Open, timestamp: validTimestamp, - data: JSON.stringify(data) - } + data: JSON.stringify(data), + }, }); appInsights.startTrackEvent(Action[action]); @@ -107,8 +107,8 @@ export function traceMark(action: Action, data?: TelemetryData, timestamp?: numb action: Action[action], actionModifier: ActionModifiers.Mark, timestamp: validTimestamp, - data: JSON.stringify(data) - } + data: JSON.stringify(data), + }, }); appInsights.startTrackEvent(Action[action]); @@ -125,6 +125,6 @@ function getData(actionModifier: string, data: TelemetryData = {}): { [key: stri platform: configContext.platform, env: process.env.NODE_ENV as string, actionModifier, - ...data + ...data, }; } diff --git a/src/Shared/appInsights.ts b/src/Shared/appInsights.ts index 5fe708eec..dd18682d5 100644 --- a/src/Shared/appInsights.ts +++ b/src/Shared/appInsights.ts @@ -4,8 +4,8 @@ const appInsights = new ApplicationInsights({ config: { instrumentationKey: "fa645d97-6237-4656-9559-0ee0cb55ee49", disableFetchTracking: false, - disableCorrelationHeaders: true - } + disableCorrelationHeaders: true, + }, }); appInsights.loadAppInsights(); appInsights.trackPageView(); // Manually call trackPageView to establish the current user/session/pageview diff --git a/src/SparkClusterManager/ArcadiaResourceManager.ts b/src/SparkClusterManager/ArcadiaResourceManager.ts index b16121ec0..cbfa68a58 100644 --- a/src/SparkClusterManager/ArcadiaResourceManager.ts +++ b/src/SparkClusterManager/ArcadiaResourceManager.ts @@ -1,78 +1,78 @@ -import { - ArcadiaWorkspace, - ArcadiaWorkspaceFeedResponse, - SparkPool, - SparkPoolFeedResponse -} from "../Contracts/DataModels"; -import { ArmApiVersions, ArmResourceTypes } from "../Common/Constants"; -import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient"; -import * as Logger from "../Common/Logger"; -import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; - -export class ArcadiaResourceManager { - private resourceProviderClientFactory: IResourceProviderClientFactory; - - constructor() { - this.resourceProviderClientFactory = new ResourceProviderClientFactory(); - } - - public async getWorkspacesAsync(arcadiaResourceId: string): Promise { - const uri = `${arcadiaResourceId}/workspaces`; - try { - const response = (await this._rpClient(uri).getAsync( - uri, - ArmApiVersions.arcadia - )) as ArcadiaWorkspaceFeedResponse; - return response && response.value; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArcadiaResourceManager/getWorkspaceAsync"); - throw error; - } - } - - public async getWorkspaceAsync(arcadiaResourceId: string, workspaceId: string): Promise { - const uri = `${arcadiaResourceId}/workspaces/${workspaceId}`; - try { - return (await this._rpClient(uri).getAsync(uri, ArmApiVersions.arcadia)) as ArcadiaWorkspace; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArcadiaResourceManager/getWorkspaceAsync"); - throw error; - } - } - - public async listWorkspacesAsync(subscriptionIds: string[]): Promise { - let uriFilter = `$filter=(resourceType eq '${ArmResourceTypes.synapseWorkspaces.toLowerCase()}')`; - if (subscriptionIds && subscriptionIds.length) { - uriFilter += ` and (${"subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'"})`; - } - const uri = "/resources"; - try { - const response = (await this._rpClient(uri + uriFilter).getAsync( - uri, - ArmApiVersions.arm, - uriFilter - )) as ArcadiaWorkspaceFeedResponse; - return response && response.value; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArcadiaManager/listWorkspacesAsync"); - throw error; - } - } - - public async listSparkPoolsAsync(resourceId: string): Promise { - let uri = `${resourceId}/bigDataPools`; - - try { - const response = (await this._rpClient(uri).getAsync(uri, ArmApiVersions.arcadia)) as SparkPoolFeedResponse; - return response && response.value; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArcadiaManager/listSparkPoolsAsync"); - throw error; - } - } - - private _rpClient(uri: string): IResourceProviderClient { - return this.resourceProviderClientFactory.getOrCreate(uri); - } -} +import { + ArcadiaWorkspace, + ArcadiaWorkspaceFeedResponse, + SparkPool, + SparkPoolFeedResponse, +} from "../Contracts/DataModels"; +import { ArmApiVersions, ArmResourceTypes } from "../Common/Constants"; +import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient"; +import * as Logger from "../Common/Logger"; +import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; + +export class ArcadiaResourceManager { + private resourceProviderClientFactory: IResourceProviderClientFactory; + + constructor() { + this.resourceProviderClientFactory = new ResourceProviderClientFactory(); + } + + public async getWorkspacesAsync(arcadiaResourceId: string): Promise { + const uri = `${arcadiaResourceId}/workspaces`; + try { + const response = (await this._rpClient(uri).getAsync( + uri, + ArmApiVersions.arcadia + )) as ArcadiaWorkspaceFeedResponse; + return response && response.value; + } catch (error) { + Logger.logError(getErrorMessage(error), "ArcadiaResourceManager/getWorkspaceAsync"); + throw error; + } + } + + public async getWorkspaceAsync(arcadiaResourceId: string, workspaceId: string): Promise { + const uri = `${arcadiaResourceId}/workspaces/${workspaceId}`; + try { + return (await this._rpClient(uri).getAsync(uri, ArmApiVersions.arcadia)) as ArcadiaWorkspace; + } catch (error) { + Logger.logError(getErrorMessage(error), "ArcadiaResourceManager/getWorkspaceAsync"); + throw error; + } + } + + public async listWorkspacesAsync(subscriptionIds: string[]): Promise { + let uriFilter = `$filter=(resourceType eq '${ArmResourceTypes.synapseWorkspaces.toLowerCase()}')`; + if (subscriptionIds && subscriptionIds.length) { + uriFilter += ` and (${"subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'"})`; + } + const uri = "/resources"; + try { + const response = (await this._rpClient(uri + uriFilter).getAsync( + uri, + ArmApiVersions.arm, + uriFilter + )) as ArcadiaWorkspaceFeedResponse; + return response && response.value; + } catch (error) { + Logger.logError(getErrorMessage(error), "ArcadiaManager/listWorkspacesAsync"); + throw error; + } + } + + public async listSparkPoolsAsync(resourceId: string): Promise { + let uri = `${resourceId}/bigDataPools`; + + try { + const response = (await this._rpClient(uri).getAsync(uri, ArmApiVersions.arcadia)) as SparkPoolFeedResponse; + return response && response.value; + } catch (error) { + Logger.logError(getErrorMessage(error), "ArcadiaManager/listSparkPoolsAsync"); + throw error; + } + } + + private _rpClient(uri: string): IResourceProviderClient { + return this.resourceProviderClientFactory.getOrCreate(uri); + } +} diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts index 5ab17e41e..a1eef5bbf 100644 --- a/src/Terminal/JupyterLabAppFactory.ts +++ b/src/Terminal/JupyterLabAppFactory.ts @@ -8,7 +8,7 @@ import { Panel, Widget } from "@phosphor/widgets"; export class JupyterLabAppFactory { public static async createTerminalApp(serverSettings: ServerConnection.ISettings) { const manager = new TerminalManager({ - serverSettings: serverSettings + serverSettings: serverSettings, }); const session = await manager.startNew(); const term = new Terminal(session, { theme: "dark", shutdownOnClose: true }); diff --git a/src/Terminal/NotebookAppContracts.d.ts b/src/Terminal/NotebookAppContracts.d.ts index ba8a6bac3..4b33b5332 100644 --- a/src/Terminal/NotebookAppContracts.d.ts +++ b/src/Terminal/NotebookAppContracts.d.ts @@ -69,7 +69,7 @@ export interface KernelSpecs { } export declare enum ActionTypes { Update = 0, - Response = 1 + Response = 1, } /** * Messages Data Explorer -> JupyterLabApp @@ -99,7 +99,7 @@ export declare enum MessageTypes { Status = 21, KernelList = 22, IsDirty = 23, - Shutdown = 24 + Shutdown = 24, } export declare enum NotebookUpdateTypes { Ready = 0, @@ -107,5 +107,5 @@ export declare enum NotebookUpdateTypes { ActiveCellType = 2, KernelChange = 3, FileSaved = 4, - SessionStatusChange = 5 + SessionStatusChange = 5, } diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index 3a7ae8913..5994d7057 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -23,10 +23,10 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect let headers: HeadersInit | undefined; if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) { body = JSON.stringify({ - endpoint: urlVars[TerminalQueryParams.TerminalEndpoint] + endpoint: urlVars[TerminalQueryParams.TerminalEndpoint], }); headers = { - [HttpHeaders.contentType]: "application/json" + [HttpHeaders.contentType]: "application/json", }; } @@ -34,7 +34,7 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect let options: Partial = { baseUrl: server, init: { body, headers }, - fetch: window.parent.fetch + fetch: window.parent.fetch, }; if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) { options = { @@ -42,7 +42,7 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect token: urlVars[TerminalQueryParams.Token], appendToken: true, init: { body, headers }, - fetch: window.parent.fetch + fetch: window.parent.fetch, }; } @@ -54,7 +54,7 @@ const main = async (): Promise => { // Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor updateUserContext({ - subscriptionId: urlVars[TerminalQueryParams.SubscriptionId] + subscriptionId: urlVars[TerminalQueryParams.SubscriptionId], }); const serverSettings = createServerSettings(urlVars); diff --git a/src/TokenProviders/PortalTokenProvider.ts b/src/TokenProviders/PortalTokenProvider.ts index 89a5494dd..c7b1480ad 100644 --- a/src/TokenProviders/PortalTokenProvider.ts +++ b/src/TokenProviders/PortalTokenProvider.ts @@ -1,13 +1,13 @@ -import * as ViewModels from "../Contracts/ViewModels"; -import { userContext } from "../UserContext"; - -export class PortalTokenProvider implements ViewModels.TokenProvider { - constructor() {} - - public async getAuthHeader(): Promise { - const bearerToken = userContext.authorizationToken; - let fetchHeaders = new Headers(); - fetchHeaders.append("authorization", bearerToken); - return fetchHeaders; - } -} +import * as ViewModels from "../Contracts/ViewModels"; +import { userContext } from "../UserContext"; + +export class PortalTokenProvider implements ViewModels.TokenProvider { + constructor() {} + + public async getAuthHeader(): Promise { + const bearerToken = userContext.authorizationToken; + let fetchHeaders = new Headers(); + fetchHeaders.append("authorization", bearerToken); + return fetchHeaders; + } +} diff --git a/src/TokenProviders/TokenProviderFactory.ts b/src/TokenProviders/TokenProviderFactory.ts index 3beb6cb26..342661c3b 100644 --- a/src/TokenProviders/TokenProviderFactory.ts +++ b/src/TokenProviders/TokenProviderFactory.ts @@ -1,20 +1,20 @@ -import { configContext, Platform } from "../ConfigContext"; -import * as ViewModels from "../Contracts/ViewModels"; -import { PortalTokenProvider } from "./PortalTokenProvider"; - -export class TokenProviderFactory { - private constructor() {} - - public static create(): ViewModels.TokenProvider { - const platformType = configContext.platform; - switch (platformType) { - case Platform.Portal: - case Platform.Hosted: - return new PortalTokenProvider(); - case Platform.Emulator: - default: - // should never get into this state - throw new Error(`Unknown platform ${platformType}`); - } - } -} +import { configContext, Platform } from "../ConfigContext"; +import * as ViewModels from "../Contracts/ViewModels"; +import { PortalTokenProvider } from "./PortalTokenProvider"; + +export class TokenProviderFactory { + private constructor() {} + + public static create(): ViewModels.TokenProvider { + const platformType = configContext.platform; + switch (platformType) { + case Platform.Portal: + case Platform.Hosted: + return new PortalTokenProvider(); + case Platform.Emulator: + default: + // should never get into this state + throw new Error(`Unknown platform ${platformType}`); + } + } +} diff --git a/src/Utils/AuthorizationUtils.test.ts b/src/Utils/AuthorizationUtils.test.ts index 51faee8b6..00e5857ef 100644 --- a/src/Utils/AuthorizationUtils.test.ts +++ b/src/Utils/AuthorizationUtils.test.ts @@ -1,105 +1,105 @@ -import * as Constants from "../Common/Constants"; -import * as AuthorizationUtils from "./AuthorizationUtils"; -import { AuthType } from "../AuthType"; -import Explorer from "../Explorer/Explorer"; -import { updateUserContext } from "../UserContext"; -import { Platform, updateConfigContext } from "../ConfigContext"; -jest.mock("../Explorer/Explorer"); - -describe("AuthorizationUtils", () => { - describe("getAuthorizationHeader()", () => { - it("should return authorization header if authentication type is AAD", () => { - window.authType = AuthType.AAD; - updateUserContext({ - authorizationToken: "some-token" - }); - - expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.authorization); - expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token"); - }); - - it("should return guest access header if authentication type is EncryptedToken", () => { - window.authType = AuthType.EncryptedToken; - updateUserContext({ - accessToken: "some-token" - }); - - expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.guestAccessToken); - expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token"); - }); - }); - - describe("decryptJWTToken()", () => { - it("should throw an error if token is undefined", () => { - expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrowError(); - }); - - it("should throw an error if token is null", () => { - expect(() => AuthorizationUtils.decryptJWTToken(null)).toThrowError(); - }); - - it("should throw an error if token is empty", () => { - expect(() => AuthorizationUtils.decryptJWTToken("")).toThrowError(); - }); - - it("should throw an error if token is malformed", () => { - expect(() => - AuthorizationUtils.decryptJWTToken( - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9." - ) - ).toThrowError(); - }); - - it("should return decrypted token payload", () => { - expect( - AuthorizationUtils.decryptJWTToken( - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ" - ) - ).toBeDefined(); - }); - }); - - describe("displayTokenRenewalPromptForStatus()", () => { - let explorer = new Explorer() as jest.Mocked; - - beforeEach(() => { - jest.clearAllMocks(); - window.dataExplorer = explorer; - updateConfigContext({ - platform: Platform.Hosted - }); - }); - - afterEach(() => { - window.dataExplorer = undefined; - }); - - it("should not open token renewal prompt if status code is undefined", () => { - AuthorizationUtils.displayTokenRenewalPromptForStatus(undefined); - expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); - }); - - it("should not open token renewal prompt if status code is null", () => { - AuthorizationUtils.displayTokenRenewalPromptForStatus(null); - expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); - }); - - it("should not open token renewal prompt if status code is not 401", () => { - AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Forbidden); - expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); - }); - - it("should not open token renewal prompt if running on a different platform", () => { - updateConfigContext({ - platform: Platform.Portal - }); - AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized); - expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); - }); - - it("should open token renewal prompt if running on hosted platform and status code is 401", () => { - AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized); - expect(explorer.displayGuestAccessTokenRenewalPrompt).toHaveBeenCalled(); - }); - }); -}); +import * as Constants from "../Common/Constants"; +import * as AuthorizationUtils from "./AuthorizationUtils"; +import { AuthType } from "../AuthType"; +import Explorer from "../Explorer/Explorer"; +import { updateUserContext } from "../UserContext"; +import { Platform, updateConfigContext } from "../ConfigContext"; +jest.mock("../Explorer/Explorer"); + +describe("AuthorizationUtils", () => { + describe("getAuthorizationHeader()", () => { + it("should return authorization header if authentication type is AAD", () => { + window.authType = AuthType.AAD; + updateUserContext({ + authorizationToken: "some-token", + }); + + expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.authorization); + expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token"); + }); + + it("should return guest access header if authentication type is EncryptedToken", () => { + window.authType = AuthType.EncryptedToken; + updateUserContext({ + accessToken: "some-token", + }); + + expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.guestAccessToken); + expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token"); + }); + }); + + describe("decryptJWTToken()", () => { + it("should throw an error if token is undefined", () => { + expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrowError(); + }); + + it("should throw an error if token is null", () => { + expect(() => AuthorizationUtils.decryptJWTToken(null)).toThrowError(); + }); + + it("should throw an error if token is empty", () => { + expect(() => AuthorizationUtils.decryptJWTToken("")).toThrowError(); + }); + + it("should throw an error if token is malformed", () => { + expect(() => + AuthorizationUtils.decryptJWTToken( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9." + ) + ).toThrowError(); + }); + + it("should return decrypted token payload", () => { + expect( + AuthorizationUtils.decryptJWTToken( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ" + ) + ).toBeDefined(); + }); + }); + + describe("displayTokenRenewalPromptForStatus()", () => { + let explorer = new Explorer() as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + window.dataExplorer = explorer; + updateConfigContext({ + platform: Platform.Hosted, + }); + }); + + afterEach(() => { + window.dataExplorer = undefined; + }); + + it("should not open token renewal prompt if status code is undefined", () => { + AuthorizationUtils.displayTokenRenewalPromptForStatus(undefined); + expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); + }); + + it("should not open token renewal prompt if status code is null", () => { + AuthorizationUtils.displayTokenRenewalPromptForStatus(null); + expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); + }); + + it("should not open token renewal prompt if status code is not 401", () => { + AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Forbidden); + expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); + }); + + it("should not open token renewal prompt if running on a different platform", () => { + updateConfigContext({ + platform: Platform.Portal, + }); + AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized); + expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled(); + }); + + it("should open token renewal prompt if running on hosted platform and status code is 401", () => { + AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized); + expect(explorer.displayGuestAccessTokenRenewalPrompt).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index eda4d766d..5beaea836 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -1,56 +1,56 @@ -import { AuthType } from "../AuthType"; -import * as Constants from "../Common/Constants"; -import * as Logger from "../Common/Logger"; -import { configContext, Platform } from "../ConfigContext"; -import * as ViewModels from "../Contracts/ViewModels"; -import { userContext } from "../UserContext"; - -export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { - if (window.authType === AuthType.EncryptedToken) { - return { - header: Constants.HttpHeaders.guestAccessToken, - token: userContext.accessToken - }; - } else { - return { - header: Constants.HttpHeaders.authorization, - token: userContext.authorizationToken || "" - }; - } -} - -export function decryptJWTToken(token: string) { - if (!token) { - Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken"); - throw new Error("No JWT token found"); - } - const tokenParts = token.split("."); - if (tokenParts.length < 2) { - Logger.logError(`Invalid JWT token: ${token}`, "AuthorizationUtils/decryptJWTToken"); - throw new Error(`Invalid JWT token: ${token}`); - } - let tokenPayloadBase64: string = tokenParts[1]; - tokenPayloadBase64 = tokenPayloadBase64.replace(/-/g, "+").replace(/_/g, "/"); - const tokenPayload = decodeURIComponent( - atob(tokenPayloadBase64) - .split("") - .map(p => "%" + ("00" + p.charCodeAt(0).toString(16)).slice(-2)) - .join("") - ); - - return JSON.parse(tokenPayload); -} - -export function displayTokenRenewalPromptForStatus(httpStatusCode: number): void { - const explorer = window.dataExplorer; - - if ( - httpStatusCode == null || - httpStatusCode != Constants.HttpStatusCodes.Unauthorized || - configContext.platform !== Platform.Hosted - ) { - return; - } - - explorer.displayGuestAccessTokenRenewalPrompt(); -} +import { AuthType } from "../AuthType"; +import * as Constants from "../Common/Constants"; +import * as Logger from "../Common/Logger"; +import { configContext, Platform } from "../ConfigContext"; +import * as ViewModels from "../Contracts/ViewModels"; +import { userContext } from "../UserContext"; + +export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { + if (window.authType === AuthType.EncryptedToken) { + return { + header: Constants.HttpHeaders.guestAccessToken, + token: userContext.accessToken, + }; + } else { + return { + header: Constants.HttpHeaders.authorization, + token: userContext.authorizationToken || "", + }; + } +} + +export function decryptJWTToken(token: string) { + if (!token) { + Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken"); + throw new Error("No JWT token found"); + } + const tokenParts = token.split("."); + if (tokenParts.length < 2) { + Logger.logError(`Invalid JWT token: ${token}`, "AuthorizationUtils/decryptJWTToken"); + throw new Error(`Invalid JWT token: ${token}`); + } + let tokenPayloadBase64: string = tokenParts[1]; + tokenPayloadBase64 = tokenPayloadBase64.replace(/-/g, "+").replace(/_/g, "/"); + const tokenPayload = decodeURIComponent( + atob(tokenPayloadBase64) + .split("") + .map((p) => "%" + ("00" + p.charCodeAt(0).toString(16)).slice(-2)) + .join("") + ); + + return JSON.parse(tokenPayload); +} + +export function displayTokenRenewalPromptForStatus(httpStatusCode: number): void { + const explorer = window.dataExplorer; + + if ( + httpStatusCode == null || + httpStatusCode != Constants.HttpStatusCodes.Unauthorized || + configContext.platform !== Platform.Hosted + ) { + return; + } + + explorer.displayGuestAccessTokenRenewalPrompt(); +} diff --git a/src/Utils/Base64Utils.test.ts b/src/Utils/Base64Utils.test.ts index 938fe3f46..4aa90ae6c 100644 --- a/src/Utils/Base64Utils.test.ts +++ b/src/Utils/Base64Utils.test.ts @@ -1,11 +1,11 @@ -import * as Base64Utils from "./Base64Utils"; - -describe("Base64Utils", () => { - describe("utf8ToB64", () => { - it("should convert utf8 to base64", () => { - expect(Base64Utils.utf8ToB64("abcd")).toEqual(btoa("abcd")); - expect(Base64Utils.utf8ToB64("小飼弾")).toEqual("5bCP6aO85by+"); - expect(Base64Utils.utf8ToB64("à mon hôpital préféré")).toEqual("w6AgbW9uIGjDtHBpdGFsIHByw6lmw6lyw6k="); - }); - }); -}); +import * as Base64Utils from "./Base64Utils"; + +describe("Base64Utils", () => { + describe("utf8ToB64", () => { + it("should convert utf8 to base64", () => { + expect(Base64Utils.utf8ToB64("abcd")).toEqual(btoa("abcd")); + expect(Base64Utils.utf8ToB64("小飼弾")).toEqual("5bCP6aO85by+"); + expect(Base64Utils.utf8ToB64("à mon hôpital préféré")).toEqual("w6AgbW9uIGjDtHBpdGFsIHByw6lmw6lyw6k="); + }); + }); +}); diff --git a/src/Utils/Base64Utils.ts b/src/Utils/Base64Utils.ts index 83396f0b9..2146ae9ba 100644 --- a/src/Utils/Base64Utils.ts +++ b/src/Utils/Base64Utils.ts @@ -1,7 +1,7 @@ -export const utf8ToB64 = (utf8Str: string): string => { - return btoa( - encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, (_, args) => { - return String.fromCharCode(parseInt(args, 16)); - }) - ); -}; +export const utf8ToB64 = (utf8Str: string): string => { + return btoa( + encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, (_, args) => { + return String.fromCharCode(parseInt(args, 16)); + }) + ); +}; diff --git a/src/Utils/BlobUtils.ts b/src/Utils/BlobUtils.ts index a08391a4d..22e6ffeaf 100644 --- a/src/Utils/BlobUtils.ts +++ b/src/Utils/BlobUtils.ts @@ -1,17 +1,17 @@ -export const stringToBlob = (data: string, contentType: string, sliceSize = 512): Blob => { - const byteArrays = []; - - for (let offset = 0; offset < data.length; offset += sliceSize) { - const slice = data.slice(offset, offset + sliceSize); - - const byteNumbers = new Array(slice.length); - for (let i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); - } - - const byteArray = new Uint8Array(byteNumbers); - byteArrays.push(byteArray); - } - - return new Blob(byteArrays, { type: contentType }); -}; +export const stringToBlob = (data: string, contentType: string, sliceSize = 512): Blob => { + const byteArrays = []; + + for (let offset = 0; offset < data.length; offset += sliceSize) { + const slice = data.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + return new Blob(byteArrays, { type: contentType }); +}; diff --git a/src/Utils/GalleryUtils.test.ts b/src/Utils/GalleryUtils.test.ts index 6cf7332df..b911fa352 100644 --- a/src/Utils/GalleryUtils.test.ts +++ b/src/Utils/GalleryUtils.test.ts @@ -1,111 +1,111 @@ -import * as GalleryUtils from "./GalleryUtils"; -import { JunoClient, IGalleryItem } from "../Juno/JunoClient"; -import { HttpStatusCodes } from "../Common/Constants"; -import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; -import Explorer from "../Explorer/Explorer"; - -const galleryItem: IGalleryItem = { - id: "id", - name: "name", - description: "description", - gitSha: "gitSha", - tags: ["tag1"], - author: "author", - thumbnailUrl: "thumbnailUrl", - created: "created", - isSample: false, - downloads: 0, - favorites: 0, - views: 0, - newCellId: undefined, - policyViolations: undefined, - pendingScanJobIds: undefined -}; - -describe("GalleryUtils", () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it("downloadItem shows dialog in data explorer", () => { - const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - - GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); - - expect(container.showOkCancelModalDialog).toBeCalled(); - }); - - it("favoriteItem favorites item", async () => { - const container = {} as Explorer; - const junoClient = new JunoClient(); - junoClient.favoriteNotebook = jest - .fn() - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem })); - const onComplete = jest.fn().mockImplementation(); - - await GalleryUtils.favoriteItem(container, junoClient, galleryItem, onComplete); - - expect(junoClient.favoriteNotebook).toBeCalledWith(galleryItem.id); - expect(onComplete).toBeCalledWith(galleryItem); - }); - - it("unfavoriteItem unfavorites item", async () => { - const container = {} as Explorer; - const junoClient = new JunoClient(); - junoClient.unfavoriteNotebook = jest - .fn() - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem })); - const onComplete = jest.fn().mockImplementation(); - - await GalleryUtils.unfavoriteItem(container, junoClient, galleryItem, onComplete); - - expect(junoClient.unfavoriteNotebook).toBeCalledWith(galleryItem.id); - expect(onComplete).toBeCalledWith(galleryItem); - }); - - it("deleteItem shows dialog in data explorer", () => { - const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - - GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); - - expect(container.showOkCancelModalDialog).toBeCalled(); - }); - - it("getGalleryViewerProps gets gallery viewer props correctly", () => { - const selectedTab: GalleryTab = GalleryTab.OfficialSamples; - const sortBy: SortBy = SortBy.MostDownloaded; - const searchText = "my-complicated%20search%20query!!!"; - - const response = GalleryUtils.getGalleryViewerProps( - `?${GalleryUtils.GalleryViewerParams.SelectedTab}=${GalleryTab[selectedTab]}&${GalleryUtils.GalleryViewerParams.SortBy}=${SortBy[sortBy]}&${GalleryUtils.GalleryViewerParams.SearchText}=${searchText}` - ); - - expect(response).toEqual({ - selectedTab, - sortBy, - searchText: decodeURIComponent(searchText) - } as GalleryUtils.GalleryViewerProps); - }); - - it("getNotebookViewerProps gets notebook viewer props correctly", () => { - const notebookUrl = "https%3A%2F%2Fnotebook.url"; - const galleryItemId = "1234-abcd-efgh"; - const hideInputs = "true"; - - const response = GalleryUtils.getNotebookViewerProps( - `?${GalleryUtils.NotebookViewerParams.NotebookUrl}=${notebookUrl}&${GalleryUtils.NotebookViewerParams.GalleryItemId}=${galleryItemId}&${GalleryUtils.NotebookViewerParams.HideInputs}=${hideInputs}` - ); - - expect(response).toEqual({ - notebookUrl: decodeURIComponent(notebookUrl), - galleryItemId, - hideInputs: true - } as GalleryUtils.NotebookViewerProps); - }); - - it("getTabTitle returns correct title for official samples", () => { - expect(GalleryUtils.getTabTitle(GalleryTab.OfficialSamples)).toBe("Official samples"); - }); -}); +import * as GalleryUtils from "./GalleryUtils"; +import { JunoClient, IGalleryItem } from "../Juno/JunoClient"; +import { HttpStatusCodes } from "../Common/Constants"; +import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; +import Explorer from "../Explorer/Explorer"; + +const galleryItem: IGalleryItem = { + id: "id", + name: "name", + description: "description", + gitSha: "gitSha", + tags: ["tag1"], + author: "author", + thumbnailUrl: "thumbnailUrl", + created: "created", + isSample: false, + downloads: 0, + favorites: 0, + views: 0, + newCellId: undefined, + policyViolations: undefined, + pendingScanJobIds: undefined, +}; + +describe("GalleryUtils", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it("downloadItem shows dialog in data explorer", () => { + const container = {} as Explorer; + container.showOkCancelModalDialog = jest.fn().mockImplementation(); + + GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); + + expect(container.showOkCancelModalDialog).toBeCalled(); + }); + + it("favoriteItem favorites item", async () => { + const container = {} as Explorer; + const junoClient = new JunoClient(); + junoClient.favoriteNotebook = jest + .fn() + .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem })); + const onComplete = jest.fn().mockImplementation(); + + await GalleryUtils.favoriteItem(container, junoClient, galleryItem, onComplete); + + expect(junoClient.favoriteNotebook).toBeCalledWith(galleryItem.id); + expect(onComplete).toBeCalledWith(galleryItem); + }); + + it("unfavoriteItem unfavorites item", async () => { + const container = {} as Explorer; + const junoClient = new JunoClient(); + junoClient.unfavoriteNotebook = jest + .fn() + .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem })); + const onComplete = jest.fn().mockImplementation(); + + await GalleryUtils.unfavoriteItem(container, junoClient, galleryItem, onComplete); + + expect(junoClient.unfavoriteNotebook).toBeCalledWith(galleryItem.id); + expect(onComplete).toBeCalledWith(galleryItem); + }); + + it("deleteItem shows dialog in data explorer", () => { + const container = {} as Explorer; + container.showOkCancelModalDialog = jest.fn().mockImplementation(); + + GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); + + expect(container.showOkCancelModalDialog).toBeCalled(); + }); + + it("getGalleryViewerProps gets gallery viewer props correctly", () => { + const selectedTab: GalleryTab = GalleryTab.OfficialSamples; + const sortBy: SortBy = SortBy.MostDownloaded; + const searchText = "my-complicated%20search%20query!!!"; + + const response = GalleryUtils.getGalleryViewerProps( + `?${GalleryUtils.GalleryViewerParams.SelectedTab}=${GalleryTab[selectedTab]}&${GalleryUtils.GalleryViewerParams.SortBy}=${SortBy[sortBy]}&${GalleryUtils.GalleryViewerParams.SearchText}=${searchText}` + ); + + expect(response).toEqual({ + selectedTab, + sortBy, + searchText: decodeURIComponent(searchText), + } as GalleryUtils.GalleryViewerProps); + }); + + it("getNotebookViewerProps gets notebook viewer props correctly", () => { + const notebookUrl = "https%3A%2F%2Fnotebook.url"; + const galleryItemId = "1234-abcd-efgh"; + const hideInputs = "true"; + + const response = GalleryUtils.getNotebookViewerProps( + `?${GalleryUtils.NotebookViewerParams.NotebookUrl}=${notebookUrl}&${GalleryUtils.NotebookViewerParams.GalleryItemId}=${galleryItemId}&${GalleryUtils.NotebookViewerParams.HideInputs}=${hideInputs}` + ); + + expect(response).toEqual({ + notebookUrl: decodeURIComponent(notebookUrl), + galleryItemId, + hideInputs: true, + } as GalleryUtils.NotebookViewerProps); + }); + + it("getTabTitle returns correct title for official samples", () => { + expect(GalleryUtils.getTabTitle(GalleryTab.OfficialSamples)).toBe("Official samples"); + }); +}); diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 426e235b0..b9ab5f4f9 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -1,344 +1,344 @@ -import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; -import * as NotificationConsoleUtils from "./NotificationConsoleUtils"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { - GalleryTab, - SortBy, - GalleryViewerComponent -} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; -import Explorer from "../Explorer/Explorer"; -import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react"; -import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent"; -import { handleError } from "../Common/ErrorHandlingUtils"; -import { HttpStatusCodes } from "../Common/Constants"; - -const defaultSelectedAbuseCategory = "Other"; -const abuseCategories: IChoiceGroupOption[] = [ - { - key: "ChildEndangermentExploitation", - text: "Child endangerment or exploitation" - }, - { - key: "ContentInfringement", - text: "Content infringement" - }, - { - key: "OffensiveContent", - text: "Offensive content" - }, - { - key: "Terrorism", - text: "Terrorism" - }, - { - key: "ThreatsCyberbullyingHarassment", - text: "Threats, cyber bullying or harassment" - }, - { - key: "VirusSpywareMalware", - text: "Virus, spyware or malware" - }, - { - key: "Fraud", - text: "Fraud" - }, - { - key: "HateSpeech", - text: "Hate speech" - }, - { - key: "ImminentHarmToPersonsOrProperty", - text: "Imminent harm to persons or property" - }, - { - key: "Other", - text: "Other" - } -]; - -export enum NotebookViewerParams { - NotebookUrl = "notebookUrl", - GalleryItemId = "galleryItemId", - HideInputs = "hideInputs" -} - -export interface NotebookViewerProps { - notebookUrl: string; - galleryItemId: string; - hideInputs: boolean; -} - -export enum GalleryViewerParams { - SelectedTab = "tab", - SortBy = "sort", - SearchText = "q" -} - -export interface GalleryViewerProps { - selectedTab: GalleryTab; - sortBy: SortBy; - searchText: string; -} - -export interface DialogHost { - showOkCancelModalDialog( - title: string, - msg: string, - okLabel: string, - onOk: () => void, - cancelLabel: string, - onCancel: () => void, - choiceGroupProps?: IChoiceGroupProps, - textFieldProps?: TextFieldProps - ): void; -} - -export function reportAbuse( - junoClient: JunoClient, - data: IGalleryItem, - dialogHost: DialogHost, - onComplete: (success: boolean) => void -): void { - const notebookId = data.id; - let abuseCategory = defaultSelectedAbuseCategory; - let additionalDetails: string; - - dialogHost.showOkCancelModalDialog( - "Report Abuse", - undefined, - "Report Abuse", - async () => { - const clearSubmitReportNotification = NotificationConsoleUtils.logConsoleProgress( - `Submitting your report on ${data.name} violating code of conduct` - ); - - try { - const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails); - if (response.status !== HttpStatusCodes.Accepted) { - throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`); - } - - NotificationConsoleUtils.logConsoleInfo( - `Your report on ${data.name} has been submitted. Thank you for reporting the violation.` - ); - onComplete(response.data); - } catch (error) { - handleError( - error, - "GalleryUtils/reportAbuse", - `Failed to submit report on ${data.name} violating code of conduct` - ); - } - - clearSubmitReportNotification(); - }, - "Cancel", - undefined, - { - label: "How does this content violate the code of conduct?", - options: abuseCategories, - defaultSelectedKey: defaultSelectedAbuseCategory, - onChange: (_event?: React.FormEvent, option?: IChoiceGroupOption) => { - abuseCategory = option?.key; - } - }, - { - label: "You can also include additional relevant details on the offensive content", - multiline: true, - rows: 3, - autoAdjustHeight: false, - onChange: (_event: React.FormEvent, newValue?: string) => { - additionalDetails = newValue; - } - } - ); -} - -export function downloadItem( - container: Explorer, - junoClient: JunoClient, - data: IGalleryItem, - onComplete: (item: IGalleryItem) => void -): void { - const name = data.name; - container.showOkCancelModalDialog( - "Download to My Notebooks", - `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, - "Download", - async () => { - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Downloading ${name} to My Notebooks` - ); - - try { - const response = await junoClient.getNotebookContent(data.id); - if (!response.data) { - throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`); - } - - await container.importAndOpenContent(data.name, response.data); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully downloaded ${name} to My Notebooks` - ); - - const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id); - if (increaseDownloadResponse.data) { - onComplete(increaseDownloadResponse.data); - } - } catch (error) { - handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`); - } - - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }, - "Cancel", - undefined - ); -} - -export async function favoriteItem( - container: Explorer, - junoClient: JunoClient, - data: IGalleryItem, - onComplete: (item: IGalleryItem) => void -): Promise { - if (container) { - try { - const response = await junoClient.favoriteNotebook(data.id); - if (!response.data) { - throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`); - } - - onComplete(response.data); - } catch (error) { - handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`); - } - } -} - -export async function unfavoriteItem( - container: Explorer, - junoClient: JunoClient, - data: IGalleryItem, - onComplete: (item: IGalleryItem) => void -): Promise { - if (container) { - try { - const response = await junoClient.unfavoriteNotebook(data.id); - if (!response.data) { - throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`); - } - - onComplete(response.data); - } catch (error) { - handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`); - } - } -} - -export function deleteItem( - container: Explorer, - junoClient: JunoClient, - data: IGalleryItem, - onComplete: (item: IGalleryItem) => void -): void { - if (container) { - container.showOkCancelModalDialog( - "Remove published notebook", - `Would you like to remove ${data.name} from the gallery?`, - "Remove", - async () => { - const name = data.name; - const notificationId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Removing ${name} from gallery` - ); - - try { - const response = await junoClient.deleteNotebook(data.id); - if (!response.data) { - throw new Error(`Received HTTP ${response.status} while removing ${name}`); - } - - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully removed ${name} from gallery`); - onComplete(response.data); - } catch (error) { - handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`); - } - - NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); - }, - "Cancel", - undefined - ); - } -} - -export function getGalleryViewerProps(search: string): GalleryViewerProps { - const params = new URLSearchParams(search); - let selectedTab: GalleryTab; - if (params.has(GalleryViewerParams.SelectedTab)) { - selectedTab = GalleryTab[params.get(GalleryViewerParams.SelectedTab) as keyof typeof GalleryTab]; - } - - let sortBy: SortBy; - if (params.has(GalleryViewerParams.SortBy)) { - sortBy = SortBy[params.get(GalleryViewerParams.SortBy) as keyof typeof SortBy]; - } - - return { - selectedTab, - sortBy, - searchText: params.get(GalleryViewerParams.SearchText) - }; -} - -export function getNotebookViewerProps(search: string): NotebookViewerProps { - const params = new URLSearchParams(search); - return { - notebookUrl: params.get(NotebookViewerParams.NotebookUrl), - galleryItemId: params.get(NotebookViewerParams.GalleryItemId), - hideInputs: JSON.parse(params.get(NotebookViewerParams.HideInputs)) - }; -} - -export function getTabTitle(tab: GalleryTab): string { - switch (tab) { - case GalleryTab.OfficialSamples: - return GalleryViewerComponent.OfficialSamplesTitle; - case GalleryTab.PublicGallery: - return GalleryViewerComponent.PublicGalleryTitle; - case GalleryTab.Favorites: - return GalleryViewerComponent.FavoritesTitle; - case GalleryTab.Published: - return GalleryViewerComponent.PublishedTitle; - default: - throw new Error(`Unknown tab ${tab}`); - } -} - -export function filterPublishedNotebooks( - items: IGalleryItem[] -): { - published: IGalleryItem[]; - underReview: IGalleryItem[]; - removed: IGalleryItem[]; -} { - const underReview: IGalleryItem[] = []; - const removed: IGalleryItem[] = []; - const published: IGalleryItem[] = []; - - items?.forEach(item => { - if (item.policyViolations?.length > 0) { - removed.push(item); - } else if (item.pendingScanJobIds?.length > 0) { - underReview.push(item); - } else { - published.push(item); - } - }); - - return { published, underReview, removed }; -} +import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; +import * as NotificationConsoleUtils from "./NotificationConsoleUtils"; +import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { + GalleryTab, + SortBy, + GalleryViewerComponent, +} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; +import Explorer from "../Explorer/Explorer"; +import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react"; +import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent"; +import { handleError } from "../Common/ErrorHandlingUtils"; +import { HttpStatusCodes } from "../Common/Constants"; + +const defaultSelectedAbuseCategory = "Other"; +const abuseCategories: IChoiceGroupOption[] = [ + { + key: "ChildEndangermentExploitation", + text: "Child endangerment or exploitation", + }, + { + key: "ContentInfringement", + text: "Content infringement", + }, + { + key: "OffensiveContent", + text: "Offensive content", + }, + { + key: "Terrorism", + text: "Terrorism", + }, + { + key: "ThreatsCyberbullyingHarassment", + text: "Threats, cyber bullying or harassment", + }, + { + key: "VirusSpywareMalware", + text: "Virus, spyware or malware", + }, + { + key: "Fraud", + text: "Fraud", + }, + { + key: "HateSpeech", + text: "Hate speech", + }, + { + key: "ImminentHarmToPersonsOrProperty", + text: "Imminent harm to persons or property", + }, + { + key: "Other", + text: "Other", + }, +]; + +export enum NotebookViewerParams { + NotebookUrl = "notebookUrl", + GalleryItemId = "galleryItemId", + HideInputs = "hideInputs", +} + +export interface NotebookViewerProps { + notebookUrl: string; + galleryItemId: string; + hideInputs: boolean; +} + +export enum GalleryViewerParams { + SelectedTab = "tab", + SortBy = "sort", + SearchText = "q", +} + +export interface GalleryViewerProps { + selectedTab: GalleryTab; + sortBy: SortBy; + searchText: string; +} + +export interface DialogHost { + showOkCancelModalDialog( + title: string, + msg: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps + ): void; +} + +export function reportAbuse( + junoClient: JunoClient, + data: IGalleryItem, + dialogHost: DialogHost, + onComplete: (success: boolean) => void +): void { + const notebookId = data.id; + let abuseCategory = defaultSelectedAbuseCategory; + let additionalDetails: string; + + dialogHost.showOkCancelModalDialog( + "Report Abuse", + undefined, + "Report Abuse", + async () => { + const clearSubmitReportNotification = NotificationConsoleUtils.logConsoleProgress( + `Submitting your report on ${data.name} violating code of conduct` + ); + + try { + const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails); + if (response.status !== HttpStatusCodes.Accepted) { + throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`); + } + + NotificationConsoleUtils.logConsoleInfo( + `Your report on ${data.name} has been submitted. Thank you for reporting the violation.` + ); + onComplete(response.data); + } catch (error) { + handleError( + error, + "GalleryUtils/reportAbuse", + `Failed to submit report on ${data.name} violating code of conduct` + ); + } + + clearSubmitReportNotification(); + }, + "Cancel", + undefined, + { + label: "How does this content violate the code of conduct?", + options: abuseCategories, + defaultSelectedKey: defaultSelectedAbuseCategory, + onChange: (_event?: React.FormEvent, option?: IChoiceGroupOption) => { + abuseCategory = option?.key; + }, + }, + { + label: "You can also include additional relevant details on the offensive content", + multiline: true, + rows: 3, + autoAdjustHeight: false, + onChange: (_event: React.FormEvent, newValue?: string) => { + additionalDetails = newValue; + }, + } + ); +} + +export function downloadItem( + container: Explorer, + junoClient: JunoClient, + data: IGalleryItem, + onComplete: (item: IGalleryItem) => void +): void { + const name = data.name; + container.showOkCancelModalDialog( + "Download to My Notebooks", + `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, + "Download", + async () => { + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Downloading ${name} to My Notebooks` + ); + + try { + const response = await junoClient.getNotebookContent(data.id); + if (!response.data) { + throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`); + } + + await container.importAndOpenContent(data.name, response.data); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + `Successfully downloaded ${name} to My Notebooks` + ); + + const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id); + if (increaseDownloadResponse.data) { + onComplete(increaseDownloadResponse.data); + } + } catch (error) { + handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`); + } + + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }, + "Cancel", + undefined + ); +} + +export async function favoriteItem( + container: Explorer, + junoClient: JunoClient, + data: IGalleryItem, + onComplete: (item: IGalleryItem) => void +): Promise { + if (container) { + try { + const response = await junoClient.favoriteNotebook(data.id); + if (!response.data) { + throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`); + } + + onComplete(response.data); + } catch (error) { + handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`); + } + } +} + +export async function unfavoriteItem( + container: Explorer, + junoClient: JunoClient, + data: IGalleryItem, + onComplete: (item: IGalleryItem) => void +): Promise { + if (container) { + try { + const response = await junoClient.unfavoriteNotebook(data.id); + if (!response.data) { + throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`); + } + + onComplete(response.data); + } catch (error) { + handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`); + } + } +} + +export function deleteItem( + container: Explorer, + junoClient: JunoClient, + data: IGalleryItem, + onComplete: (item: IGalleryItem) => void +): void { + if (container) { + container.showOkCancelModalDialog( + "Remove published notebook", + `Would you like to remove ${data.name} from the gallery?`, + "Remove", + async () => { + const name = data.name; + const notificationId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Removing ${name} from gallery` + ); + + try { + const response = await junoClient.deleteNotebook(data.id); + if (!response.data) { + throw new Error(`Received HTTP ${response.status} while removing ${name}`); + } + + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully removed ${name} from gallery`); + onComplete(response.data); + } catch (error) { + handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`); + } + + NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); + }, + "Cancel", + undefined + ); + } +} + +export function getGalleryViewerProps(search: string): GalleryViewerProps { + const params = new URLSearchParams(search); + let selectedTab: GalleryTab; + if (params.has(GalleryViewerParams.SelectedTab)) { + selectedTab = GalleryTab[params.get(GalleryViewerParams.SelectedTab) as keyof typeof GalleryTab]; + } + + let sortBy: SortBy; + if (params.has(GalleryViewerParams.SortBy)) { + sortBy = SortBy[params.get(GalleryViewerParams.SortBy) as keyof typeof SortBy]; + } + + return { + selectedTab, + sortBy, + searchText: params.get(GalleryViewerParams.SearchText), + }; +} + +export function getNotebookViewerProps(search: string): NotebookViewerProps { + const params = new URLSearchParams(search); + return { + notebookUrl: params.get(NotebookViewerParams.NotebookUrl), + galleryItemId: params.get(NotebookViewerParams.GalleryItemId), + hideInputs: JSON.parse(params.get(NotebookViewerParams.HideInputs)), + }; +} + +export function getTabTitle(tab: GalleryTab): string { + switch (tab) { + case GalleryTab.OfficialSamples: + return GalleryViewerComponent.OfficialSamplesTitle; + case GalleryTab.PublicGallery: + return GalleryViewerComponent.PublicGalleryTitle; + case GalleryTab.Favorites: + return GalleryViewerComponent.FavoritesTitle; + case GalleryTab.Published: + return GalleryViewerComponent.PublishedTitle; + default: + throw new Error(`Unknown tab ${tab}`); + } +} + +export function filterPublishedNotebooks( + items: IGalleryItem[] +): { + published: IGalleryItem[]; + underReview: IGalleryItem[]; + removed: IGalleryItem[]; +} { + const underReview: IGalleryItem[] = []; + const removed: IGalleryItem[] = []; + const published: IGalleryItem[] = []; + + items?.forEach((item) => { + if (item.policyViolations?.length > 0) { + removed.push(item); + } else if (item.pendingScanJobIds?.length > 0) { + underReview.push(item); + } else { + published.push(item); + } + }); + + return { published, underReview, removed }; +} diff --git a/src/Utils/GitHubUtils.test.ts b/src/Utils/GitHubUtils.test.ts index 7f0c1b0bc..33578bd6b 100644 --- a/src/Utils/GitHubUtils.test.ts +++ b/src/Utils/GitHubUtils.test.ts @@ -1,32 +1,32 @@ -import * as GitHubUtils from "./GitHubUtils"; - -const owner = "owner-1"; -const repo = "repo-1"; -const branch = "branch/name.1-2"; -const path = "folder name/file name1:2.ipynb"; - -describe("GitHubUtils", () => { - it("fromRepoUri parses github repo url correctly", () => { - const repoInfo = GitHubUtils.fromRepoUri(`https://github.com/${owner}/${repo}/tree/${branch}`); - expect(repoInfo).toEqual({ - owner, - repo, - branch - }); - }); - - it("toContentUri generates github uris correctly", () => { - const uri = GitHubUtils.toContentUri(owner, repo, branch, path); - expect(uri).toBe(`github://${owner}/${repo}/${path}?ref=${branch}`); - }); - - it("fromContentUri parses the github uris correctly", () => { - const contentInfo = GitHubUtils.fromContentUri(`github://${owner}/${repo}/${path}?ref=${branch}`); - expect(contentInfo).toEqual({ - owner, - repo, - branch, - path - }); - }); -}); +import * as GitHubUtils from "./GitHubUtils"; + +const owner = "owner-1"; +const repo = "repo-1"; +const branch = "branch/name.1-2"; +const path = "folder name/file name1:2.ipynb"; + +describe("GitHubUtils", () => { + it("fromRepoUri parses github repo url correctly", () => { + const repoInfo = GitHubUtils.fromRepoUri(`https://github.com/${owner}/${repo}/tree/${branch}`); + expect(repoInfo).toEqual({ + owner, + repo, + branch, + }); + }); + + it("toContentUri generates github uris correctly", () => { + const uri = GitHubUtils.toContentUri(owner, repo, branch, path); + expect(uri).toBe(`github://${owner}/${repo}/${path}?ref=${branch}`); + }); + + it("fromContentUri parses the github uris correctly", () => { + const contentInfo = GitHubUtils.fromContentUri(`github://${owner}/${repo}/${path}?ref=${branch}`); + expect(contentInfo).toEqual({ + owner, + repo, + branch, + path, + }); + }); +}); diff --git a/src/Utils/GitHubUtils.ts b/src/Utils/GitHubUtils.ts index 2b9cc2d04..13e5f828b 100644 --- a/src/Utils/GitHubUtils.ts +++ b/src/Utils/GitHubUtils.ts @@ -1,62 +1,62 @@ -// https://github.com///tree/ -// The url when users visit a repo/branch on github.com -export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/; - -// github:////?ref= -// Custom scheme for github content -export const ContentUriPattern = /github:\/\/([^/]*)\/([^/]*)\/([^?]*)\?ref=(.*)/; - -// https://github.com///blob// -// We need to support this until we move to newer scheme for quickstarts -export const LegacyContentUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/blob\/([^/]*)\/([^?]*)/; - -export function toRepoFullName(owner: string, repo: string): string { - return `${owner}/${repo}`; -} - -export function fromRepoUri(repoUri: string): undefined | { owner: string; repo: string; branch: string } { - const matches = repoUri.match(RepoUriPattern); - if (matches && matches.length > 3) { - return { - owner: matches[1], - repo: matches[2], - branch: matches[3] - }; - } - - return undefined; -} - -export function fromContentUri( - contentUri: string -): undefined | { owner: string; repo: string; branch: string; path: string } { - let matches = contentUri.match(ContentUriPattern); - if (matches && matches.length > 4) { - return { - owner: matches[1], - repo: matches[2], - branch: matches[4], - path: matches[3] - }; - } - - matches = contentUri.match(LegacyContentUriPattern); - if (matches && matches.length > 4) { - return { - owner: matches[1], - repo: matches[2], - branch: matches[3], - path: matches[4] - }; - } - - return undefined; -} - -export function toContentUri(owner: string, repo: string, branch: string, path: string): string { - return `github://${owner}/${repo}/${path}?ref=${branch}`; -} - -export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string { - return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; -} +// https://github.com///tree/ +// The url when users visit a repo/branch on github.com +export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/; + +// github:////?ref= +// Custom scheme for github content +export const ContentUriPattern = /github:\/\/([^/]*)\/([^/]*)\/([^?]*)\?ref=(.*)/; + +// https://github.com///blob// +// We need to support this until we move to newer scheme for quickstarts +export const LegacyContentUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/blob\/([^/]*)\/([^?]*)/; + +export function toRepoFullName(owner: string, repo: string): string { + return `${owner}/${repo}`; +} + +export function fromRepoUri(repoUri: string): undefined | { owner: string; repo: string; branch: string } { + const matches = repoUri.match(RepoUriPattern); + if (matches && matches.length > 3) { + return { + owner: matches[1], + repo: matches[2], + branch: matches[3], + }; + } + + return undefined; +} + +export function fromContentUri( + contentUri: string +): undefined | { owner: string; repo: string; branch: string; path: string } { + let matches = contentUri.match(ContentUriPattern); + if (matches && matches.length > 4) { + return { + owner: matches[1], + repo: matches[2], + branch: matches[4], + path: matches[3], + }; + } + + matches = contentUri.match(LegacyContentUriPattern); + if (matches && matches.length > 4) { + return { + owner: matches[1], + repo: matches[2], + branch: matches[3], + path: matches[4], + }; + } + + return undefined; +} + +export function toContentUri(owner: string, repo: string, branch: string, path: string): string { + return `github://${owner}/${repo}/${path}?ref=${branch}`; +} + +export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string { + return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; +} diff --git a/src/Utils/JunoUtils.test.ts b/src/Utils/JunoUtils.test.ts index 27254f448..1bdb60e89 100644 --- a/src/Utils/JunoUtils.test.ts +++ b/src/Utils/JunoUtils.test.ts @@ -1,45 +1,45 @@ -import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent"; -import { IPinnedRepo } from "../Juno/JunoClient"; -import { JunoUtils } from "./JunoUtils"; -import { IGitHubRepo } from "../GitHub/GitHubClient"; - -const gitHubRepo: IGitHubRepo = { - name: "repo-name", - owner: "owner", - private: false -}; - -const repoListItem: RepoListItem = { - key: "key", - repo: { - name: "repo-name", - owner: "owner", - private: false - }, - branches: [ - { - name: "branch-name" - } - ] -}; - -const pinnedRepo: IPinnedRepo = { - name: "repo-name", - owner: "owner", - private: false, - branches: [ - { - name: "branch-name" - } - ] -}; - -describe("JunoUtils", () => { - it("toPinnedRepo converts RepoListItem to IPinnedRepo", () => { - expect(JunoUtils.toPinnedRepo(repoListItem)).toEqual(pinnedRepo); - }); - - it("toGitHubRepo converts IPinnedRepo to IGitHubRepo", () => { - expect(JunoUtils.toGitHubRepo(pinnedRepo)).toEqual(gitHubRepo); - }); -}); +import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent"; +import { IPinnedRepo } from "../Juno/JunoClient"; +import { JunoUtils } from "./JunoUtils"; +import { IGitHubRepo } from "../GitHub/GitHubClient"; + +const gitHubRepo: IGitHubRepo = { + name: "repo-name", + owner: "owner", + private: false, +}; + +const repoListItem: RepoListItem = { + key: "key", + repo: { + name: "repo-name", + owner: "owner", + private: false, + }, + branches: [ + { + name: "branch-name", + }, + ], +}; + +const pinnedRepo: IPinnedRepo = { + name: "repo-name", + owner: "owner", + private: false, + branches: [ + { + name: "branch-name", + }, + ], +}; + +describe("JunoUtils", () => { + it("toPinnedRepo converts RepoListItem to IPinnedRepo", () => { + expect(JunoUtils.toPinnedRepo(repoListItem)).toEqual(pinnedRepo); + }); + + it("toGitHubRepo converts IPinnedRepo to IGitHubRepo", () => { + expect(JunoUtils.toGitHubRepo(pinnedRepo)).toEqual(gitHubRepo); + }); +}); diff --git a/src/Utils/JunoUtils.ts b/src/Utils/JunoUtils.ts index d3c6d3823..ecfcaf65d 100644 --- a/src/Utils/JunoUtils.ts +++ b/src/Utils/JunoUtils.ts @@ -8,7 +8,7 @@ export class JunoUtils { owner: item.repo.owner, name: item.repo.name, private: item.repo.private, - branches: item.branches.map(element => ({ name: element.name })) + branches: item.branches.map((element) => ({ name: element.name })), }; } @@ -16,7 +16,7 @@ export class JunoUtils { return { owner: pinnedRepo.owner, name: pinnedRepo.name, - private: pinnedRepo.private + private: pinnedRepo.private, }; } } diff --git a/src/Utils/NotebookConfigurationUtils.ts b/src/Utils/NotebookConfigurationUtils.ts index 20a10fc76..d108d87c9 100644 --- a/src/Utils/NotebookConfigurationUtils.ts +++ b/src/Utils/NotebookConfigurationUtils.ts @@ -1,92 +1,92 @@ -import * as DataModels from "../Contracts/DataModels"; -import * as Logger from "../Common/Logger"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; - -interface KernelConnectionMetadata { - name: string; - configurationEndpoints: DataModels.NotebookConfigurationEndpoints; - notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo; -} - -export class NotebookConfigurationUtils { - private constructor() {} - - public static async configureServiceEndpoints( - notebookPath: string, - notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo, - kernelName: string, - clusterConnectionInfo: DataModels.SparkClusterConnectionInfo - ): Promise { - if (!notebookPath || !notebookConnectionInfo || !kernelName) { - Logger.logError( - "Invalid or missing notebook connection info/path", - "NotebookConfigurationUtils/configureServiceEndpoints" - ); - return Promise.reject("Invalid or missing notebook connection info"); - } - - if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) { - Logger.logError( - "Invalid or missing cluster connection info/endpoints", - "NotebookConfigurationUtils/configureServiceEndpoints" - ); - return Promise.reject("Invalid or missing cluster connection info"); - } - - const dataExplorer = window.dataExplorer; - const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map( - clusterEndpoint => ({ - type: clusterEndpoint.kind.toLowerCase(), - endpoint: clusterEndpoint && clusterEndpoint.endpoint, - username: clusterConnectionInfo.userName, - password: clusterConnectionInfo.password, - token: dataExplorer && dataExplorer.arcadiaToken() - }) - ); - const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = { - path: notebookPath, - endpoints: notebookEndpointInfo - }; - const kernelMetadata: KernelConnectionMetadata = { - configurationEndpoints, - notebookConnectionInfo, - name: kernelName - }; - - return await NotebookConfigurationUtils._configureServiceEndpoints(kernelMetadata); - } - - private static async _configureServiceEndpoints(kernelMetadata: KernelConnectionMetadata): Promise { - if (!kernelMetadata) { - // should never get into this state - Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints"); - return; - } - - const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo; - const configurationEndpoints = kernelMetadata.configurationEndpoints; - if (notebookConnectionInfo && configurationEndpoints) { - try { - const headers: any = { "Content-Type": "application/json" }; - if (notebookConnectionInfo.authToken) { - headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`; - } - const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, { - method: "POST", - headers, - body: JSON.stringify(configurationEndpoints) - }); - if (!response.ok) { - const responseMessage = await response.json(); - Logger.logError( - getErrorMessage(responseMessage), - "NotebookConfigurationUtils/configureServiceEndpoints", - response.status - ); - } - } catch (error) { - Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints"); - } - } - } -} +import * as DataModels from "../Contracts/DataModels"; +import * as Logger from "../Common/Logger"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; + +interface KernelConnectionMetadata { + name: string; + configurationEndpoints: DataModels.NotebookConfigurationEndpoints; + notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo; +} + +export class NotebookConfigurationUtils { + private constructor() {} + + public static async configureServiceEndpoints( + notebookPath: string, + notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo, + kernelName: string, + clusterConnectionInfo: DataModels.SparkClusterConnectionInfo + ): Promise { + if (!notebookPath || !notebookConnectionInfo || !kernelName) { + Logger.logError( + "Invalid or missing notebook connection info/path", + "NotebookConfigurationUtils/configureServiceEndpoints" + ); + return Promise.reject("Invalid or missing notebook connection info"); + } + + if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) { + Logger.logError( + "Invalid or missing cluster connection info/endpoints", + "NotebookConfigurationUtils/configureServiceEndpoints" + ); + return Promise.reject("Invalid or missing cluster connection info"); + } + + const dataExplorer = window.dataExplorer; + const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map( + (clusterEndpoint) => ({ + type: clusterEndpoint.kind.toLowerCase(), + endpoint: clusterEndpoint && clusterEndpoint.endpoint, + username: clusterConnectionInfo.userName, + password: clusterConnectionInfo.password, + token: dataExplorer && dataExplorer.arcadiaToken(), + }) + ); + const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = { + path: notebookPath, + endpoints: notebookEndpointInfo, + }; + const kernelMetadata: KernelConnectionMetadata = { + configurationEndpoints, + notebookConnectionInfo, + name: kernelName, + }; + + return await NotebookConfigurationUtils._configureServiceEndpoints(kernelMetadata); + } + + private static async _configureServiceEndpoints(kernelMetadata: KernelConnectionMetadata): Promise { + if (!kernelMetadata) { + // should never get into this state + Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints"); + return; + } + + const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo; + const configurationEndpoints = kernelMetadata.configurationEndpoints; + if (notebookConnectionInfo && configurationEndpoints) { + try { + const headers: any = { "Content-Type": "application/json" }; + if (notebookConnectionInfo.authToken) { + headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`; + } + const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, { + method: "POST", + headers, + body: JSON.stringify(configurationEndpoints), + }); + if (!response.ok) { + const responseMessage = await response.json(); + Logger.logError( + getErrorMessage(responseMessage), + "NotebookConfigurationUtils/configureServiceEndpoints", + response.status + ); + } + } catch (error) { + Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints"); + } + } + } +} diff --git a/src/Utils/NotificationConsoleUtils.ts b/src/Utils/NotificationConsoleUtils.ts index 6a01ed802..7446b957b 100644 --- a/src/Utils/NotificationConsoleUtils.ts +++ b/src/Utils/NotificationConsoleUtils.ts @@ -1,82 +1,82 @@ -import * as _ from "underscore"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; - -const _global = typeof self === "undefined" ? window : self; - -/** - * @deprecated - * Use logConsoleInfo, logConsoleError, logConsoleProgress instead - * */ -export function logConsoleMessage(type: ConsoleDataType, message: string, id?: string): string { - const dataExplorer = _global.dataExplorer; - if (dataExplorer) { - const date = new Date(); - const formattedDate: string = new Intl.DateTimeFormat("en-EN", { - hour12: true, - hour: "numeric", - minute: "numeric" - }).format(date); - if (!id) { - id = _.uniqueId(); - } - dataExplorer.logConsoleData({ type: type, date: formattedDate, message: message, id: id }); - } - return id || ""; -} - -export function clearInProgressMessageWithId(id: string): void { - const dataExplorer = _global.dataExplorer; - dataExplorer && dataExplorer.deleteInProgressConsoleDataWithId(id); -} - -export function logConsoleProgress(message: string): () => void { - const type = ConsoleDataType.InProgress; - const dataExplorer = _global.dataExplorer; - if (dataExplorer) { - const id = _.uniqueId(); - const date = new Date(); - const formattedDate: string = new Intl.DateTimeFormat("en-EN", { - hour12: true, - hour: "numeric", - minute: "numeric" - }).format(date); - dataExplorer.logConsoleData({ type, date: formattedDate, message, id }); - return () => { - dataExplorer.deleteInProgressConsoleDataWithId(id); - }; - } else { - return () => { - return; - }; - } -} - -export function logConsoleError(message: string): void { - const type = ConsoleDataType.Error; - const dataExplorer = _global.dataExplorer; - if (dataExplorer) { - const id = _.uniqueId(); - const date = new Date(); - const formattedDate: string = new Intl.DateTimeFormat("en-EN", { - hour12: true, - hour: "numeric", - minute: "numeric" - }).format(date); - dataExplorer.logConsoleData({ type, date: formattedDate, message, id }); - } -} - -export function logConsoleInfo(message: string): void { - const type = ConsoleDataType.Info; - const dataExplorer = _global.dataExplorer; - if (dataExplorer) { - const id = _.uniqueId(); - const date = new Date(); - const formattedDate: string = new Intl.DateTimeFormat("en-EN", { - hour12: true, - hour: "numeric", - minute: "numeric" - }).format(date); - dataExplorer.logConsoleData({ type, date: formattedDate, message, id }); - } -} +import * as _ from "underscore"; +import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; + +const _global = typeof self === "undefined" ? window : self; + +/** + * @deprecated + * Use logConsoleInfo, logConsoleError, logConsoleProgress instead + * */ +export function logConsoleMessage(type: ConsoleDataType, message: string, id?: string): string { + const dataExplorer = _global.dataExplorer; + if (dataExplorer) { + const date = new Date(); + const formattedDate: string = new Intl.DateTimeFormat("en-EN", { + hour12: true, + hour: "numeric", + minute: "numeric", + }).format(date); + if (!id) { + id = _.uniqueId(); + } + dataExplorer.logConsoleData({ type: type, date: formattedDate, message: message, id: id }); + } + return id || ""; +} + +export function clearInProgressMessageWithId(id: string): void { + const dataExplorer = _global.dataExplorer; + dataExplorer && dataExplorer.deleteInProgressConsoleDataWithId(id); +} + +export function logConsoleProgress(message: string): () => void { + const type = ConsoleDataType.InProgress; + const dataExplorer = _global.dataExplorer; + if (dataExplorer) { + const id = _.uniqueId(); + const date = new Date(); + const formattedDate: string = new Intl.DateTimeFormat("en-EN", { + hour12: true, + hour: "numeric", + minute: "numeric", + }).format(date); + dataExplorer.logConsoleData({ type, date: formattedDate, message, id }); + return () => { + dataExplorer.deleteInProgressConsoleDataWithId(id); + }; + } else { + return () => { + return; + }; + } +} + +export function logConsoleError(message: string): void { + const type = ConsoleDataType.Error; + const dataExplorer = _global.dataExplorer; + if (dataExplorer) { + const id = _.uniqueId(); + const date = new Date(); + const formattedDate: string = new Intl.DateTimeFormat("en-EN", { + hour12: true, + hour: "numeric", + minute: "numeric", + }).format(date); + dataExplorer.logConsoleData({ type, date: formattedDate, message, id }); + } +} + +export function logConsoleInfo(message: string): void { + const type = ConsoleDataType.Info; + const dataExplorer = _global.dataExplorer; + if (dataExplorer) { + const id = _.uniqueId(); + const date = new Date(); + const formattedDate: string = new Intl.DateTimeFormat("en-EN", { + hour12: true, + hour: "numeric", + minute: "numeric", + }).format(date); + dataExplorer.logConsoleData({ type, date: formattedDate, message, id }); + } +} diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index 09c0ef5e5..582c40a37 100644 --- a/src/Utils/PricingUtils.test.ts +++ b/src/Utils/PricingUtils.test.ts @@ -30,7 +30,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: null, multimasterEnabled: false, - isAutoscale: false + isAutoscale: false, }); expect(value).toBe(0); }); @@ -40,7 +40,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: null, multimasterEnabled: false, - isAutoscale: true + isAutoscale: true, }); expect(value).toBe(0); }); @@ -51,7 +51,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: -1, multimasterEnabled: false, - isAutoscale: false + isAutoscale: false, }); expect(value).toBe(0); }); @@ -61,7 +61,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: -1, multimasterEnabled: false, - isAutoscale: true + isAutoscale: true, }); expect(value).toBe(0); }); @@ -72,7 +72,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 1, multimasterEnabled: false, - isAutoscale: false + isAutoscale: false, }); expect(value).toBe(0.00008); }); @@ -82,7 +82,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 1, multimasterEnabled: false, - isAutoscale: true + isAutoscale: true, }); expect(value).toBe(0.00012); }); @@ -93,7 +93,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 1, multimasterEnabled: false, - isAutoscale: false + isAutoscale: false, }); expect(value).toBe(0.00051); }); @@ -103,7 +103,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 1, multimasterEnabled: false, - isAutoscale: true + isAutoscale: true, }); expect(value).toBe(0.00076); }); @@ -114,7 +114,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 2, multimasterEnabled: false, - isAutoscale: false + isAutoscale: false, }); expect(value).toBe(0.00016); }); @@ -124,7 +124,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 2, multimasterEnabled: false, - isAutoscale: true + isAutoscale: true, }); expect(value).toBe(0.00024); }); @@ -135,7 +135,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 1, multimasterEnabled: true, - isAutoscale: false + isAutoscale: false, }); expect(value).toBe(0.00008); }); @@ -145,7 +145,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 1, multimasterEnabled: true, - isAutoscale: true + isAutoscale: true, }); expect(value).toBe(0.00012); }); @@ -156,7 +156,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 2, multimasterEnabled: true, - isAutoscale: false + isAutoscale: false, }); expect(value).toBe(0.00048); }); @@ -166,7 +166,7 @@ describe("PricingUtils Tests", () => { requestUnits: 1, numberOfRegions: 2, multimasterEnabled: true, - isAutoscale: true + isAutoscale: true, }); expect(value).toBe(0.00096); }); diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index 686eedebc..07b52322d 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -64,7 +64,7 @@ export function computeRUUsagePriceHourly({ requestUnits, numberOfRegions, multimasterEnabled, - isAutoscale + isAutoscale, }: ComputeRUUsagePriceHourlyArgs): number { const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled); const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); @@ -183,7 +183,7 @@ export function getEstimatedAutoscaleSpendHtml( requestUnits: throughput, numberOfRegions: regions, multimasterEnabled: multimaster, - isAutoscale: true + isAutoscale: true, }); const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; const currency: string = getPriceCurrency(serverId); @@ -196,8 +196,9 @@ export function getEstimatedAutoscaleSpendHtml( `Estimated monthly cost (${currency}): ` + `${currencySign}${calculateEstimateNumber(monthlyPrice / 10)} - ` + `${currencySign}${calculateEstimateNumber(monthlyPrice)} ` + - `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput / - 10} - ${throughput} RU/s, ${currencySign}${pricePerRu}/RU)` + `(${regions} ${regions === 1 ? "region" : "regions"}, ${ + throughput / 10 + } - ${throughput} RU/s, ${currencySign}${pricePerRu}/RU)` ); } @@ -212,7 +213,7 @@ export function getEstimatedSpendHtml( requestUnits: throughput, numberOfRegions: regions, multimasterEnabled: multimaster, - isAutoscale: false + isAutoscale: false, }); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; @@ -243,7 +244,7 @@ export function getEstimatedSpendAcknowledgeString( requestUnits: throughput, numberOfRegions: regions, multimasterEnabled: multimaster, - isAutoscale: isAutoscale + isAutoscale: isAutoscale, }); const dailyPrice: number = hourlyPrice * 24; const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth; diff --git a/src/Utils/QueryUtils.test.ts b/src/Utils/QueryUtils.test.ts index 3f93dbedd..d05f57d2e 100644 --- a/src/Utils/QueryUtils.test.ts +++ b/src/Utils/QueryUtils.test.ts @@ -1,187 +1,187 @@ -import * as DataModels from "../Contracts/DataModels"; -import * as Q from "q"; -import * as sinon from "sinon"; -import * as ViewModels from "../Contracts/ViewModels"; -import { QueryUtils } from "./QueryUtils"; - -describe("Query Utils", () => { - function generatePartitionKeyForPath(path: string): DataModels.PartitionKey { - return { - paths: [path], - kind: "hash", - version: 2 - }; - } - - describe("buildDocumentsQueryPartitionProjections()", () => { - it("should return empty string if partition key is undefined", () => { - expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", undefined)).toBe(""); - }); - - it("should return empty string if partition key is null", () => { - expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", null)).toBe(""); - }); - - it("should replace slashes and embed projection in square braces", () => { - const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a"); - const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); - - expect(partitionProjection).toContain('c["a"]'); - }); - - it("should embed multiple projections in individual square braces", () => { - const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a/b"); - const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); - - expect(partitionProjection).toContain('c["a"]["b"]'); - }); - - it("should not escape double quotes if partition key definition does not have single quote prefix", () => { - const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath('/"a"'); - const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); - - expect(partitionProjection).toContain('c["a"]'); - }); - - it("should escape single quotes", () => { - const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\"a\"'"); - const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); - - expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]'); - }); - - it("should escape double quotes if partition key definition has single quote prefix", () => { - const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\\\"a\\\"'"); - const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); - - expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]'); - }); - }); - - describe("queryPagesUntilContentPresent()", () => { - const queryResultWithItemsInPage: ViewModels.QueryResults = { - documents: [{ a: "123" }], - activityId: "123", - requestCharge: 1, - hasMoreResults: false, - firstItemIndex: 0, - lastItemIndex: 1, - itemCount: 1 - }; - const queryResultWithNoItemsInPage: ViewModels.QueryResults = { - documents: [], - activityId: "123", - requestCharge: 1, - hasMoreResults: true, - firstItemIndex: 0, - lastItemIndex: 0, - itemCount: 0 - }; - - it("should perform multiple queries until it finds a page that has items", async () => { - const queryStub = sinon - .stub() - .onFirstCall() - .returns(Q.resolve(queryResultWithNoItemsInPage)) - .returns(Q.resolve(queryResultWithItemsInPage)); - - await QueryUtils.queryPagesUntilContentPresent(0, queryStub); - expect(queryStub.callCount).toBe(2); - expect(queryStub.getCall(0).args[0]).toBe(0); - expect(queryStub.getCall(1).args[0]).toBe(0); - }); - - it("should not perform multiple queries if the first page of results has items", done => { - const queryStub = sinon.stub().returns(Q.resolve(queryResultWithItemsInPage)); - QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => { - expect(queryStub.callCount).toBe(1); - expect(queryStub.getCall(0).args[0]).toBe(0); - done(); - }); - }); - - it("should not proceed with subsequent queries if the first one errors out", done => { - const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes")); - QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => { - expect(queryStub.callCount).toBe(1); - expect(queryStub.getCall(0).args[0]).toBe(0); - done(); - }); - }); - }); - - describe("queryAllPages()", () => { - const queryResultWithNoContinuation: ViewModels.QueryResults = { - documents: [{ a: "123" }], - activityId: "123", - requestCharge: 1, - hasMoreResults: false, - firstItemIndex: 1, - lastItemIndex: 1, - itemCount: 1 - }; - const queryResultWithContinuation: ViewModels.QueryResults = { - documents: [{ b: "123" }], - activityId: "123", - requestCharge: 1, - hasMoreResults: true, - firstItemIndex: 0, - lastItemIndex: 0, - itemCount: 1 - }; - - it("should follow continuation token to fetch all pages", done => { - const queryStub = sinon - .stub() - .onFirstCall() - .returns(Q.resolve(queryResultWithContinuation)) - .returns(Q.resolve(queryResultWithNoContinuation)); - QueryUtils.queryAllPages(queryStub).then( - (results: ViewModels.QueryResults) => { - expect(queryStub.callCount).toBe(2); - expect(queryStub.getCall(0).args[0]).toBe(0); - expect(queryStub.getCall(1).args[0]).toBe(1); - expect(results.itemCount).toBe( - queryResultWithContinuation.documents.length + queryResultWithNoContinuation.documents.length - ); - expect(results.requestCharge).toBe( - queryResultWithContinuation.requestCharge + queryResultWithNoContinuation.requestCharge - ); - expect(results.documents).toEqual( - queryResultWithContinuation.documents.concat(queryResultWithNoContinuation.documents) - ); - done(); - }, - (error: any) => { - fail(error); - } - ); - }); - - it("should not perform subsequent fetches when result has no continuation", done => { - const queryStub = sinon.stub().returns(Q.resolve(queryResultWithNoContinuation)); - QueryUtils.queryAllPages(queryStub).then( - (results: ViewModels.QueryResults) => { - expect(queryStub.callCount).toBe(1); - expect(queryStub.getCall(0).args[0]).toBe(0); - expect(results.itemCount).toBe(queryResultWithNoContinuation.documents.length); - expect(results.requestCharge).toBe(queryResultWithNoContinuation.requestCharge); - expect(results.documents).toEqual(queryResultWithNoContinuation.documents); - done(); - }, - (error: any) => { - fail(error); - } - ); - }); - - it("should not proceed with subsequent fetches if the first one errors out", done => { - const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes")); - QueryUtils.queryAllPages(queryStub).finally(() => { - expect(queryStub.callCount).toBe(1); - expect(queryStub.getCall(0).args[0]).toBe(0); - done(); - }); - }); - }); -}); +import * as DataModels from "../Contracts/DataModels"; +import * as Q from "q"; +import * as sinon from "sinon"; +import * as ViewModels from "../Contracts/ViewModels"; +import { QueryUtils } from "./QueryUtils"; + +describe("Query Utils", () => { + function generatePartitionKeyForPath(path: string): DataModels.PartitionKey { + return { + paths: [path], + kind: "hash", + version: 2, + }; + } + + describe("buildDocumentsQueryPartitionProjections()", () => { + it("should return empty string if partition key is undefined", () => { + expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", undefined)).toBe(""); + }); + + it("should return empty string if partition key is null", () => { + expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", null)).toBe(""); + }); + + it("should replace slashes and embed projection in square braces", () => { + const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a"); + const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); + + expect(partitionProjection).toContain('c["a"]'); + }); + + it("should embed multiple projections in individual square braces", () => { + const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a/b"); + const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); + + expect(partitionProjection).toContain('c["a"]["b"]'); + }); + + it("should not escape double quotes if partition key definition does not have single quote prefix", () => { + const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath('/"a"'); + const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); + + expect(partitionProjection).toContain('c["a"]'); + }); + + it("should escape single quotes", () => { + const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\"a\"'"); + const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); + + expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]'); + }); + + it("should escape double quotes if partition key definition has single quote prefix", () => { + const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\\\"a\\\"'"); + const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); + + expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]'); + }); + }); + + describe("queryPagesUntilContentPresent()", () => { + const queryResultWithItemsInPage: ViewModels.QueryResults = { + documents: [{ a: "123" }], + activityId: "123", + requestCharge: 1, + hasMoreResults: false, + firstItemIndex: 0, + lastItemIndex: 1, + itemCount: 1, + }; + const queryResultWithNoItemsInPage: ViewModels.QueryResults = { + documents: [], + activityId: "123", + requestCharge: 1, + hasMoreResults: true, + firstItemIndex: 0, + lastItemIndex: 0, + itemCount: 0, + }; + + it("should perform multiple queries until it finds a page that has items", async () => { + const queryStub = sinon + .stub() + .onFirstCall() + .returns(Q.resolve(queryResultWithNoItemsInPage)) + .returns(Q.resolve(queryResultWithItemsInPage)); + + await QueryUtils.queryPagesUntilContentPresent(0, queryStub); + expect(queryStub.callCount).toBe(2); + expect(queryStub.getCall(0).args[0]).toBe(0); + expect(queryStub.getCall(1).args[0]).toBe(0); + }); + + it("should not perform multiple queries if the first page of results has items", (done) => { + const queryStub = sinon.stub().returns(Q.resolve(queryResultWithItemsInPage)); + QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => { + expect(queryStub.callCount).toBe(1); + expect(queryStub.getCall(0).args[0]).toBe(0); + done(); + }); + }); + + it("should not proceed with subsequent queries if the first one errors out", (done) => { + const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes")); + QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => { + expect(queryStub.callCount).toBe(1); + expect(queryStub.getCall(0).args[0]).toBe(0); + done(); + }); + }); + }); + + describe("queryAllPages()", () => { + const queryResultWithNoContinuation: ViewModels.QueryResults = { + documents: [{ a: "123" }], + activityId: "123", + requestCharge: 1, + hasMoreResults: false, + firstItemIndex: 1, + lastItemIndex: 1, + itemCount: 1, + }; + const queryResultWithContinuation: ViewModels.QueryResults = { + documents: [{ b: "123" }], + activityId: "123", + requestCharge: 1, + hasMoreResults: true, + firstItemIndex: 0, + lastItemIndex: 0, + itemCount: 1, + }; + + it("should follow continuation token to fetch all pages", (done) => { + const queryStub = sinon + .stub() + .onFirstCall() + .returns(Q.resolve(queryResultWithContinuation)) + .returns(Q.resolve(queryResultWithNoContinuation)); + QueryUtils.queryAllPages(queryStub).then( + (results: ViewModels.QueryResults) => { + expect(queryStub.callCount).toBe(2); + expect(queryStub.getCall(0).args[0]).toBe(0); + expect(queryStub.getCall(1).args[0]).toBe(1); + expect(results.itemCount).toBe( + queryResultWithContinuation.documents.length + queryResultWithNoContinuation.documents.length + ); + expect(results.requestCharge).toBe( + queryResultWithContinuation.requestCharge + queryResultWithNoContinuation.requestCharge + ); + expect(results.documents).toEqual( + queryResultWithContinuation.documents.concat(queryResultWithNoContinuation.documents) + ); + done(); + }, + (error: any) => { + fail(error); + } + ); + }); + + it("should not perform subsequent fetches when result has no continuation", (done) => { + const queryStub = sinon.stub().returns(Q.resolve(queryResultWithNoContinuation)); + QueryUtils.queryAllPages(queryStub).then( + (results: ViewModels.QueryResults) => { + expect(queryStub.callCount).toBe(1); + expect(queryStub.getCall(0).args[0]).toBe(0); + expect(results.itemCount).toBe(queryResultWithNoContinuation.documents.length); + expect(results.requestCharge).toBe(queryResultWithNoContinuation.requestCharge); + expect(results.documents).toEqual(queryResultWithNoContinuation.documents); + done(); + }, + (error: any) => { + fail(error); + } + ); + }); + + it("should not proceed with subsequent fetches if the first one errors out", (done) => { + const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes")); + QueryUtils.queryAllPages(queryStub).finally(() => { + expect(queryStub.callCount).toBe(1); + expect(queryStub.getCall(0).args[0]).toBe(0); + done(); + }); + }); + }); +}); diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index db374b9e7..2b3aafc90 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -1,118 +1,118 @@ -import Q from "q"; -import * as DataModels from "../Contracts/DataModels"; -import * as ViewModels from "../Contracts/ViewModels"; - -export class QueryUtils { - public static buildDocumentsQuery( - filter: string, - partitionKeyProperty: string, - partitionKey: DataModels.PartitionKey - ): string { - let query: string = partitionKeyProperty - ? `select c.id, c._self, c._rid, c._ts, ${QueryUtils.buildDocumentsQueryPartitionProjections( - "c", - partitionKey - )} as _partitionKeyValue from c` - : `select c.id, c._self, c._rid, c._ts from c`; - - if (filter) { - query += " " + filter; - } - - return query; - } - - public static buildDocumentsQueryPartitionProjections( - collectionAlias: string, - partitionKey: DataModels.PartitionKey - ): string { - if (!partitionKey) { - return ""; - } - - // e.g., path /order/id will be projected as c["order"]["id"], - // to escape any property names that match a keyword - let projections = []; - for (let index in partitionKey.paths) { - // TODO: Handle "/" in partition key definitions - const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1); - let projectedProperty: string = ""; - - projectedProperties.forEach((property: string) => { - const projection = property.trim(); - if (projection.length > 0 && projection.charAt(0) != "'" && projection.charAt(0) != '"') { - projectedProperty = projectedProperty + `["${projection}"]`; - } else if (projection.length > 0 && projection.charAt(0) == "'") { - // trim single quotes and escape double quotes - const projectionSlice = projection.slice(1, projection.length - 1); - projectedProperty = - projectedProperty + `["${projectionSlice.replace(/\\"/g, '"').replace(/"/g, '\\\\\\"')}"]`; - } else { - projectedProperty = projectedProperty + `[${projection}]`; - } - }); - - projections.push(`${collectionAlias}${projectedProperty}`); - } - - return projections.join(","); - } - - public static async queryPagesUntilContentPresent( - firstItemIndex: number, - queryItems: (itemIndex: number) => Promise - ): Promise { - let roundTrips: number = 0; - let netRequestCharge: number = 0; - const doRequest = async (itemIndex: number): Promise => { - const results: ViewModels.QueryResults = await queryItems(itemIndex); - roundTrips = roundTrips + 1; - results.roundTrips = roundTrips; - results.requestCharge = Number(results.requestCharge) + netRequestCharge; - netRequestCharge = Number(results.requestCharge); - const resultsMetadata: ViewModels.QueryResultsMetadata = { - hasMoreResults: results.hasMoreResults, - itemCount: results.itemCount, - firstItemIndex: results.firstItemIndex, - lastItemIndex: results.lastItemIndex - }; - if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) { - return await doRequest(resultsMetadata.lastItemIndex); - } - return results; - }; - - return await doRequest(firstItemIndex); - } - - public static async queryAllPages( - queryItems: (itemIndex: number) => Promise - ): Promise { - const queryResults: ViewModels.QueryResults = { - documents: [], - activityId: undefined, - hasMoreResults: false, - itemCount: 0, - firstItemIndex: 0, - lastItemIndex: 0, - requestCharge: 0, - roundTrips: 0 - }; - const doRequest = async (itemIndex: number): Promise => { - const results: ViewModels.QueryResults = await queryItems(itemIndex); - const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results; - queryResults.roundTrips = queryResults.roundTrips + 1; - queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge); - queryResults.hasMoreResults = hasMoreResults; - queryResults.itemCount = queryResults.itemCount + itemCount; - queryResults.lastItemIndex = lastItemIndex; - queryResults.documents = queryResults.documents.concat(documents); - if (queryResults.hasMoreResults) { - return doRequest(queryResults.lastItemIndex + 1); - } - return queryResults; - }; - - return doRequest(0); - } -} +import Q from "q"; +import * as DataModels from "../Contracts/DataModels"; +import * as ViewModels from "../Contracts/ViewModels"; + +export class QueryUtils { + public static buildDocumentsQuery( + filter: string, + partitionKeyProperty: string, + partitionKey: DataModels.PartitionKey + ): string { + let query: string = partitionKeyProperty + ? `select c.id, c._self, c._rid, c._ts, ${QueryUtils.buildDocumentsQueryPartitionProjections( + "c", + partitionKey + )} as _partitionKeyValue from c` + : `select c.id, c._self, c._rid, c._ts from c`; + + if (filter) { + query += " " + filter; + } + + return query; + } + + public static buildDocumentsQueryPartitionProjections( + collectionAlias: string, + partitionKey: DataModels.PartitionKey + ): string { + if (!partitionKey) { + return ""; + } + + // e.g., path /order/id will be projected as c["order"]["id"], + // to escape any property names that match a keyword + let projections = []; + for (let index in partitionKey.paths) { + // TODO: Handle "/" in partition key definitions + const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1); + let projectedProperty: string = ""; + + projectedProperties.forEach((property: string) => { + const projection = property.trim(); + if (projection.length > 0 && projection.charAt(0) != "'" && projection.charAt(0) != '"') { + projectedProperty = projectedProperty + `["${projection}"]`; + } else if (projection.length > 0 && projection.charAt(0) == "'") { + // trim single quotes and escape double quotes + const projectionSlice = projection.slice(1, projection.length - 1); + projectedProperty = + projectedProperty + `["${projectionSlice.replace(/\\"/g, '"').replace(/"/g, '\\\\\\"')}"]`; + } else { + projectedProperty = projectedProperty + `[${projection}]`; + } + }); + + projections.push(`${collectionAlias}${projectedProperty}`); + } + + return projections.join(","); + } + + public static async queryPagesUntilContentPresent( + firstItemIndex: number, + queryItems: (itemIndex: number) => Promise + ): Promise { + let roundTrips: number = 0; + let netRequestCharge: number = 0; + const doRequest = async (itemIndex: number): Promise => { + const results: ViewModels.QueryResults = await queryItems(itemIndex); + roundTrips = roundTrips + 1; + results.roundTrips = roundTrips; + results.requestCharge = Number(results.requestCharge) + netRequestCharge; + netRequestCharge = Number(results.requestCharge); + const resultsMetadata: ViewModels.QueryResultsMetadata = { + hasMoreResults: results.hasMoreResults, + itemCount: results.itemCount, + firstItemIndex: results.firstItemIndex, + lastItemIndex: results.lastItemIndex, + }; + if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) { + return await doRequest(resultsMetadata.lastItemIndex); + } + return results; + }; + + return await doRequest(firstItemIndex); + } + + public static async queryAllPages( + queryItems: (itemIndex: number) => Promise + ): Promise { + const queryResults: ViewModels.QueryResults = { + documents: [], + activityId: undefined, + hasMoreResults: false, + itemCount: 0, + firstItemIndex: 0, + lastItemIndex: 0, + requestCharge: 0, + roundTrips: 0, + }; + const doRequest = async (itemIndex: number): Promise => { + const results: ViewModels.QueryResults = await queryItems(itemIndex); + const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results; + queryResults.roundTrips = queryResults.roundTrips + 1; + queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge); + queryResults.hasMoreResults = hasMoreResults; + queryResults.itemCount = queryResults.itemCount + itemCount; + queryResults.lastItemIndex = lastItemIndex; + queryResults.documents = queryResults.documents.concat(documents); + if (queryResults.hasMoreResults) { + return doRequest(queryResults.lastItemIndex + 1); + } + return queryResults; + }; + + return doRequest(0); + } +} diff --git a/src/Utils/StringUtils.test.ts b/src/Utils/StringUtils.test.ts index d6f644d46..b1ae4419b 100644 --- a/src/Utils/StringUtils.test.ts +++ b/src/Utils/StringUtils.test.ts @@ -1,30 +1,30 @@ -import { StringUtils } from "./StringUtils"; - -describe("StringUtils", () => { - describe("stripSpacesFromString()", () => { - it("should strip all spaces from input string", () => { - const transformedString: string = StringUtils.stripSpacesFromString("a b c"); - expect(transformedString).toBe("abc"); - }); - - it("should return original string if input string has no spaces", () => { - const transformedString: string = StringUtils.stripSpacesFromString("abc"); - expect(transformedString).toBe("abc"); - }); - - it("should return null if input is null", () => { - const transformedString: string = StringUtils.stripSpacesFromString(null); - expect(transformedString).toBeNull(); - }); - - it("should return undefined if input is undefiend", () => { - const transformedString: string = StringUtils.stripSpacesFromString(undefined); - expect(transformedString).toBe(undefined); - }); - - it("should return empty string if input is an empty string", () => { - const transformedString: string = StringUtils.stripSpacesFromString(""); - expect(transformedString).toBe(""); - }); - }); -}); +import { StringUtils } from "./StringUtils"; + +describe("StringUtils", () => { + describe("stripSpacesFromString()", () => { + it("should strip all spaces from input string", () => { + const transformedString: string = StringUtils.stripSpacesFromString("a b c"); + expect(transformedString).toBe("abc"); + }); + + it("should return original string if input string has no spaces", () => { + const transformedString: string = StringUtils.stripSpacesFromString("abc"); + expect(transformedString).toBe("abc"); + }); + + it("should return null if input is null", () => { + const transformedString: string = StringUtils.stripSpacesFromString(null); + expect(transformedString).toBeNull(); + }); + + it("should return undefined if input is undefiend", () => { + const transformedString: string = StringUtils.stripSpacesFromString(undefined); + expect(transformedString).toBe(undefined); + }); + + it("should return empty string if input is an empty string", () => { + const transformedString: string = StringUtils.stripSpacesFromString(""); + expect(transformedString).toBe(""); + }); + }); +}); diff --git a/src/Utils/StringUtils.ts b/src/Utils/StringUtils.ts index 58b012887..303b99931 100644 --- a/src/Utils/StringUtils.ts +++ b/src/Utils/StringUtils.ts @@ -1,21 +1,21 @@ -export class StringUtils { - public static stripSpacesFromString(inputString: string): string { - if (inputString == null || typeof inputString !== "string") { - return inputString; - } - return inputString.replace(/ /g, ""); - } - - /** - * Implementation of endsWith which works for IE - * @param stringToTest - * @param suffix - */ - public static endsWith(stringToTest: string, suffix: string): boolean { - return stringToTest.indexOf(suffix, stringToTest.length - suffix.length) !== -1; - } - - public static startsWith(stringToTest: string, prefix: string): boolean { - return stringToTest.indexOf(prefix) === 0; - } -} +export class StringUtils { + public static stripSpacesFromString(inputString: string): string { + if (inputString == null || typeof inputString !== "string") { + return inputString; + } + return inputString.replace(/ /g, ""); + } + + /** + * Implementation of endsWith which works for IE + * @param stringToTest + * @param suffix + */ + public static endsWith(stringToTest: string, suffix: string): boolean { + return stringToTest.indexOf(suffix, stringToTest.length - suffix.length) !== -1; + } + + public static startsWith(stringToTest: string, prefix: string): boolean { + return stringToTest.indexOf(prefix) === 0; + } +} diff --git a/src/Utils/UserUtils.ts b/src/Utils/UserUtils.ts index 5275b2702..78270c37b 100644 --- a/src/Utils/UserUtils.ts +++ b/src/Utils/UserUtils.ts @@ -1,8 +1,8 @@ -import { decryptJWTToken } from "./AuthorizationUtils"; -import { userContext } from "../UserContext"; - -export function getFullName(): string { - const authToken = userContext.authorizationToken; - const props = decryptJWTToken(authToken); - return props.name; -} +import { decryptJWTToken } from "./AuthorizationUtils"; +import { userContext } from "../UserContext"; + +export function getFullName(): string { + const authToken = userContext.authorizationToken; + const props = decryptJWTToken(authToken); + return props.name; +} diff --git a/src/Utils/WindowUtils.test.ts b/src/Utils/WindowUtils.test.ts index 567219e22..f2c9ac7d4 100644 --- a/src/Utils/WindowUtils.test.ts +++ b/src/Utils/WindowUtils.test.ts @@ -1,39 +1,39 @@ -import { getDataExplorerWindow } from "./WindowUtils"; - -interface MockWindow { - parent?: MockWindow; - top?: MockWindow; -} - -describe("WindowUtils", () => { - describe("getDataExplorerWindow", () => { - it("should return undefined if current window is at the top", () => { - const mockWindow: MockWindow = {}; - mockWindow.parent = mockWindow; - - expect(getDataExplorerWindow(mockWindow as Window)).toEqual(undefined); - }); - - it("should return current window if parent is top", () => { - const dataExplorerWindow: MockWindow = {}; - const portalWindow: MockWindow = {}; - dataExplorerWindow.parent = portalWindow; - dataExplorerWindow.top = portalWindow; - - expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow); - }); - - it("should return closest window to top if in nested windows", () => { - const terminalWindow: MockWindow = {}; - const dataExplorerWindow: MockWindow = {}; - const portalWindow: MockWindow = {}; - dataExplorerWindow.top = portalWindow; - dataExplorerWindow.parent = portalWindow; - terminalWindow.top = portalWindow; - terminalWindow.parent = dataExplorerWindow; - - expect(getDataExplorerWindow(terminalWindow as Window)).toEqual(dataExplorerWindow); - expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow); - }); - }); -}); +import { getDataExplorerWindow } from "./WindowUtils"; + +interface MockWindow { + parent?: MockWindow; + top?: MockWindow; +} + +describe("WindowUtils", () => { + describe("getDataExplorerWindow", () => { + it("should return undefined if current window is at the top", () => { + const mockWindow: MockWindow = {}; + mockWindow.parent = mockWindow; + + expect(getDataExplorerWindow(mockWindow as Window)).toEqual(undefined); + }); + + it("should return current window if parent is top", () => { + const dataExplorerWindow: MockWindow = {}; + const portalWindow: MockWindow = {}; + dataExplorerWindow.parent = portalWindow; + dataExplorerWindow.top = portalWindow; + + expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow); + }); + + it("should return closest window to top if in nested windows", () => { + const terminalWindow: MockWindow = {}; + const dataExplorerWindow: MockWindow = {}; + const portalWindow: MockWindow = {}; + dataExplorerWindow.top = portalWindow; + dataExplorerWindow.parent = portalWindow; + terminalWindow.top = portalWindow; + terminalWindow.parent = dataExplorerWindow; + + expect(getDataExplorerWindow(terminalWindow as Window)).toEqual(dataExplorerWindow); + expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow); + }); + }); +}); diff --git a/src/Utils/WindowUtils.ts b/src/Utils/WindowUtils.ts index e24fd8290..1ddd451e8 100644 --- a/src/Utils/WindowUtils.ts +++ b/src/Utils/WindowUtils.ts @@ -1,18 +1,18 @@ -export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => { - // Data explorer is always loaded in an iframe, so traverse the parents until we hit the top and return the first child window. - try { - while (currentWindow) { - if (currentWindow.parent === currentWindow) { - return undefined; - } - if (currentWindow.parent === currentWindow.top) { - return currentWindow; - } - currentWindow = currentWindow.parent; - } - } catch (error) { - // Hitting a cross domain error means we are in the portal and the current window is data explorer - return currentWindow; - } - return undefined; -}; +export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => { + // Data explorer is always loaded in an iframe, so traverse the parents until we hit the top and return the first child window. + try { + while (currentWindow) { + if (currentWindow.parent === currentWindow) { + return undefined; + } + if (currentWindow.parent === currentWindow.top) { + return currentWindow; + } + currentWindow = currentWindow.parent; + } + } catch (error) { + // Hitting a cross domain error means we are in the portal and the current window is data explorer + return currentWindow; + } + return undefined; +}; diff --git a/src/Utils/arm/request.test.ts b/src/Utils/arm/request.test.ts index 6ed76093c..06ba1a7bb 100644 --- a/src/Utils/arm/request.test.ts +++ b/src/Utils/arm/request.test.ts @@ -12,7 +12,7 @@ interface Global { describe("ARM request", () => { window.authType = AuthType.AAD; updateUserContext({ - authorizationToken: "some-token" + authorizationToken: "some-token", }); it("should call window.fetch", async () => { @@ -20,7 +20,7 @@ describe("ARM request", () => { ok: true, json: async () => { return {}; - } + }, }); await armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" }); expect(window.fetch).toHaveBeenCalled(); @@ -33,7 +33,7 @@ describe("ARM request", () => { ok: true, headers, status: 200, - json: async () => ({}) + json: async () => ({}), }); await armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" }); expect(window.fetch).toHaveBeenCalledTimes(2); @@ -48,7 +48,7 @@ describe("ARM request", () => { status: 200, json: async () => { return { status: "Failed" }; - } + }, }); await expect(() => armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" }) @@ -59,7 +59,7 @@ describe("ARM request", () => { it("should throw token error", async () => { window.authType = AuthType.AAD; updateUserContext({ - authorizationToken: undefined + authorizationToken: undefined, }); const headers = new Headers(); headers.set("location", "https://foo.com/operationStatus"); @@ -69,7 +69,7 @@ describe("ARM request", () => { status: 200, json: async () => { return { status: "Failed" }; - } + }, }); await expect(() => armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" }) diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts index 429f69c51..b2d4adce8 100644 --- a/src/Utils/arm/request.ts +++ b/src/Utils/arm/request.ts @@ -54,7 +54,7 @@ export async function armRequest({ apiVersion, method, body: requestBody, - queryParams + queryParams, }: Options): Promise { const url = new URL(path, host); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); @@ -70,9 +70,9 @@ export async function armRequest({ const response = await window.fetch(url.href, { method, headers: { - Authorization: userContext.authorizationToken + Authorization: userContext.authorizationToken, }, - body: requestBody ? JSON.stringify(requestBody) : undefined + body: requestBody ? JSON.stringify(requestBody) : undefined, }); if (!response.ok) { let error: ARMError; @@ -108,8 +108,8 @@ async function getOperationStatus(operationStatusUrl: string) { const response = await window.fetch(operationStatusUrl, { headers: { - Authorization: userContext.authorizationToken - } + Authorization: userContext.authorizationToken, + }, }); if (!response.ok) { diff --git a/src/connectToGitHub.html b/src/connectToGitHub.html index 1470c2ec1..8d12c45bc 100644 --- a/src/connectToGitHub.html +++ b/src/connectToGitHub.html @@ -1,14 +1,14 @@ - - - - - - - - Connect to GitHub - - - - Connecting to GitHub... - - + + + + + + + + Connect to GitHub + + + + Connecting to GitHub... + + diff --git a/src/explorer.html b/src/explorer.html index 5b3fb8c1b..521484b62 100644 --- a/src/explorer.html +++ b/src/explorer.html @@ -1,12 +1,12 @@ - - - - - - - Azure Cosmos DB - - - - - + + + + + + + Azure Cosmos DB + + + + + diff --git a/src/global.d.ts b/src/global.d.ts index f33de89cc..3ef031859 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,13 +1,13 @@ -import { AuthType } from "./AuthType"; -import Explorer from "./Explorer/Explorer"; - -declare global { - interface Window { - authType: AuthType; - dataExplorer: Explorer; - __REACT_DEVTOOLS_GLOBAL_HOOK__: any; - $: any; - jQuery: any; - gitSha: string; - } -} +import { AuthType } from "./AuthType"; +import Explorer from "./Explorer/Explorer"; + +declare global { + interface Window { + authType: AuthType; + dataExplorer: Explorer; + __REACT_DEVTOOLS_GLOBAL_HOOK__: any; + $: any; + jQuery: any; + gitSha: string; + } +} diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index dce78d087..c6d9521a5 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -4,12 +4,12 @@ import { UserAgentApplication, Account, Configuration } from "msal"; const config: Configuration = { cache: { - cacheLocation: "localStorage" + cacheLocation: "localStorage", }, auth: { authority: "https://login.microsoftonline.com/common", - clientId: "203f1145-856a-4232-83d4-a43568fba23d" - } + clientId: "203f1145-856a-4232-83d4-a43568fba23d", + }, }; if (process.env.NODE_ENV === "development") { @@ -56,9 +56,9 @@ export function useAADAuth(): ReturnType { }, []); const switchTenant = React.useCallback( - async id => { + async (id) => { const response = await msal.loginPopup({ - authority: `https://login.microsoftonline.com/${id}` + authority: `https://login.microsoftonline.com/${id}`, }); setTenantId(response.tenantId); setAccount(response.account); @@ -73,14 +73,14 @@ export function useAADAuth(): ReturnType { // There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority forceRefresh: true, authority: `https://login.microsoftonline.com/${tenantId}`, - scopes: ["https://graph.windows.net//.default"] + scopes: ["https://graph.windows.net//.default"], }), msal.acquireTokenSilent({ // There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority forceRefresh: true, authority: `https://login.microsoftonline.com/${tenantId}`, - scopes: ["https://management.azure.com//.default"] - }) + scopes: ["https://management.azure.com//.default"], + }), ]).then(([graphTokenResponse, armTokenResponse]) => { setGraphToken(graphTokenResponse.accessToken); setArmToken(armTokenResponse.accessToken); @@ -96,6 +96,6 @@ export function useAADAuth(): ReturnType { armToken, login, logout, - switchTenant + switchTenant, }; } diff --git a/src/hooks/useDirectories.tsx b/src/hooks/useDirectories.tsx index 70b36e18e..e78ff5a14 100644 --- a/src/hooks/useDirectories.tsx +++ b/src/hooks/useDirectories.tsx @@ -33,7 +33,7 @@ export function useDirectories(armToken: string): Tenant[] { useEffect(() => { if (armToken) { - fetchDirectories(armToken).then(response => setState(response)); + fetchDirectories(armToken).then((response) => setState(response)); } }, [armToken]); return state || []; diff --git a/src/hooks/useGraphPhoto.tsx b/src/hooks/useGraphPhoto.tsx index 92b11925c..1afdc56cb 100644 --- a/src/hooks/useGraphPhoto.tsx +++ b/src/hooks/useGraphPhoto.tsx @@ -9,10 +9,10 @@ export async function fetchPhoto(accessToken: string): Promise { const options = { method: "GET", - headers: headers + headers: headers, }; - return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options).then(response => + return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options).then((response) => response.blob() ); } @@ -22,7 +22,7 @@ export function useGraphPhoto(graphToken: string): string { useEffect(() => { if (graphToken) { - fetchPhoto(graphToken).then(response => setPhoto(URL.createObjectURL(response))); + fetchPhoto(graphToken).then((response) => setPhoto(URL.createObjectURL(response))); } }, [graphToken]); return photo; diff --git a/src/hooks/usePortalAccessToken.tsx b/src/hooks/usePortalAccessToken.tsx index 7a4d856a2..da5cbe2f8 100644 --- a/src/hooks/usePortalAccessToken.tsx +++ b/src/hooks/usePortalAccessToken.tsx @@ -12,15 +12,15 @@ export async function fetchAccessData(portalToken: string): Promise response.json()) + .then((response) => response.json()) // Portal encrypted token API quirk: The response is double JSON encoded - .then(json => JSON.parse(json)) - .catch(error => console.error(error)) + .then((json) => JSON.parse(json)) + .catch((error) => console.error(error)) ); } @@ -29,7 +29,7 @@ export function useTokenMetadata(token: string): AccessInputMetadata { useEffect(() => { if (token) { - fetchAccessData(token).then(response => setState(response)); + fetchAccessData(token).then((response) => setState(response)); } }, [token]); return state; diff --git a/src/hooks/useSubscriptions.tsx b/src/hooks/useSubscriptions.tsx index d5627c2d6..954a2539b 100644 --- a/src/hooks/useSubscriptions.tsx +++ b/src/hooks/useSubscriptions.tsx @@ -24,7 +24,7 @@ export async function fetchSubscriptions(accessToken: string): Promise sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue" + (sub) => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue" ); subscriptions = [...subscriptions, ...validSubscriptions]; } diff --git a/src/hostedExplorer.html b/src/hostedExplorer.html index 1c46ca907..db733e6ee 100644 --- a/src/hostedExplorer.html +++ b/src/hostedExplorer.html @@ -1,12 +1,12 @@ - - - - - Azure Cosmos DB - - - - -
- - + + + + + Azure Cosmos DB + + + + +
+ + diff --git a/src/index.html b/src/index.html index 14dd33a2e..af0bddcc8 100644 --- a/src/index.html +++ b/src/index.html @@ -1,60 +1,60 @@ - - - - - - - Azure Cosmos DB Emulator - - - - - -
-
- Azure Cosmos DB - - Create an Azure Cosmos DB account - - Azure Cosmos DB Emulator -
-
- - - - - - - - + + + + + + + Azure Cosmos DB Emulator + + + + + +
+
+ Azure Cosmos DB + + Create an Azure Cosmos DB account + + Azure Cosmos DB Emulator +
+
+ + + + + + + + diff --git a/src/koComment.tsx b/src/koComment.tsx index 670ca5aea..7b9f8f889 100644 --- a/src/koComment.tsx +++ b/src/koComment.tsx @@ -1,20 +1,20 @@ -/* eslint-disable react/prop-types */ -import React, { useEffect, useRef } from "react"; - -export const KOCommentIfStart: React.FunctionComponent<{ if: string }> = props => { - const el = useRef(); - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (el.current as any).outerHTML = ``; - }, []); - return
; -}; - -export const KOCommentEnd: React.FunctionComponent = () => { - const el = useRef(); - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (el.current as any).outerHTML = ``; - }, []); - return
; -}; +/* eslint-disable react/prop-types */ +import React, { useEffect, useRef } from "react"; + +export const KOCommentIfStart: React.FunctionComponent<{ if: string }> = (props) => { + const el = useRef(); + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (el.current as any).outerHTML = ``; + }, []); + return
; +}; + +export const KOCommentEnd: React.FunctionComponent = () => { + const el = useRef(); + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (el.current as any).outerHTML = ``; + }, []); + return
; +}; diff --git a/src/quickstart.html b/src/quickstart.html index 2fa753ac6..7e678292c 100644 --- a/src/quickstart.html +++ b/src/quickstart.html @@ -1,353 +1,353 @@ - - - - - - - Azure Cosmos DB Emulator - - - -
-
-

Congratulations! Your Azure Cosmos DB emulator is running.

-

Now, let's connect a sample app to it.

-
-

URI

- -

Primary Key

- -

Primary Connection String

- -

Mongo Connection String

- -
-

Choose a platform

-
-
- -
-
-
-
1
-
- Open and run a sample .NET app -

- We created a sample .NET app connected to your Azure Cosmos DB Emulator instance. Download, extract, - build and run the app. -

- -
-
- -
-
2
-
- Learn more about Azure Cosmos DB - -
-
-
-
-
-
1
-
- Open and run a sample .NET Core app -

- We created a sample .NET Core app connected to your Azure Cosmos DB Emulator instance. Download, - extract, build and run the app. -

- -
-
- -
-
2
-
- Learn more about Azure Cosmos DB. - -
-
-
- -
-
-
1
-
- Open and run a sample Java app -

- We created a sample Java app connected to your Azure Cosmos DB Emulator instance. Download, extract, - build and run the app. -

- -

- Follow instructions in the readme.md to setup prerequisites needed to run Java web apps, if you - haven’t already. -

-
-
- -
-
2
-
- Learn more about Azure Cosmos DB. - -
-
-
- -
-
-
1
-
- Open and run a sample Node.js app -

- We created a sample Node.js app connected to your Azure Cosmos DB Emulator instance. Download, - extract, build and run the app. -

- -

- Run npm install and npm start, and navigate to - http://localhost:3000. -

-
-
- -
-
2
-
- Learn more about Azure Cosmos DB. - -
-
-
- -
-
-
1
-
- Create a new Python app. -

- Follow this - tutorial - to create a new Python app connected to Azure Cosmos DB. -

-
-
- -
-
2
-
- Learn more about Azure Cosmos DB. - -
-
-
-
-
-
- - + + + + + + + Azure Cosmos DB Emulator + + + +
+
+

Congratulations! Your Azure Cosmos DB emulator is running.

+

Now, let's connect a sample app to it.

+
+

URI

+ +

Primary Key

+ +

Primary Connection String

+ +

Mongo Connection String

+ +
+

Choose a platform

+
+
+ +
+
+
+
1
+
+ Open and run a sample .NET app +

+ We created a sample .NET app connected to your Azure Cosmos DB Emulator instance. Download, extract, + build and run the app. +

+ +
+
+ +
+
2
+
+ Learn more about Azure Cosmos DB + +
+
+
+
+
+
1
+
+ Open and run a sample .NET Core app +

+ We created a sample .NET Core app connected to your Azure Cosmos DB Emulator instance. Download, + extract, build and run the app. +

+ +
+
+ +
+
2
+
+ Learn more about Azure Cosmos DB. + +
+
+
+ +
+
+
1
+
+ Open and run a sample Java app +

+ We created a sample Java app connected to your Azure Cosmos DB Emulator instance. Download, extract, + build and run the app. +

+ +

+ Follow instructions in the readme.md to setup prerequisites needed to run Java web apps, if you + haven’t already. +

+
+
+ +
+
2
+
+ Learn more about Azure Cosmos DB. + +
+
+
+ +
+
+
1
+
+ Open and run a sample Node.js app +

+ We created a sample Node.js app connected to your Azure Cosmos DB Emulator instance. Download, + extract, build and run the app. +

+ +

+ Run npm install and npm start, and navigate to + http://localhost:3000. +

+
+
+ +
+
2
+
+ Learn more about Azure Cosmos DB. + +
+
+
+ +
+
+
1
+
+ Create a new Python app. +

+ Follow this + tutorial + to create a new Python app connected to Azure Cosmos DB. +

+
+
+ +
+
2
+
+ Learn more about Azure Cosmos DB. + +
+
+
+
+
+
+ + diff --git a/src/quickstart.ts b/src/quickstart.ts index 86130c663..c819e0a65 100644 --- a/src/quickstart.ts +++ b/src/quickstart.ts @@ -1,5 +1,5 @@ -import "bootstrap/dist/css/bootstrap.css"; -import "../less/quickstart.less"; - -import "./Libs/jquery"; -import "bootstrap/dist/js/npm"; +import "bootstrap/dist/css/bootstrap.css"; +import "../less/quickstart.less"; + +import "./Libs/jquery"; +import "bootstrap/dist/js/npm"; diff --git a/src/workers/upload/index.ts b/src/workers/upload/index.ts index 5e38b8d79..10b02a18c 100644 --- a/src/workers/upload/index.ts +++ b/src/workers/upload/index.ts @@ -21,7 +21,7 @@ onerror = (event: ProgressEvent) => { numUploadsFailed: numUploadsFailed, uploadDetails: transformDetailsMap(fileUploadDetails), // TODO: Typescript complains about event.error below - runtimeError: (event as any).error.message + runtimeError: (event as any).error.message, }, undefined ); @@ -36,10 +36,10 @@ onmessage = (event: MessageEvent) => { masterKey: clientParams.masterKey, endpoint: clientParams.endpoint, accessToken: clientParams.accessToken, - databaseAccount: clientParams.databaseAccount + databaseAccount: clientParams.databaseAccount, }); updateConfigContext({ - platform: clientParams.platform + platform: clientParams.platform, }); if (!!files && files.length > 0) { numFiles = files.length; @@ -48,14 +48,14 @@ onmessage = (event: MessageEvent) => { fileName: files[i].name, numSucceeded: 0, numFailed: 0, - errors: [] + errors: [], }; uploadFile(files[i]); } } else { postMessage( { - runtimeError: "No files specified" + runtimeError: "No files specified", }, undefined ); @@ -88,11 +88,11 @@ function createDocumentsFromFile(fileName: string, documentContent: string): voi .database(databaseId) .container(containerId) .items.create(documentContent) - .then(savedDoc => { + .then((savedDoc) => { fileUploadDetails[fileName].numSucceeded++; numUploadsSuccessful++; }) - .catch(error => { + .catch((error) => { console.error(error); recordUploadDetailErrorForFile(fileName, getErrorMessage(error)); numUploadsFailed++; @@ -124,7 +124,7 @@ function transmitResultIfUploadComplete(): void { { numUploadsSuccessful: numUploadsSuccessful, numUploadsFailed: numUploadsFailed, - uploadDetails: transformDetailsMap(fileUploadDetails) + uploadDetails: transformDetailsMap(fileUploadDetails), }, undefined ); diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 47a15a697..c1a0ad198 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -38,7 +38,7 @@ describe("Collection Add and Delete Cassandra spec", () => { await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); - const selectedDbId = await frame.evaluate(element => { + const selectedDbId = await frame.evaluate((element) => { return element.attributes["data-test"].textContent; }, databases[0]); @@ -46,8 +46,8 @@ describe("Collection Add and Delete Cassandra spec", () => { await frame.waitFor(CREATE_DELAY); await frame.waitFor("div[class='rowData'] > span[class='message']"); - const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { - return elements.some(el => el.textContent.includes("Successfully created")); + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", (elements) => { + return elements.some((el) => el.textContent.includes("Successfully created")); }); expect(didCreateContainer).toBe(true); @@ -63,10 +63,10 @@ describe("Collection Add and Delete Cassandra spec", () => { if (collections.length) { await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, { - visible: true + visible: true, }); - const textId = await frame.evaluate(element => { + const textId = await frame.evaluate((element) => { return element.attributes["data-test"].textContent; }, collections[0]); await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); @@ -138,7 +138,7 @@ async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { await frame.waitFor(RETRY_DELAY); const button = await frame.$(`div[data-test="${dbId}"]`); - const classList = await frame.evaluate(button => { + const classList = await frame.evaluate((button) => { return button.parentElement.classList; }, button); if (!Object.values(classList).includes("selected") && retries < 5) { diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 31ee62762..7c995e13d 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -58,7 +58,7 @@ describe("Collection Add and Delete Mongo spec", () => { await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); - const selectedDbId = await frame.evaluate(element => { + const selectedDbId = await frame.evaluate((element) => { return element.attributes["data-test"].textContent; }, databases[0]); @@ -66,8 +66,8 @@ describe("Collection Add and Delete Mongo spec", () => { await frame.waitFor(CREATE_DELAY); await frame.waitFor("div[class='rowData'] > span[class='message']"); - const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { - return elements.some(el => el.textContent.includes("Successfully created")); + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", (elements) => { + return elements.some((el) => el.textContent.includes("Successfully created")); }); expect(didCreateContainer).toBe(true); @@ -82,7 +82,7 @@ describe("Collection Add and Delete Mongo spec", () => { ); if (collections.length) { - const textId = await frame.evaluate(element => { + const textId = await frame.evaluate((element) => { return element.attributes["data-test"].textContent; }, collections[0]); await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); @@ -154,7 +154,7 @@ async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { await frame.waitFor(RETRY_DELAY); const button = await frame.$(`div[data-test="${dbId}"]`); - const classList = await frame.evaluate(button => { + const classList = await frame.evaluate((button) => { return button.parentElement.classList; }, button); if (!Object.values(classList).includes("selected") && retries < 5) { diff --git a/test/notebooks/notebookTestUtils.ts b/test/notebooks/notebookTestUtils.ts index 75826b509..1001344be 100644 --- a/test/notebooks/notebookTestUtils.ts +++ b/test/notebooks/notebookTestUtils.ts @@ -42,7 +42,7 @@ export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader"); for (let i = 1; i < treeNodeHeaders.length; i++) { currentNotebookNode = treeNodeHeaders[i]; - const nodeLabel = await currentNotebookNode.$eval(".nodeLabel", element => element.textContent); + const nodeLabel = await currentNotebookNode.$eval(".nodeLabel", (element) => element.textContent); if (nodeLabel === uploadNotebookName) { return currentNotebookNode; } diff --git a/test/notebooks/uploadAndOpenNotebook.spec.ts b/test/notebooks/uploadAndOpenNotebook.spec.ts index c7017eb3d..4d76d9e56 100644 --- a/test/notebooks/uploadAndOpenNotebook.spec.ts +++ b/test/notebooks/uploadAndOpenNotebook.spec.ts @@ -16,7 +16,7 @@ describe("Notebook UI tests", () => { uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName); await uploadedNotebookNode.click(); await frame.waitForSelector(".tabNavText"); - const tabTitle = await frame.$eval(".tabNavText", element => element.textContent); + const tabTitle = await frame.$eval(".tabNavText", (element) => element.textContent); expect(tabTitle).toEqual(notebookName); const closeIcon = await frame.waitForSelector(".close-Icon"); await closeIcon.click(); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 853eff9e3..f1f8fa49a 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -57,7 +57,7 @@ describe("Collection Add and Delete SQL spec", () => { await frame.waitFor(CREATE_DELAY); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); - const selectedDbId = await frame.evaluate(element => { + const selectedDbId = await frame.evaluate((element) => { return element.attributes["data-test"].textContent; }, databases[0]); @@ -65,8 +65,8 @@ describe("Collection Add and Delete SQL spec", () => { await frame.waitFor(CREATE_DELAY); await frame.waitFor("div[class='rowData'] > span[class='message']"); - const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { - return elements.some(el => el.textContent.includes("Successfully created")); + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", (elements) => { + return elements.some((el) => el.textContent.includes("Successfully created")); }); expect(didCreateContainer).toBe(true); @@ -82,10 +82,10 @@ describe("Collection Add and Delete SQL spec", () => { if (collections.length) { await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, { - visible: true + visible: true, }); - const textId = await frame.evaluate(element => { + const textId = await frame.evaluate((element) => { return element.attributes["data-test"].textContent; }, collections[0]); await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); @@ -157,7 +157,7 @@ async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { await frame.waitFor(RETRY_DELAY); const button = await frame.$(`div[data-test="${dbId}"]`); - const classList = await frame.evaluate(button => { + const classList = await frame.evaluate((button) => { return button.parentElement.classList; }, button); if (!Object.values(classList).includes("selected") && retries < 5) { diff --git a/test/sql/resourceToken.spec.ts b/test/sql/resourceToken.spec.ts index bf9907f75..fbd4f6608 100644 --- a/test/sql/resourceToken.spec.ts +++ b/test/sql/resourceToken.spec.ts @@ -21,7 +21,7 @@ describe("Collection Add and Delete SQL spec", () => { const { resource: containerPermission } = await user.permissions.upsert({ id: "partitionLevelPermission", permissionMode: PermissionMode.All, - resource: container.url + resource: container.url, }); const resourceTokenConnectionString = `AccountEndpoint=${endpoint};DatabaseId=${database.id};CollectionId=${container.id};${containerPermission._token}`; try { @@ -60,7 +60,7 @@ async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { await frame.waitFor(RETRY_DELAY); const button = await frame.$(`div[data-test="${dbId}"]`); - const classList = await frame.evaluate(button => { + const classList = await frame.evaluate((button) => { return button.parentElement.classList; }, button); if (!Object.values(classList).includes("selected") && retries < 5) { diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index 0f1c07eef..974e643ab 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -37,8 +37,8 @@ describe("Collection Add and Delete Tables spec", () => { await frame.waitFor(`div[data-test="TablesDB"]`), { visible: true }; await frame.waitFor(LOADING_STATE_DELAY); - const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { - return elements.some(el => el.textContent.includes("Successfully created")); + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", (elements) => { + return elements.some((el) => el.textContent.includes("Successfully created")); }); expect(didCreateContainer).toBe(true); @@ -51,7 +51,7 @@ describe("Collection Add and Delete Tables spec", () => { const collections = await frame.$$( `div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]` ); - const textId = await frame.evaluate(element => { + const textId = await frame.evaluate((element) => { return element.attributes["data-test"].textContent; }, collections[0]); await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); @@ -101,7 +101,7 @@ async function clickTablesMenu(frame: Frame, retries = 0) { async function ensureMenuIsOpen(frame: Frame, retries: number) { await frame.waitFor(RETRY_DELAY); const button = await frame.$(`div[data-test="TablesDB"]`); - const classList = await frame.evaluate(button => { + const classList = await frame.evaluate((button) => { return button.parentElement.classList; }, button); if (!Object.values(classList).includes("selected") && retries < 5) { diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index c1a83cf67..fec9c10b2 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -58,7 +58,7 @@ const sendMessageToExplorerFrame = (data: unknown): void => { explorerFrame.contentWindow.postMessage( { signature: "pcIframe", - data: data + data: data, }, explorerFrame.contentDocument.referrer || window.location.href ); @@ -126,12 +126,12 @@ const initTestExplorer = async (): Promise => { sharedThroughputDefault: 400, defaultCollectionThroughput: { storage: "100", - throughput: { fixed: 400, unlimited: 400, unlimitedmax: 100000, unlimitedmin: 400, shared: 400 } + throughput: { fixed: 400, unlimited: 400, unlimitedmax: 100000, unlimitedmin: 400, shared: 400 }, }, // add UI test only when feature is not dependent on flights anymore flights: [], - selfServeType: selfServeType - } as ViewModels.DataExplorerInputsFrame + selfServeType: selfServeType, + } as ViewModels.DataExplorerInputsFrame, }; window.postMessage(initTestExplorerContent, window.location.href); diff --git a/test/testExplorer/TestExplorerParams.ts b/test/testExplorer/TestExplorerParams.ts index c5436eed8..bcf65b458 100644 --- a/test/testExplorer/TestExplorerParams.ts +++ b/test/testExplorer/TestExplorerParams.ts @@ -6,5 +6,5 @@ export enum TestExplorerParams { portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey", portalRunnerSubscripton = "portalRunnerSubscripton", portalRunnerResourceGroup = "portalRunnerResourceGroup", - selfServeType = "selfServeType" + selfServeType = "selfServeType", } diff --git a/webpack.config.js b/webpack.config.js index 7769dc9a2..a2e1c771b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,29 +16,29 @@ const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8"); const cssRule = { test: /\.css$/, - use: [MiniCssExtractPlugin.loader, "css-loader"] + use: [MiniCssExtractPlugin.loader, "css-loader"], }; const lessRule = { test: /\.less$/, use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"], - exclude: [path.resolve(__dirname, "less/Common/Constants.less")] + exclude: [path.resolve(__dirname, "less/Common/Constants.less")], }; const imagesRule = { test: /\.(jpg|jpeg|png|gif|svg|pdf|ico)$/, loader: "file-loader", options: { - name: "images/[name].[ext]" - } + name: "images/[name].[ext]", + }, }; const fontRule = { test: /\.(woff|woff2|ttf|eot)$/, loader: "file-loader", options: { - name: "[name].[ext]" - } + name: "[name].[ext]", + }, }; const htmlRule = { @@ -50,10 +50,10 @@ const htmlRule = { minify: false, removeComments: false, collapseWhitespace: false, - root: path.resolve(__dirname, "images") - } - } - ] + root: path.resolve(__dirname, "images"), + }, + }, + ], }; // We compile our own code with ts-loader @@ -63,11 +63,11 @@ const typescriptRule = { { loader: "ts-loader", options: { - transpileOnly: true - } - } + transpileOnly: true, + }, + }, ], - exclude: /node_modules/ + exclude: /node_modules/, }; // Third party modules are compiled with babel since using ts-loader that much causes webpack to run out of memory @@ -78,21 +78,21 @@ const ModulesRule = { loader: "babel-loader", options: { cacheDirectory: ".cache/babel", - presets: [["@babel/preset-env", { targets: { ie: "11" }, useBuiltIns: false }]] - } - } + presets: [["@babel/preset-env", { targets: { ie: "11" }, useBuiltIns: false }]], + }, + }, ], include: /node_modules/, // Exclude large modules we know don't need transpiling - exclude: /vega|monaco|plotly/ + exclude: /vega|monaco|plotly/, }; -module.exports = function(env = {}, argv = {}) { +module.exports = function (env = {}, argv = {}) { const mode = argv.mode || "development"; const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule]; const envVars = { GIT_SHA: gitSha, - PORT: process.env.PORT || "1234" + PORT: process.env.PORT || "1234", }; if (mode === "production") { @@ -110,67 +110,67 @@ module.exports = function(env = {}, argv = {}) { new CreateFileWebpack({ path: "./dist", fileName: "version.txt", - content: `${gitSha.trim()} ${new Date().toUTCString()}` + content: `${gitSha.trim()} ${new Date().toUTCString()}`, }), new CaseSensitivePathsPlugin(), new MiniCssExtractPlugin({ - filename: "[name].[contenthash].css" + filename: "[name].[contenthash].css", }), new HtmlWebpackPlugin({ filename: "explorer.html", template: "src/explorer.html", - chunks: ["main"] + chunks: ["main"], }), new HtmlWebpackPlugin({ filename: "terminal.html", template: "src/Terminal/index.html", - chunks: ["terminal"] + chunks: ["terminal"], }), new HtmlWebpackPlugin({ filename: "quickstart.html", template: "src/quickstart.html", - chunks: ["quickstart"] + chunks: ["quickstart"], }), new HtmlWebpackPlugin({ filename: "index.html", template: "src/index.html", - chunks: ["index"] + chunks: ["index"], }), new HtmlWebpackPlugin({ filename: "hostedExplorer.html", template: "src/hostedExplorer.html", - chunks: ["hostedExplorer"] + chunks: ["hostedExplorer"], }), new HtmlWebpackPlugin({ filename: "testExplorer.html", template: "test/testExplorer/testExplorer.html", - chunks: ["testExplorer"] + chunks: ["testExplorer"], }), new HtmlWebpackPlugin({ filename: "Heatmap.html", template: "src/Controls/Heatmap/Heatmap.html", - chunks: ["heatmap"] + chunks: ["heatmap"], }), new HtmlWebpackPlugin({ filename: "notebookViewer.html", template: "src/NotebookViewer/notebookViewer.html", - chunks: ["notebookViewer"] + chunks: ["notebookViewer"], }), new HtmlWebpackPlugin({ filename: "gallery.html", template: "src/GalleryViewer/galleryViewer.html", - chunks: ["galleryViewer"] + chunks: ["galleryViewer"], }), new HtmlWebpackPlugin({ filename: "connectToGitHub.html", template: "src/connectToGitHub.html", - chunks: ["connectToGitHub"] + chunks: ["connectToGitHub"], }), new MonacoWebpackPlugin(), new CopyWebpackPlugin({ - patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }] + patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }], }), - new EnvironmentPlugin(envVars) + new EnvironmentPlugin(envVars), ]; if (argv.analyze) { @@ -189,25 +189,25 @@ module.exports = function(env = {}, argv = {}) { terminal: "./src/Terminal/index.ts", notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx", galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx", - connectToGitHub: "./src/GitHub/GitHubConnector.ts" + connectToGitHub: "./src/GitHub/GitHubConnector.ts", }, node: { util: true, tls: "empty", - net: "empty" + net: "empty", }, output: { chunkFilename: "[name].[chunkhash:6].js", filename: "[name].[chunkhash:6].js", - path: path.resolve(__dirname, "dist") + path: path.resolve(__dirname, "dist"), }, devtool: mode === "development" ? "cheap-eval-source-map" : "source-map", plugins, module: { - rules + rules, }, resolve: { - extensions: [".tsx", ".ts", ".js"] + extensions: [".tsx", ".ts", ".js"], }, optimization: { minimize: mode === "production" ? true : false, @@ -217,10 +217,10 @@ module.exports = function(env = {}, argv = {}) { terserOptions: { // These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory compress: false, - mangle: true - } - }) - ] + mangle: true, + }, + }), + ], }, watch: isCI || mode === "production" ? false : true, // Hack since it is hard to disable watch entirely with webpack dev server https://github.com/webpack/webpack-dev-server/issues/1251#issuecomment-654240734 @@ -238,19 +238,19 @@ module.exports = function(env = {}, argv = {}) { "Access-Control-Allow-Credentials": "true", "Access-Control-Max-Age": "3600", "Access-Control-Allow-Headers": "*", - "Access-Control-Allow-Methods": "*" + "Access-Control-Allow-Methods": "*", }, proxy: { "/api": { target: "https://main.documentdb.ext.azure.com", changeOrigin: true, logLevel: "debug", - bypass: function(req, res, proxyOptions) { + bypass: function (req, res, proxyOptions) { if (req.method === "OPTIONS") { res.statusCode = 200; res.send(); } - } + }, }, "/proxy": { target: "https://main.documentdb.ext.azure.com", @@ -258,29 +258,29 @@ module.exports = function(env = {}, argv = {}) { secure: false, logLevel: "debug", pathRewrite: { "^/proxy": "" }, - router: req => { + router: (req) => { let newTarget = req.headers["x-ms-proxy-target"]; return newTarget; - } + }, }, "/_explorer": { target: process.env.EMULATOR_ENDPOINT || "https://localhost:8081/", changeOrigin: true, secure: false, - logLevel: "debug" + logLevel: "debug", }, "/explorerProxy": { target: process.env.EMULATOR_ENDPOINT || "https://localhost:8081/", pathRewrite: { "^/explorerProxy": "" }, changeOrigin: true, secure: false, - logLevel: "debug" - } - } + logLevel: "debug", + }, + }, }, stats: "minimal", node: { - fs: "empty" - } + fs: "empty", + }, }; };