diff --git a/.env.example b/.env.example index 62538cbc0..ea79c9a84 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1 @@ -PORTAL_RUNNER_USERNAME= -PORTAL_RUNNER_PASSWORD= -PORTAL_RUNNER_SUBSCRIPTION= -PORTAL_RUNNER_RESOURCE_GROUP= -PORTAL_RUNNER_DATABASE_ACCOUNT= -PORTAL_RUNNER_DATABASE_ACCOUNT_KEY= -PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT= -PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY= -PORTAL_RUNNER_CONNECTION_STRING= -NOTEBOOKS_TEST_RUNNER_TENANT_ID= -NOTEBOOKS_TEST_RUNNER_CLIENT_ID= -NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET= -CASSANDRA_CONNECTION_STRING= -MONGO_CONNECTION_STRING= -TABLES_CONNECTION_STRING= DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 967345584..7a5d06bbf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -44,7 +44,6 @@ src/Definitions/png.d.ts src/Definitions/svg.d.ts src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.ts -src/Explorer/ContextMenuButtonFactory.ts src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts src/Explorer/Controls/DynamicList/DynamicList.test.ts @@ -72,7 +71,6 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts src/Explorer/DataSamples/ContainerSampleGenerator.ts src/Explorer/DataSamples/DataSamplesUtil.test.ts src/Explorer/DataSamples/DataSamplesUtil.ts -src/Explorer/Explorer.tsx src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts @@ -84,11 +82,6 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts -# src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts -# src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts - -src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts -src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts src/Explorer/Menus/ContextMenu.ts src/Explorer/MostRecentActivity/MostRecentActivity.ts src/Explorer/Notebook/NotebookClientV2.ts @@ -105,27 +98,11 @@ src/Explorer/Notebook/NotebookContainerClient.ts src/Explorer/Notebook/NotebookContentClient.ts src/Explorer/Notebook/NotebookContentItem.ts src/Explorer/Notebook/NotebookUtil.ts -src/Explorer/OpenActions.test.ts -src/Explorer/OpenActions.ts src/Explorer/OpenActionsStubs.ts -src/Explorer/Panes/AddDatabasePane.ts -src/Explorer/Panes/AddDatabasePane.test.ts -src/Explorer/Panes/BrowseQueriesPane.ts -src/Explorer/Panes/CassandraAddCollectionPane.ts -src/Explorer/Panes/ContextualPaneBase.ts -src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts -src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts -# src/Explorer/Panes/GraphStylingPane.ts -# src/Explorer/Panes/NewVertexPane.ts -src/Explorer/Panes/PaneComponents.ts -src/Explorer/Panes/RenewAdHocAccessPane.ts -src/Explorer/Panes/SetupNotebooksPane.ts -src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts src/Explorer/SplashScreen/SplashScreen.test.ts -src/Explorer/Tables/Constants.ts src/Explorer/Tables/DataTable/CacheBase.ts src/Explorer/Tables/DataTable/DataTableBindingManager.ts src/Explorer/Tables/DataTable/DataTableBuilder.ts @@ -142,7 +119,6 @@ src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts -src/Explorer/Tables/QueryBuilder/QueryViewModel.ts src/Explorer/Tables/TableDataClient.ts src/Explorer/Tables/TableEntityProcessor.ts src/Explorer/Tables/Utilities.ts @@ -152,170 +128,67 @@ src/Explorer/Tabs/DocumentsTab.test.ts src/Explorer/Tabs/DocumentsTab.ts src/Explorer/Tabs/GraphTab.ts src/Explorer/Tabs/MongoDocumentsTab.ts -src/Explorer/Tabs/MongoQueryTab.ts -src/Explorer/Tabs/MongoShellTab.ts src/Explorer/Tabs/NotebookV2Tab.ts -src/Explorer/Tabs/QueryTab.test.ts -src/Explorer/Tabs/QueryTab.ts -src/Explorer/Tabs/QueryTablesTab.ts src/Explorer/Tabs/ScriptTabBase.ts -src/Explorer/Tabs/StoredProcedureTab.ts src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabsBase.ts src/Explorer/Tabs/TriggerTab.ts src/Explorer/Tabs/UserDefinedFunctionTab.ts src/Explorer/Tree/AccessibleVerticalList.ts -src/Explorer/Tree/Collection.test.ts src/Explorer/Tree/Collection.ts src/Explorer/Tree/ConflictId.ts -src/Explorer/Tree/Database.ts src/Explorer/Tree/DocumentId.ts src/Explorer/Tree/ObjectId.ts src/Explorer/Tree/ResourceTokenCollection.ts src/Explorer/Tree/StoredProcedure.ts src/Explorer/Tree/TreeComponents.ts src/Explorer/Tree/Trigger.ts -src/Explorer/Tree/UserDefinedFunction.ts src/Explorer/WaitsForTemplateViewModel.ts src/GitHub/GitHubClient.test.ts src/GitHub/GitHubClient.ts src/GitHub/GitHubConnector.ts -src/GitHub/GitHubContentProvider.test.ts -src/GitHub/GitHubContentProvider.ts src/GitHub/GitHubOAuthService.ts -src/HostedExplorer.ts src/Index.ts src/Juno/JunoClient.test.ts src/Juno/JunoClient.ts -src/Main.ts -src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts -src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts -src/Platform/Emulator/DataAccessUtility.ts -src/Platform/Emulator/ExplorerFactory.ts -src/Platform/Emulator/Main.ts -src/Platform/Emulator/NotificationsClient.ts -src/Platform/Hosted/ArmResourceUtils.ts src/Platform/Hosted/Authorization.ts -src/Platform/Hosted/DataAccessUtility.ts -src/Platform/Hosted/ExplorerFactory.ts -src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts -src/Platform/Hosted/Main.ts -src/Platform/Hosted/Maint.test.ts -src/Platform/Hosted/NotificationsClient.ts -src/Platform/Portal/DataAccessUtility.ts -src/Platform/Portal/ExplorerFactory.ts -src/Platform/Portal/Main.ts -src/Platform/Portal/NotificationsClient.ts -src/PlatformType.ts src/ReactDevTools.ts -src/ResourceProvider/IResourceProviderClient.test.ts -src/ResourceProvider/IResourceProviderClient.ts -src/ResourceProvider/ResourceProviderClient.ts -src/ResourceProvider/ResourceProviderClientFactory.ts -src/RouteHandlers/RouteHandler.ts -src/RouteHandlers/TabRouteHandler.test.ts -src/RouteHandlers/TabRouteHandler.ts src/Shared/Constants.ts src/Shared/DefaultExperienceUtility.test.ts src/Shared/DefaultExperienceUtility.ts -src/Shared/ExplorerSettings.ts -src/Shared/PriceEstimateCalculator.ts -src/Shared/StorageUtility.test.ts -src/Shared/StorageUtility.ts src/Shared/appInsights.ts src/SparkClusterManager/ArcadiaResourceManager.ts src/SparkClusterManager/SparkClusterManager.ts src/Terminal/JupyterLabAppFactory.ts src/Terminal/NotebookAppContracts.d.ts -src/Terminal/index.ts -src/TokenProviders/PortalTokenProvider.ts -src/TokenProviders/TokenProviderFactory.ts -src/Utils/PricingUtils.test.ts -src/Utils/QueryUtils.test.ts src/applyExplorerBindings.ts src/global.d.ts src/setupTests.ts -src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx -src/Explorer/Controls/Accordion/AccordionComponent.tsx -src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx -src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx -src/Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter.tsx -src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx -src/Explorer/Controls/CollapsiblePanel/CollapsiblePanel.tsx -src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx -src/Explorer/Controls/DialogReactComponent/DialogComponent.tsx -src/Explorer/Controls/DialogReactComponent/DialogComponentAdapter.tsx -src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx -src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx -src/Explorer/Controls/Directory/DirectoryComponentAdapter.tsx -src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx -src/Explorer/Controls/Directory/DirectoryListComponent.tsx -src/Explorer/Controls/Editor/EditorReact.tsx src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx -src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx -src/NotebookViewer/NotebookViewer.tsx src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx -src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx -src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx -src/Explorer/Controls/ResizeSensorReactComponent/ResizeSensorComponent.tsx -src/Explorer/Controls/Spark/ClusterSettingsComponent.tsx -src/Explorer/Controls/Spark/ClusterSettingsComponentAdapter.tsx -src/Explorer/Controls/Tabs/TabComponent.tsx -src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx src/Explorer/Controls/TreeComponent/TreeComponent.tsx -src/Explorer/Graph/GraphExplorerComponent/EditorNeighborsComponent.tsx -src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx -src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx -src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx -src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx -src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx -src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx -src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx -src/Explorer/Notebook/NotebookComponent/contents/file/index.tsx -src/Explorer/Notebook/NotebookComponent/contents/file/text-file.tsx src/Explorer/Notebook/NotebookComponent/contents/index.tsx -src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx -src/Explorer/Notebook/NotebookRenderer/Prompt.tsx -src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx -src/Explorer/Notebook/NotebookRenderer/StatusBar.test.tsx -src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx -src/Explorer/Notebook/NotebookRenderer/Toolbar.tsx -src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx -src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx -src/Explorer/Notebook/NotebookRenderer/decorators/HoverableCell.tsx src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx -src/Explorer/Notebook/temp/inputs/editor.tsx -src/Explorer/Notebook/temp/markdown-cell.tsx -src/Explorer/Notebook/temp/source.tsx -src/Explorer/Notebook/temp/syntax-highlighter/index.tsx -src/Explorer/SplashScreen/SplashScreen.tsx -src/Explorer/Tabs/GalleryTab.tsx -src/Explorer/Tabs/NotebookViewerTab.tsx -src/Explorer/Tabs/TerminalTab.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx -src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx -src/GalleryViewer/Cards/GalleryCardComponent.tsx -src/GalleryViewer/GalleryViewer.tsx -src/GalleryViewer/GalleryViewerComponent.tsx __mocks__/monaco-editor.ts -src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx \ No newline at end of file +src/Explorer/Tree/ResourceTree.tsx \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b96b256b..8e62e4423 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,11 +92,11 @@ jobs: name: dist path: dist/ - name: Upload build to preview blob storage - run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --destination-path "${{github.event.pull_request.head.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" + run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" env: PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} - name: Upload preview config to blob storage - run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --name "${{github.event.pull_request.head.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" + run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" env: PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} endtoendemulator: diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index d00fd9724..9468565b3 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: schedule: # Once every hour - - cron: "0 * * * *" + - cron: "0 15 * * *" # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff --git a/configs/mpac.json b/configs/mpac.json index dd9a572a1..7c270c6d5 100644 --- a/configs/mpac.json +++ b/configs/mpac.json @@ -1,3 +1,3 @@ { - "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com" -} + "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com" +} \ No newline at end of file diff --git a/docs/assets/css/main.css b/docs/assets/css/main.css new file mode 100644 index 000000000..46571c27c --- /dev/null +++ b/docs/assets/css/main.css @@ -0,0 +1,2660 @@ +:root { + --color-background: #fdfdfd; + --color-text: #222; + --color-text-aside: #707070; + --color-link: #4da6ff; + --color-menu-divider: #eee; + --color-menu-divider-focus: #000; + --color-menu-label: #707070; + --color-panel: #fff; + --color-panel-divider: #eee; + --color-comment-tag: #707070; + --color-comment-tag-text: #fff; + --color-code-background: rgba(0, 0, 0, 0.04); + --color-ts: #9600ff; + --color-ts-interface: #647f1b; + --color-ts-enum: #937210; + --color-ts-class: #0672de; + --color-ts-private: #707070; + --color-toolbar: #fff; + --color-toolbar-text: #333; +} + +/*! normalize.css v1.1.3 | MIT License | git.io/normalize */ +/* ========================================================================== + * * HTML5 display definitions + * * ========================================================================== */ +/** + * * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { + display: block; +} + +/** + * * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. */ +audio, canvas, video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/** + * * Prevent modern browsers from displaying `audio` without controls. + * * Remove excess height in iOS 5 devices. */ +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. + * * Known issue: no IE 6 support. */ +[hidden] { + display: none; +} + +/* ========================================================================== + * * Base + * * ========================================================================== */ +/** + * * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using + * * `em` units. + * * 2. Prevent iOS text size adjust after orientation change, without disabling + * * user zoom. */ +html { + font-size: 100%; + /* 1 */ + -ms-text-size-adjust: 100%; + /* 2 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + font-family: sans-serif; +} + +/** + * * Address `font-family` inconsistency between `textarea` and other form + * * elements. */ +button, input, select, textarea { + font-family: sans-serif; +} + +/** + * * Address margins handled incorrectly in IE 6/7. */ +body { + margin: 0; +} + +/* ========================================================================== + * * Links + * * ========================================================================== */ +/** + * * Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { + outline: thin dotted; +} +a:active, a:hover { + outline: 0; +} + +/** + * * Improve readability when focused and also mouse hovered in all browsers. */ +/* ========================================================================== + * * Typography + * * ========================================================================== */ +/** + * * Address font sizes and margins set differently in IE 6/7. + * * Address font sizes within `section` and `article` in Firefox 4+, Safari 5, + * * and Chrome. */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4, .tsd-index-panel h3 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.67em; + margin: 2.33em 0; +} + +/** + * * Address styling not present in IE 7/8/9, Safari 5, and Chrome. */ +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. */ +b, strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/** + * * Address styling not present in Safari 5 and Chrome. */ +dfn { + font-style: italic; +} + +/** + * * Address differences between Firefox and other browsers. + * * Known issue: no IE 6/7 normalization. */ +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * * Address styling not present in IE 6/7/8/9. */ +mark { + background: #ff0; + color: #000; +} + +/** + * * Address margins set differently in IE 6/7. */ +p, pre { + margin: 1em 0; +} + +/** + * * Correct font family set oddly in IE 6, Safari 4/5, and Chrome. */ +code, kbd, pre, samp { + font-family: monospace, serif; + _font-family: "courier new", monospace; + font-size: 1em; +} + +/** + * * Improve readability of pre-formatted text in all browsers. */ +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/** + * * Address CSS quotes not supported in IE 6/7. */ +q { + quotes: none; +} +q:before, q:after { + content: ""; + content: none; +} + +/** + * * Address `quotes` property not supported in Safari 4. */ +/** + * * Address inconsistent and variable font size in all browsers. */ +small { + font-size: 80%; +} + +/** + * * Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ========================================================================== + * * Lists + * * ========================================================================== */ +/** + * * Address margins set differently in IE 6/7. */ +dl, menu, ol, ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/** + * * Address paddings set differently in IE 6/7. */ +menu, ol, ul { + padding: 0 0 0 40px; +} + +/** + * * Correct list images handled incorrectly in IE 7. */ +nav ul, nav ol { + list-style: none; + list-style-image: none; +} + +/* ========================================================================== + * * Embedded content + * * ========================================================================== */ +/** + * * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. + * * 2. Improve image quality when scaled in IE 7. */ +img { + border: 0; + /* 1 */ + -ms-interpolation-mode: bicubic; +} + +/* 2 */ +/** + * * Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { + overflow: hidden; +} + +/* ========================================================================== + * * Figures + * * ========================================================================== */ +/** + * * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. */ +figure, form { + margin: 0; +} + +/* ========================================================================== + * * Forms + * * ========================================================================== */ +/** + * * Correct margin displayed oddly in IE 6/7. */ +/** + * * Define consistent border, margin, and padding. */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * * 1. Correct color not being inherited in IE 6/7/8/9. + * * 2. Correct text not wrapping in Firefox 3. + * * 3. Correct alignment displayed oddly in IE 6/7. */ +legend { + border: 0; + /* 1 */ + padding: 0; + white-space: normal; + /* 2 */ + *margin-left: -7px; +} + +/* 3 */ +/** + * * 1. Correct font size not being inherited in all browsers. + * * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, + * * and Chrome. + * * 3. Improve appearance and consistency in all browsers. */ +button, input, select, textarea { + font-size: 100%; + /* 1 */ + margin: 0; + /* 2 */ + vertical-align: baseline; + /* 3 */ + *vertical-align: middle; +} + +/* 3 */ +/** + * * Address Firefox 3+ setting `line-height` on `input` using `!important` in + * * the UA stylesheet. */ +button, input { + line-height: normal; +} + +/** + * * Address inconsistent `text-transform` inheritance for `button` and `select`. + * * All other form control elements do not inherit `text-transform` values. + * * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. + * * Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { + text-transform: none; +} + +/** + * * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * * and `video` controls. + * * 2. Correct inability to style clickable `input` types in iOS. + * * 3. Improve usability and consistency of cursor style between image-type + * * `input` and others. + * * 4. Remove inner spacing in IE 7 without affecting normal text inputs. + * * Known issue: inner spacing remains in IE 6. */ +button, html input[type=button] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ + *overflow: visible; +} + +/* 4 */ +input[type=reset], input[type=submit] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ + *overflow: visible; +} + +/* 4 */ +/** + * * Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { + cursor: default; +} + +/** + * * 1. Address box sizing set to content-box in IE 8/9. + * * 2. Remove excess padding in IE 8/9. + * * 3. Remove excess padding in IE 7. + * * Known issue: excess padding remains in IE 6. */ +input { + /* 3 */ +} +input[type=checkbox], input[type=radio] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ + *height: 13px; + /* 3 */ + *width: 13px; +} +input[type=search] { + -webkit-appearance: textfield; + /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + /* 2 */ + box-sizing: content-box; +} +input[type=search]::-webkit-search-cancel-button, input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. + * * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome + * * (include `-moz` to future-proof). */ +/** + * * Remove inner padding and search cancel button in Safari 5 and Chrome + * * on OS X. */ +/** + * * Remove inner padding and border in Firefox 3+. */ +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * * 1. Remove default vertical scrollbar in IE 6/7/8/9. + * * 2. Improve readability and alignment in all browsers. */ +textarea { + overflow: auto; + /* 1 */ + vertical-align: top; +} + +/* 2 */ +/* ========================================================================== + * * Tables + * * ========================================================================== */ +/** + * * Remove most spacing between table cells. */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +ul.tsd-descriptions > li > :first-child, .tsd-panel > :first-child, .col > :first-child, .col-11 > :first-child, .col-10 > :first-child, .col-9 > :first-child, .col-8 > :first-child, .col-7 > :first-child, .col-6 > :first-child, .col-5 > :first-child, .col-4 > :first-child, .col-3 > :first-child, .col-2 > :first-child, .col-1 > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child, +.tsd-panel > :first-child > :first-child, +.col > :first-child > :first-child, +.col-11 > :first-child > :first-child, +.col-10 > :first-child > :first-child, +.col-9 > :first-child > :first-child, +.col-8 > :first-child > :first-child, +.col-7 > :first-child > :first-child, +.col-6 > :first-child > :first-child, +.col-5 > :first-child > :first-child, +.col-4 > :first-child > :first-child, +.col-3 > :first-child > :first-child, +.col-2 > :first-child > :first-child, +.col-1 > :first-child > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child > :first-child, +.tsd-panel > :first-child > :first-child > :first-child, +.col > :first-child > :first-child > :first-child, +.col-11 > :first-child > :first-child > :first-child, +.col-10 > :first-child > :first-child > :first-child, +.col-9 > :first-child > :first-child > :first-child, +.col-8 > :first-child > :first-child > :first-child, +.col-7 > :first-child > :first-child > :first-child, +.col-6 > :first-child > :first-child > :first-child, +.col-5 > :first-child > :first-child > :first-child, +.col-4 > :first-child > :first-child > :first-child, +.col-3 > :first-child > :first-child > :first-child, +.col-2 > :first-child > :first-child > :first-child, +.col-1 > :first-child > :first-child > :first-child { + margin-top: 0; +} +ul.tsd-descriptions > li > :last-child, .tsd-panel > :last-child, .col > :last-child, .col-11 > :last-child, .col-10 > :last-child, .col-9 > :last-child, .col-8 > :last-child, .col-7 > :last-child, .col-6 > :last-child, .col-5 > :last-child, .col-4 > :last-child, .col-3 > :last-child, .col-2 > :last-child, .col-1 > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child, +.tsd-panel > :last-child > :last-child, +.col > :last-child > :last-child, +.col-11 > :last-child > :last-child, +.col-10 > :last-child > :last-child, +.col-9 > :last-child > :last-child, +.col-8 > :last-child > :last-child, +.col-7 > :last-child > :last-child, +.col-6 > :last-child > :last-child, +.col-5 > :last-child > :last-child, +.col-4 > :last-child > :last-child, +.col-3 > :last-child > :last-child, +.col-2 > :last-child > :last-child, +.col-1 > :last-child > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child > :last-child, +.tsd-panel > :last-child > :last-child > :last-child, +.col > :last-child > :last-child > :last-child, +.col-11 > :last-child > :last-child > :last-child, +.col-10 > :last-child > :last-child > :last-child, +.col-9 > :last-child > :last-child > :last-child, +.col-8 > :last-child > :last-child > :last-child, +.col-7 > :last-child > :last-child > :last-child, +.col-6 > :last-child > :last-child > :last-child, +.col-5 > :last-child > :last-child > :last-child, +.col-4 > :last-child > :last-child > :last-child, +.col-3 > :last-child > :last-child > :last-child, +.col-2 > :last-child > :last-child > :last-child, +.col-1 > :last-child > :last-child > :last-child { + margin-bottom: 0; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 40px; +} +@media (max-width: 640px) { + .container { + padding: 0 20px; + } +} + +.container-main { + padding-bottom: 200px; +} + +.row { + display: flex; + position: relative; + margin: 0 -10px; +} +.row:after { + visibility: hidden; + display: block; + content: ""; + clear: both; + height: 0; +} + +.col, .col-11, .col-10, .col-9, .col-8, .col-7, .col-6, .col-5, .col-4, .col-3, .col-2, .col-1 { + box-sizing: border-box; + float: left; + padding: 0 10px; +} + +.col-1 { + width: 8.3333333333%; +} + +.offset-1 { + margin-left: 8.3333333333%; +} + +.col-2 { + width: 16.6666666667%; +} + +.offset-2 { + margin-left: 16.6666666667%; +} + +.col-3 { + width: 25%; +} + +.offset-3 { + margin-left: 25%; +} + +.col-4 { + width: 33.3333333333%; +} + +.offset-4 { + margin-left: 33.3333333333%; +} + +.col-5 { + width: 41.6666666667%; +} + +.offset-5 { + margin-left: 41.6666666667%; +} + +.col-6 { + width: 50%; +} + +.offset-6 { + margin-left: 50%; +} + +.col-7 { + width: 58.3333333333%; +} + +.offset-7 { + margin-left: 58.3333333333%; +} + +.col-8 { + width: 66.6666666667%; +} + +.offset-8 { + margin-left: 66.6666666667%; +} + +.col-9 { + width: 75%; +} + +.offset-9 { + margin-left: 75%; +} + +.col-10 { + width: 83.3333333333%; +} + +.offset-10 { + margin-left: 83.3333333333%; +} + +.col-11 { + width: 91.6666666667%; +} + +.offset-11 { + margin-left: 91.6666666667%; +} + +.tsd-kind-icon { + display: block; + position: relative; + padding-left: 20px; + text-indent: -20px; +} +.tsd-kind-icon:before { + content: ""; + display: inline-block; + vertical-align: middle; + width: 17px; + height: 17px; + margin: 0 3px 2px 0; + background-image: url(../images/icons.png); +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-kind-icon:before { + background-image: url(../images/icons@2x.png); + background-size: 238px 204px; + } +} + +.tsd-signature.tsd-kind-icon:before { + background-position: 0 -153px; +} + +.tsd-kind-object-literal > .tsd-kind-icon:before { + background-position: 0px -17px; +} +.tsd-kind-object-literal.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -17px; +} +.tsd-kind-object-literal.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -17px; +} + +.tsd-kind-class > .tsd-kind-icon:before { + background-position: 0px -34px; +} +.tsd-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -34px; +} +.tsd-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -34px; +} + +.tsd-kind-class.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -51px; +} + +.tsd-kind-interface > .tsd-kind-icon:before { + background-position: 0px -68px; +} +.tsd-kind-interface.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -68px; +} +.tsd-kind-interface.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -68px; +} + +.tsd-kind-interface.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -85px; +} + +.tsd-kind-namespace > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-namespace.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-namespace.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-module > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-module.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-module.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-enum > .tsd-kind-icon:before { + background-position: 0px -119px; +} +.tsd-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -119px; +} +.tsd-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -119px; +} + +.tsd-kind-enum-member > .tsd-kind-icon:before { + background-position: 0px -136px; +} +.tsd-kind-enum-member.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -136px; +} +.tsd-kind-enum-member.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -136px; +} + +.tsd-kind-signature > .tsd-kind-icon:before { + background-position: 0px -153px; +} +.tsd-kind-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -153px; +} +.tsd-kind-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -153px; +} + +.tsd-kind-type-alias > .tsd-kind-icon:before { + background-position: 0px -170px; +} +.tsd-kind-type-alias.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -170px; +} +.tsd-kind-type-alias.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -170px; +} + +.tsd-kind-type-alias.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -187px; +} + +.tsd-kind-variable > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-variable.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-variable.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-property > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-property.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-property.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-get-signature > .tsd-kind-icon:before { + background-position: -136px -17px; +} +.tsd-kind-get-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -17px; +} +.tsd-kind-get-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -17px; +} + +.tsd-kind-set-signature > .tsd-kind-icon:before { + background-position: -136px -34px; +} +.tsd-kind-set-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -34px; +} +.tsd-kind-set-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -34px; +} + +.tsd-kind-accessor > .tsd-kind-icon:before { + background-position: -136px -51px; +} +.tsd-kind-accessor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -51px; +} +.tsd-kind-accessor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -51px; +} + +.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-function.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-method.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-constructor > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-constructor-signature > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-index-signature > .tsd-kind-icon:before { + background-position: -136px -119px; +} +.tsd-kind-index-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -119px; +} +.tsd-kind-index-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -119px; +} + +.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -136px; +} +.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -136px; +} +.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -136px; +} + +.tsd-is-static > .tsd-kind-icon:before { + background-position: -136px -153px; +} +.tsd-is-static.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -153px; +} +.tsd-is-static.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -153px; +} +.tsd-is-static.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -153px; +} + +.tsd-is-static.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -187px; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } +} +@keyframes fade-in-delayed { + 0% { + opacity: 0; + } + 33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fade-out-delayed { + 0% { + opacity: 1; + visibility: visible; + } + 66% { + opacity: 0; + } + 100% { + opacity: 0; + } +} +@keyframes shift-to-left { + from { + transform: translate(0, 0); + } + to { + transform: translate(-25%, 0); + } +} +@keyframes unshift-to-left { + from { + transform: translate(-25%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } +} +body { + background: var(--color-background); + font-family: "Segoe UI", sans-serif; + font-size: 16px; + color: var(--color-text); +} + +a { + color: var(--color-link); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +code, pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 14px; + background-color: var(--color-code-background); +} + +pre { + padding: 10px; +} +pre code { + padding: 0; + font-size: 100%; + background-color: transparent; +} + +blockquote { + margin: 1em 0; + padding-left: 1em; + border-left: 4px solid gray; +} + +.tsd-typography { + line-height: 1.333em; +} +.tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-typography h4, .tsd-typography .tsd-index-panel h3, .tsd-index-panel .tsd-typography h3, .tsd-typography h5, .tsd-typography h6 { + font-size: 1em; + margin: 0; +} +.tsd-typography h5, .tsd-typography h6 { + font-weight: normal; +} +.tsd-typography p, .tsd-typography ul, .tsd-typography ol { + margin: 1em 0; +} + +@media (min-width: 901px) and (max-width: 1024px) { + html.default .col-content { + width: 72%; + } + html.default .col-menu { + width: 28%; + } + html.default .tsd-navigation { + padding-left: 10px; + } +} +@media (max-width: 900px) { + html.default .col-content { + float: none; + width: 100%; + } + html.default .col-menu { + position: fixed !important; + overflow: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + width: 100%; + padding: 20px 20px 0 0; + max-width: 450px; + visibility: hidden; + background-color: var(--color-panel); + transform: translate(100%, 0); + } + html.default .col-menu > *:last-child { + padding-bottom: 20px; + } + html.default .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + html.default.to-has-menu .overlay { + animation: fade-in 0.4s; + } + html.default.to-has-menu header, +html.default.to-has-menu footer, +html.default.to-has-menu .col-content { + animation: shift-to-left 0.4s; + } + html.default.to-has-menu .col-menu { + animation: pop-in-from-right 0.4s; + } + html.default.from-has-menu .overlay { + animation: fade-out 0.4s; + } + html.default.from-has-menu header, +html.default.from-has-menu footer, +html.default.from-has-menu .col-content { + animation: unshift-to-left 0.4s; + } + html.default.from-has-menu .col-menu { + animation: pop-out-to-right 0.4s; + } + html.default.has-menu body { + overflow: hidden; + } + html.default.has-menu .overlay { + visibility: visible; + } + html.default.has-menu header, +html.default.has-menu footer, +html.default.has-menu .col-content { + transform: translate(-25%, 0); + } + html.default.has-menu .col-menu { + visibility: visible; + transform: translate(0, 0); + } +} + +.tsd-page-title { + padding: 70px 0 20px 0; + margin: 0 0 40px 0; + background: var(--color-panel); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.35); +} +.tsd-page-title h1 { + margin: 0; +} + +.tsd-breadcrumb { + margin: 0; + padding: 0; + color: var(--color-text-aside); +} +.tsd-breadcrumb a { + color: var(--color-text-aside); + text-decoration: none; +} +.tsd-breadcrumb a:hover { + text-decoration: underline; +} +.tsd-breadcrumb li { + display: inline; +} +.tsd-breadcrumb li:after { + content: " / "; +} + +html.minimal .container { + margin: 0; +} +html.minimal .container-main { + padding-top: 50px; + padding-bottom: 0; +} +html.minimal .content-wrap { + padding-left: 300px; +} +html.minimal .tsd-navigation { + position: fixed !important; + overflow: auto; + -webkit-overflow-scrolling: touch; + box-sizing: border-box; + z-index: 1; + left: 0; + top: 40px; + bottom: 0; + width: 300px; + padding: 20px; + margin: 0; +} +html.minimal .tsd-member .tsd-member { + margin-left: 0; +} +html.minimal .tsd-page-toolbar { + position: fixed; + z-index: 2; +} +html.minimal #tsd-filter .tsd-filter-group { + right: 0; + transform: none; +} +html.minimal footer { + background-color: transparent; +} +html.minimal footer .container { + padding: 0; +} +html.minimal .tsd-generator { + padding: 0; +} +@media (max-width: 900px) { + html.minimal .tsd-navigation { + display: none; + } + html.minimal .content-wrap { + padding-left: 0; + } +} + +dl.tsd-comment-tags { + overflow: hidden; +} +dl.tsd-comment-tags dt { + float: left; + padding: 1px 5px; + margin: 0 10px 0 0; + border-radius: 4px; + border: 1px solid var(--color-comment-tag); + color: var(--color-comment-tag); + font-size: 0.8em; + font-weight: normal; +} +dl.tsd-comment-tags dd { + margin: 0 0 10px 0; +} +dl.tsd-comment-tags dd:before, dl.tsd-comment-tags dd:after { + display: table; + content: " "; +} +dl.tsd-comment-tags dd pre, dl.tsd-comment-tags dd:after { + clear: both; +} +dl.tsd-comment-tags p { + margin: 0; +} + +.tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; +} +.tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; +} + +.toggle-protected .tsd-is-private { + display: none; +} + +.toggle-public .tsd-is-private, +.toggle-public .tsd-is-protected, +.toggle-public .tsd-is-private-protected { + display: none; +} + +.toggle-inherited .tsd-is-inherited { + display: none; +} + +.toggle-externals .tsd-is-external { + display: none; +} + +#tsd-filter { + position: relative; + display: inline-block; + height: 40px; + vertical-align: bottom; +} +.no-filter #tsd-filter { + display: none; +} +#tsd-filter .tsd-filter-group { + display: inline-block; + height: 40px; + vertical-align: bottom; + white-space: nowrap; +} +#tsd-filter input { + display: none; +} +@media (max-width: 900px) { + #tsd-filter .tsd-filter-group { + display: block; + position: absolute; + top: 40px; + right: 20px; + height: auto; + background-color: var(--color-panel); + visibility: hidden; + transform: translate(50%, 0); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + } + .has-options #tsd-filter .tsd-filter-group { + visibility: visible; + } + .to-has-options #tsd-filter .tsd-filter-group { + animation: fade-in 0.2s; + } + .from-has-options #tsd-filter .tsd-filter-group { + animation: fade-out 0.2s; + } + #tsd-filter label, +#tsd-filter .tsd-select { + display: block; + padding-right: 20px; + } +} + +footer { + border-top: 1px solid var(--color-panel-divider); + background-color: var(--color-panel); +} +footer.with-border-bottom { + border-bottom: 1px solid var(--color-panel-divider); +} +footer .tsd-legend-group { + font-size: 0; +} +footer .tsd-legend { + display: inline-block; + width: 25%; + padding: 0; + font-size: 16px; + list-style: none; + line-height: 1.333em; + vertical-align: top; +} +@media (max-width: 900px) { + footer .tsd-legend { + width: 50%; + } +} + +.tsd-hierarchy { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-hierarchy .target { + font-weight: bold; +} + +.tsd-index-panel .tsd-index-content { + margin-bottom: -30px !important; +} +.tsd-index-panel .tsd-index-section { + margin-bottom: 30px !important; +} +.tsd-index-panel h3 { + margin: 0 -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 3; + -moz-column-count: 3; + -ms-column-count: 3; + -o-column-count: 3; + column-count: 3; + -webkit-column-gap: 20px; + -moz-column-gap: 20px; + -ms-column-gap: 20px; + -o-column-gap: 20px; + column-gap: 20px; + padding: 0; + list-style: none; + line-height: 1.333em; +} +@media (max-width: 900px) { + .tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 1; + -moz-column-count: 1; + -ms-column-count: 1; + -o-column-count: 1; + column-count: 1; + } +} +@media (min-width: 901px) and (max-width: 1024px) { + .tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 2; + -moz-column-count: 2; + -ms-column-count: 2; + -o-column-count: 2; + column-count: 2; + } +} +.tsd-index-panel ul.tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; +} +.tsd-index-panel a, +.tsd-index-panel .tsd-parent-kind-module a { + color: var(--color-ts); +} +.tsd-index-panel .tsd-parent-kind-interface a { + color: var(--color-ts-interface); +} +.tsd-index-panel .tsd-parent-kind-enum a { + color: var(--color-ts-enum); +} +.tsd-index-panel .tsd-parent-kind-class a { + color: var(--color-ts-class); +} +.tsd-index-panel .tsd-kind-module a { + color: var(--color-ts); +} +.tsd-index-panel .tsd-kind-interface a { + color: var(--color-ts-interface); +} +.tsd-index-panel .tsd-kind-enum a { + color: var(--color-ts-enum); +} +.tsd-index-panel .tsd-kind-class a { + color: var(--color-ts-class); +} +.tsd-index-panel .tsd-is-private a { + color: var(--color-ts-private); +} + +.tsd-flag { + display: inline-block; + padding: 1px 5px; + border-radius: 4px; + color: var(--color-comment-tag-text); + background-color: var(--color-comment-tag); + text-indent: 0; + font-size: 14px; + font-weight: normal; +} + +.tsd-anchor { + position: absolute; + top: -100px; +} + +.tsd-member { + position: relative; +} +.tsd-member .tsd-anchor + h3 { + margin-top: 0; + margin-bottom: 0; + border-bottom: none; +} +.tsd-member a[data-tsd-kind] { + color: var(--color-ts); +} +.tsd-member a[data-tsd-kind=Interface] { + color: var(--color-ts-interface); +} +.tsd-member a[data-tsd-kind=Enum] { + color: var(--color-ts-enum); +} +.tsd-member a[data-tsd-kind=Class] { + color: var(--color-ts-class); +} +.tsd-member a[data-tsd-kind=Private] { + color: var(--color-ts-private); +} + +.tsd-navigation { + margin: 0 0 0 40px; +} +.tsd-navigation a { + display: block; + padding-top: 2px; + padding-bottom: 2px; + border-left: 2px solid transparent; + color: var(--color-text); + text-decoration: none; + transition: border-left-color 0.1s; +} +.tsd-navigation a:hover { + text-decoration: underline; +} +.tsd-navigation ul { + margin: 0; + padding: 0; + list-style: none; +} +.tsd-navigation li { + padding: 0; +} + +.tsd-navigation.primary { + padding-bottom: 40px; +} +.tsd-navigation.primary a { + display: block; + padding-top: 6px; + padding-bottom: 6px; +} +.tsd-navigation.primary ul li a { + padding-left: 5px; +} +.tsd-navigation.primary ul li li a { + padding-left: 25px; +} +.tsd-navigation.primary ul li li li a { + padding-left: 45px; +} +.tsd-navigation.primary ul li li li li a { + padding-left: 65px; +} +.tsd-navigation.primary ul li li li li li a { + padding-left: 85px; +} +.tsd-navigation.primary ul li li li li li li a { + padding-left: 105px; +} +.tsd-navigation.primary > ul { + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-navigation.primary li { + border-top: 1px solid var(--color-panel-divider); +} +.tsd-navigation.primary li.current > a { + font-weight: bold; +} +.tsd-navigation.primary li.label span { + display: block; + padding: 20px 0 6px 5px; + color: var(--color-menu-label); +} +.tsd-navigation.primary li.globals + li > span, .tsd-navigation.primary li.globals + li > a { + padding-top: 20px; +} + +.tsd-navigation.secondary { + max-height: calc(100vh - 1rem - 40px); + overflow: auto; + position: -webkit-sticky; + position: sticky; + top: calc(.5rem + 40px); + transition: 0.3s; +} +.tsd-navigation.secondary.tsd-navigation--toolbar-hide { + max-height: calc(100vh - 1rem); + top: 0.5rem; +} +.tsd-navigation.secondary ul { + transition: opacity 0.2s; +} +.tsd-navigation.secondary ul li a { + padding-left: 25px; +} +.tsd-navigation.secondary ul li li a { + padding-left: 45px; +} +.tsd-navigation.secondary ul li li li a { + padding-left: 65px; +} +.tsd-navigation.secondary ul li li li li a { + padding-left: 85px; +} +.tsd-navigation.secondary ul li li li li li a { + padding-left: 105px; +} +.tsd-navigation.secondary ul li li li li li li a { + padding-left: 125px; +} +.tsd-navigation.secondary ul.current a { + border-left-color: var(--color-panel-divider); +} +.tsd-navigation.secondary li.focus > a, +.tsd-navigation.secondary ul.current li.focus > a { + border-left-color: var(--color-menu-divider-focus); +} +.tsd-navigation.secondary li.current { + margin-top: 20px; + margin-bottom: 20px; + border-left-color: var(--color-panel-divider); +} +.tsd-navigation.secondary li.current > a { + font-weight: bold; +} + +@media (min-width: 901px) { + .menu-sticky-wrap { + position: static; + } +} + +.tsd-panel { + margin: 20px 0; + padding: 20px; + background-color: var(--color-panel); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +.tsd-panel:empty { + display: none; +} +.tsd-panel > h1, .tsd-panel > h2, .tsd-panel > h3 { + margin: 1.5em -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-panel > h1.tsd-before-signature, .tsd-panel > h2.tsd-before-signature, .tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: 0; +} +.tsd-panel table { + display: block; + width: 100%; + overflow: auto; + margin-top: 10px; + word-break: normal; + word-break: keep-all; +} +.tsd-panel table th { + font-weight: bold; +} +.tsd-panel table th, .tsd-panel table td { + padding: 6px 13px; + border: 1px solid #ddd; +} +.tsd-panel table tr { + background-color: #fff; + border-top: 1px solid #ccc; +} +.tsd-panel table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +.tsd-panel-group { + margin: 60px 0; +} +.tsd-panel-group > h1, .tsd-panel-group > h2, .tsd-panel-group > h3 { + padding-left: 20px; + padding-right: 20px; +} + +#tsd-search { + transition: background-color 0.2s; +} +#tsd-search .title { + position: relative; + z-index: 2; +} +#tsd-search .field { + position: absolute; + left: 0; + top: 0; + right: 40px; + height: 40px; +} +#tsd-search .field input { + box-sizing: border-box; + position: relative; + top: -50px; + z-index: 1; + width: 100%; + padding: 0 10px; + opacity: 0; + outline: 0; + border: 0; + background: transparent; + color: var(--color-text); +} +#tsd-search .field label { + position: absolute; + overflow: hidden; + right: -40px; +} +#tsd-search .field input, +#tsd-search .title { + transition: opacity 0.2s; +} +#tsd-search .results { + position: absolute; + visibility: hidden; + top: 40px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +#tsd-search .results li { + padding: 0 10px; + background-color: var(--color-background); +} +#tsd-search .results li:nth-child(even) { + background-color: var(--color-panel); +} +#tsd-search .results li.state { + display: none; +} +#tsd-search .results li.current, +#tsd-search .results li:hover { + background-color: var(--color-panel-divider); +} +#tsd-search .results a { + display: block; +} +#tsd-search .results a:before { + top: 10px; +} +#tsd-search .results span.parent { + color: var(--color-text-aside); + font-weight: normal; +} +#tsd-search.has-focus { + background-color: var(--color-panel-divider); +} +#tsd-search.has-focus .field input { + top: 0; + opacity: 1; +} +#tsd-search.has-focus .title { + z-index: 0; + opacity: 0; +} +#tsd-search.has-focus .results { + visibility: visible; +} +#tsd-search.loading .results li.state.loading { + display: block; +} +#tsd-search.failure .results li.state.failure { + display: block; +} + +.tsd-signature { + margin: 0 0 1em 0; + padding: 10px; + border: 1px solid var(--color-panel-divider); + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; +} +.tsd-signature.tsd-kind-icon { + padding-left: 30px; +} +.tsd-signature.tsd-kind-icon:before { + top: 10px; + left: 10px; +} +.tsd-panel > .tsd-signature { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signature.tsd-kind-icon:before { + left: 20px; +} + +.tsd-signature-symbol { + color: var(--color-text-aside); + font-weight: normal; +} + +.tsd-signature-type { + font-style: italic; + font-weight: normal; +} + +.tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + border: 1px solid var(--color-panel-divider); +} +.tsd-signatures .tsd-signature { + margin: 0; + border-width: 1px 0 0 0; + transition: background-color 0.1s; +} +.tsd-signatures .tsd-signature:first-child { + border-top-width: 0; +} +.tsd-signatures .tsd-signature.current { + background-color: var(--color-panel-divider); +} +.tsd-signatures.active > .tsd-signature { + cursor: pointer; +} +.tsd-panel > .tsd-signatures { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon:before { + left: 20px; +} +.tsd-panel > a.anchor + .tsd-signatures { + border-top-width: 0; + margin-top: -20px; +} + +ul.tsd-descriptions { + position: relative; + overflow: hidden; + padding: 0; + list-style: none; +} +ul.tsd-descriptions.active > .tsd-description { + display: none; +} +ul.tsd-descriptions.active > .tsd-description.current { + display: block; +} +ul.tsd-descriptions.active > .tsd-description.fade-in { + animation: fade-in-delayed 0.3s; +} +ul.tsd-descriptions.active > .tsd-description.fade-out { + animation: fade-out-delayed 0.3s; + position: absolute; + display: block; + top: 0; + left: 0; + right: 0; + opacity: 0; + visibility: hidden; +} +ul.tsd-descriptions h4, ul.tsd-descriptions .tsd-index-panel h3, .tsd-index-panel ul.tsd-descriptions h3 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} + +ul.tsd-parameters, +ul.tsd-type-parameters { + list-style: square; + margin: 0; + padding-left: 20px; +} +ul.tsd-parameters > li.tsd-parameter-signature, +ul.tsd-type-parameters > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; +} +ul.tsd-parameters h5, +ul.tsd-type-parameters h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} +ul.tsd-parameters .tsd-comment, +ul.tsd-type-parameters .tsd-comment { + margin-top: -0.5em; +} + +.tsd-sources { + font-size: 14px; + color: var(--color-text-aside); + margin: 0 0 1em 0; +} +.tsd-sources a { + color: var(--color-text-aside); + text-decoration: underline; +} +.tsd-sources ul, .tsd-sources p { + margin: 0 !important; +} +.tsd-sources ul { + list-style: none; + padding: 0; +} + +.tsd-page-toolbar { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 40px; + color: var(--color-toolbar-text); + background: var(--color-toolbar); + border-bottom: 1px solid var(--color-panel-divider); + transition: transform 0.3s linear; +} +.tsd-page-toolbar a { + color: var(--color-toolbar-text); + text-decoration: none; +} +.tsd-page-toolbar a.title { + font-weight: bold; +} +.tsd-page-toolbar a.title:hover { + text-decoration: underline; +} +.tsd-page-toolbar .table-wrap { + display: table; + width: 100%; + height: 40px; +} +.tsd-page-toolbar .table-cell { + display: table-cell; + position: relative; + white-space: nowrap; + line-height: 40px; +} +.tsd-page-toolbar .table-cell:first-child { + width: 100%; +} + +.tsd-page-toolbar--hide { + transform: translateY(-100%); +} + +.tsd-select .tsd-select-list li:before, .tsd-select .tsd-select-label:before, .tsd-widget:before { + content: ""; + display: inline-block; + width: 40px; + height: 40px; + margin: 0 -8px 0 0; + background-image: url(../images/widgets.png); + background-repeat: no-repeat; + text-indent: -1024px; + vertical-align: bottom; +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-select .tsd-select-list li:before, .tsd-select .tsd-select-label:before, .tsd-widget:before { + background-image: url(../images/widgets@2x.png); + background-size: 320px 40px; + } +} + +.tsd-widget { + display: inline-block; + overflow: hidden; + opacity: 0.6; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-widget:hover { + opacity: 0.8; +} +.tsd-widget.active { + opacity: 1; + background-color: var(--color-panel-divider); +} +.tsd-widget.no-caption { + width: 40px; +} +.tsd-widget.no-caption:before { + margin: 0; +} +.tsd-widget.search:before { + background-position: 0 0; +} +.tsd-widget.menu:before { + background-position: -40px 0; +} +.tsd-widget.options:before { + background-position: -80px 0; +} +.tsd-widget.options, .tsd-widget.menu { + display: none; +} +@media (max-width: 900px) { + .tsd-widget.options, .tsd-widget.menu { + display: inline-block; + } +} +input[type=checkbox] + .tsd-widget:before { + background-position: -120px 0; +} +input[type=checkbox]:checked + .tsd-widget:before { + background-position: -160px 0; +} + +.tsd-select { + position: relative; + display: inline-block; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-select .tsd-select-label { + opacity: 0.6; + transition: opacity 0.2s; +} +.tsd-select .tsd-select-label:before { + background-position: -240px 0; +} +.tsd-select.active .tsd-select-label { + opacity: 0.8; +} +.tsd-select.active .tsd-select-list { + visibility: visible; + opacity: 1; + transition-delay: 0s; +} +.tsd-select .tsd-select-list { + position: absolute; + visibility: hidden; + top: 40px; + left: 0; + margin: 0; + padding: 0; + opacity: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + transition: visibility 0s 0.2s, opacity 0.2s; +} +.tsd-select .tsd-select-list li { + padding: 0 20px 0 0; + background-color: var(--color-background); +} +.tsd-select .tsd-select-list li:before { + background-position: 40px 0; +} +.tsd-select .tsd-select-list li:nth-child(even) { + background-color: var(--color-panel); +} +.tsd-select .tsd-select-list li:hover { + background-color: var(--color-panel-divider); +} +.tsd-select .tsd-select-list li.selected:before { + background-position: -200px 0; +} +@media (max-width: 900px) { + .tsd-select .tsd-select-list { + top: 0; + left: auto; + right: 100%; + margin-right: -5px; + } + .tsd-select .tsd-select-label:before { + background-position: -280px 0; + } +} + +img { + max-width: 100%; +} diff --git a/docs/assets/images/icons.png b/docs/assets/images/icons.png new file mode 100644 index 000000000..3836d5fe4 Binary files /dev/null and b/docs/assets/images/icons.png differ diff --git a/docs/assets/images/icons@2x.png b/docs/assets/images/icons@2x.png new file mode 100644 index 000000000..5a209e2f6 Binary files /dev/null and b/docs/assets/images/icons@2x.png differ diff --git a/docs/assets/images/widgets.png b/docs/assets/images/widgets.png new file mode 100644 index 000000000..c7380532a Binary files /dev/null and b/docs/assets/images/widgets.png differ diff --git a/docs/assets/images/widgets@2x.png b/docs/assets/images/widgets@2x.png new file mode 100644 index 000000000..4bbbd5727 Binary files /dev/null and b/docs/assets/images/widgets@2x.png differ diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js new file mode 100644 index 000000000..dc257a868 --- /dev/null +++ b/docs/assets/js/main.js @@ -0,0 +1,248 @@ +/* + * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). + * This devtool is not neither made for production nor for readable output files. + * It uses "eval()" calls to create a separate source file in the browser devtools. + * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) + * or disable the default devtool with "devtool: false". + * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). + */ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "../node_modules/lunr/lunr.js": +/*!************************************!*\ + !*** ../node_modules/lunr/lunr.js ***! + \************************************/ +/***/ ((module, exports, __webpack_require__) => { + +eval("var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;/**\n * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9\n * Copyright (C) 2020 Oliver Nightingale\n * @license MIT\n */\n\n;(function(){\n\n/**\n * A convenience function for configuring and constructing\n * a new lunr Index.\n *\n * A lunr.Builder instance is created and the pipeline setup\n * with a trimmer, stop word filter and stemmer.\n *\n * This builder object is yielded to the configuration function\n * that is passed as a parameter, allowing the list of fields\n * and other builder parameters to be customised.\n *\n * All documents _must_ be added within the passed config function.\n *\n * @example\n * var idx = lunr(function () {\n * this.field('title')\n * this.field('body')\n * this.ref('id')\n *\n * documents.forEach(function (doc) {\n * this.add(doc)\n * }, this)\n * })\n *\n * @see {@link lunr.Builder}\n * @see {@link lunr.Pipeline}\n * @see {@link lunr.trimmer}\n * @see {@link lunr.stopWordFilter}\n * @see {@link lunr.stemmer}\n * @namespace {function} lunr\n */\nvar lunr = function (config) {\n var builder = new lunr.Builder\n\n builder.pipeline.add(\n lunr.trimmer,\n lunr.stopWordFilter,\n lunr.stemmer\n )\n\n builder.searchPipeline.add(\n lunr.stemmer\n )\n\n config.call(builder, builder)\n return builder.build()\n}\n\nlunr.version = \"2.3.9\"\n/*!\n * lunr.utils\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A namespace containing utils for the rest of the lunr library\n * @namespace lunr.utils\n */\nlunr.utils = {}\n\n/**\n * Print a warning message to the console.\n *\n * @param {String} message The message to be printed.\n * @memberOf lunr.utils\n * @function\n */\nlunr.utils.warn = (function (global) {\n /* eslint-disable no-console */\n return function (message) {\n if (global.console && console.warn) {\n console.warn(message)\n }\n }\n /* eslint-enable no-console */\n})(this)\n\n/**\n * Convert an object to a string.\n *\n * In the case of `null` and `undefined` the function returns\n * the empty string, in all other cases the result of calling\n * `toString` on the passed object is returned.\n *\n * @param {Any} obj The object to convert to a string.\n * @return {String} string representation of the passed object.\n * @memberOf lunr.utils\n */\nlunr.utils.asString = function (obj) {\n if (obj === void 0 || obj === null) {\n return \"\"\n } else {\n return obj.toString()\n }\n}\n\n/**\n * Clones an object.\n *\n * Will create a copy of an existing object such that any mutations\n * on the copy cannot affect the original.\n *\n * Only shallow objects are supported, passing a nested object to this\n * function will cause a TypeError.\n *\n * Objects with primitives, and arrays of primitives are supported.\n *\n * @param {Object} obj The object to clone.\n * @return {Object} a clone of the passed object.\n * @throws {TypeError} when a nested object is passed.\n * @memberOf Utils\n */\nlunr.utils.clone = function (obj) {\n if (obj === null || obj === undefined) {\n return obj\n }\n\n var clone = Object.create(null),\n keys = Object.keys(obj)\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i],\n val = obj[key]\n\n if (Array.isArray(val)) {\n clone[key] = val.slice()\n continue\n }\n\n if (typeof val === 'string' ||\n typeof val === 'number' ||\n typeof val === 'boolean') {\n clone[key] = val\n continue\n }\n\n throw new TypeError(\"clone is not deep and does not support nested objects\")\n }\n\n return clone\n}\nlunr.FieldRef = function (docRef, fieldName, stringValue) {\n this.docRef = docRef\n this.fieldName = fieldName\n this._stringValue = stringValue\n}\n\nlunr.FieldRef.joiner = \"/\"\n\nlunr.FieldRef.fromString = function (s) {\n var n = s.indexOf(lunr.FieldRef.joiner)\n\n if (n === -1) {\n throw \"malformed field ref string\"\n }\n\n var fieldRef = s.slice(0, n),\n docRef = s.slice(n + 1)\n\n return new lunr.FieldRef (docRef, fieldRef, s)\n}\n\nlunr.FieldRef.prototype.toString = function () {\n if (this._stringValue == undefined) {\n this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef\n }\n\n return this._stringValue\n}\n/*!\n * lunr.Set\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A lunr set.\n *\n * @constructor\n */\nlunr.Set = function (elements) {\n this.elements = Object.create(null)\n\n if (elements) {\n this.length = elements.length\n\n for (var i = 0; i < this.length; i++) {\n this.elements[elements[i]] = true\n }\n } else {\n this.length = 0\n }\n}\n\n/**\n * A complete set that contains all elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.complete = {\n intersect: function (other) {\n return other\n },\n\n union: function () {\n return this\n },\n\n contains: function () {\n return true\n }\n}\n\n/**\n * An empty set that contains no elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.empty = {\n intersect: function () {\n return this\n },\n\n union: function (other) {\n return other\n },\n\n contains: function () {\n return false\n }\n}\n\n/**\n * Returns true if this set contains the specified object.\n *\n * @param {object} object - Object whose presence in this set is to be tested.\n * @returns {boolean} - True if this set contains the specified object.\n */\nlunr.Set.prototype.contains = function (object) {\n return !!this.elements[object]\n}\n\n/**\n * Returns a new set containing only the elements that are present in both\n * this set and the specified set.\n *\n * @param {lunr.Set} other - set to intersect with this set.\n * @returns {lunr.Set} a new set that is the intersection of this and the specified set.\n */\n\nlunr.Set.prototype.intersect = function (other) {\n var a, b, elements, intersection = []\n\n if (other === lunr.Set.complete) {\n return this\n }\n\n if (other === lunr.Set.empty) {\n return other\n }\n\n if (this.length < other.length) {\n a = this\n b = other\n } else {\n a = other\n b = this\n }\n\n elements = Object.keys(a.elements)\n\n for (var i = 0; i < elements.length; i++) {\n var element = elements[i]\n if (element in b.elements) {\n intersection.push(element)\n }\n }\n\n return new lunr.Set (intersection)\n}\n\n/**\n * Returns a new set combining the elements of this and the specified set.\n *\n * @param {lunr.Set} other - set to union with this set.\n * @return {lunr.Set} a new set that is the union of this and the specified set.\n */\n\nlunr.Set.prototype.union = function (other) {\n if (other === lunr.Set.complete) {\n return lunr.Set.complete\n }\n\n if (other === lunr.Set.empty) {\n return this\n }\n\n return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements)))\n}\n/**\n * A function to calculate the inverse document frequency for\n * a posting. This is shared between the builder and the index\n *\n * @private\n * @param {object} posting - The posting for a given term\n * @param {number} documentCount - The total number of documents.\n */\nlunr.idf = function (posting, documentCount) {\n var documentsWithTerm = 0\n\n for (var fieldName in posting) {\n if (fieldName == '_index') continue // Ignore the term index, its not a field\n documentsWithTerm += Object.keys(posting[fieldName]).length\n }\n\n var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5)\n\n return Math.log(1 + Math.abs(x))\n}\n\n/**\n * A token wraps a string representation of a token\n * as it is passed through the text processing pipeline.\n *\n * @constructor\n * @param {string} [str=''] - The string token being wrapped.\n * @param {object} [metadata={}] - Metadata associated with this token.\n */\nlunr.Token = function (str, metadata) {\n this.str = str || \"\"\n this.metadata = metadata || {}\n}\n\n/**\n * Returns the token string that is being wrapped by this object.\n *\n * @returns {string}\n */\nlunr.Token.prototype.toString = function () {\n return this.str\n}\n\n/**\n * A token update function is used when updating or optionally\n * when cloning a token.\n *\n * @callback lunr.Token~updateFunction\n * @param {string} str - The string representation of the token.\n * @param {Object} metadata - All metadata associated with this token.\n */\n\n/**\n * Applies the given function to the wrapped string token.\n *\n * @example\n * token.update(function (str, metadata) {\n * return str.toUpperCase()\n * })\n *\n * @param {lunr.Token~updateFunction} fn - A function to apply to the token string.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.update = function (fn) {\n this.str = fn(this.str, this.metadata)\n return this\n}\n\n/**\n * Creates a clone of this token. Optionally a function can be\n * applied to the cloned token.\n *\n * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.clone = function (fn) {\n fn = fn || function (s) { return s }\n return new lunr.Token (fn(this.str, this.metadata), this.metadata)\n}\n/*!\n * lunr.tokenizer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A function for splitting a string into tokens ready to be inserted into\n * the search index. Uses `lunr.tokenizer.separator` to split strings, change\n * the value of this property to change how strings are split into tokens.\n *\n * This tokenizer will convert its parameter to a string by calling `toString` and\n * then will split this string on the character in `lunr.tokenizer.separator`.\n * Arrays will have their elements converted to strings and wrapped in a lunr.Token.\n *\n * Optional metadata can be passed to the tokenizer, this metadata will be cloned and\n * added as metadata to every token that is created from the object to be tokenized.\n *\n * @static\n * @param {?(string|object|object[])} obj - The object to convert into tokens\n * @param {?object} metadata - Optional metadata to associate with every token\n * @returns {lunr.Token[]}\n * @see {@link lunr.Pipeline}\n */\nlunr.tokenizer = function (obj, metadata) {\n if (obj == null || obj == undefined) {\n return []\n }\n\n if (Array.isArray(obj)) {\n return obj.map(function (t) {\n return new lunr.Token(\n lunr.utils.asString(t).toLowerCase(),\n lunr.utils.clone(metadata)\n )\n })\n }\n\n var str = obj.toString().toLowerCase(),\n len = str.length,\n tokens = []\n\n for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {\n var char = str.charAt(sliceEnd),\n sliceLength = sliceEnd - sliceStart\n\n if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {\n\n if (sliceLength > 0) {\n var tokenMetadata = lunr.utils.clone(metadata) || {}\n tokenMetadata[\"position\"] = [sliceStart, sliceLength]\n tokenMetadata[\"index\"] = tokens.length\n\n tokens.push(\n new lunr.Token (\n str.slice(sliceStart, sliceEnd),\n tokenMetadata\n )\n )\n }\n\n sliceStart = sliceEnd + 1\n }\n\n }\n\n return tokens\n}\n\n/**\n * The separator used to split a string into tokens. Override this property to change the behaviour of\n * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens.\n *\n * @static\n * @see lunr.tokenizer\n */\nlunr.tokenizer.separator = /[\\s\\-]+/\n/*!\n * lunr.Pipeline\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Pipelines maintain an ordered list of functions to be applied to all\n * tokens in documents entering the search index and queries being ran against\n * the index.\n *\n * An instance of lunr.Index created with the lunr shortcut will contain a\n * pipeline with a stop word filter and an English language stemmer. Extra\n * functions can be added before or after either of these functions or these\n * default functions can be removed.\n *\n * When run the pipeline will call each function in turn, passing a token, the\n * index of that token in the original list of all tokens and finally a list of\n * all the original tokens.\n *\n * The output of functions in the pipeline will be passed to the next function\n * in the pipeline. To exclude a token from entering the index the function\n * should return undefined, the rest of the pipeline will not be called with\n * this token.\n *\n * For serialisation of pipelines to work, all functions used in an instance of\n * a pipeline should be registered with lunr.Pipeline. Registered functions can\n * then be loaded. If trying to load a serialised pipeline that uses functions\n * that are not registered an error will be thrown.\n *\n * If not planning on serialising the pipeline then registering pipeline functions\n * is not necessary.\n *\n * @constructor\n */\nlunr.Pipeline = function () {\n this._stack = []\n}\n\nlunr.Pipeline.registeredFunctions = Object.create(null)\n\n/**\n * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token\n * string as well as all known metadata. A pipeline function can mutate the token string\n * or mutate (or add) metadata for a given token.\n *\n * A pipeline function can indicate that the passed token should be discarded by returning\n * null, undefined or an empty string. This token will not be passed to any downstream pipeline\n * functions and will not be added to the index.\n *\n * Multiple tokens can be returned by returning an array of tokens. Each token will be passed\n * to any downstream pipeline functions and all will returned tokens will be added to the index.\n *\n * Any number of pipeline functions may be chained together using a lunr.Pipeline.\n *\n * @interface lunr.PipelineFunction\n * @param {lunr.Token} token - A token from the document being processed.\n * @param {number} i - The index of this token in the complete list of tokens for this document/field.\n * @param {lunr.Token[]} tokens - All tokens for this document/field.\n * @returns {(?lunr.Token|lunr.Token[])}\n */\n\n/**\n * Register a function with the pipeline.\n *\n * Functions that are used in the pipeline should be registered if the pipeline\n * needs to be serialised, or a serialised pipeline needs to be loaded.\n *\n * Registering a function does not add it to a pipeline, functions must still be\n * added to instances of the pipeline for them to be used when running a pipeline.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @param {String} label - The label to register this function with\n */\nlunr.Pipeline.registerFunction = function (fn, label) {\n if (label in this.registeredFunctions) {\n lunr.utils.warn('Overwriting existing registered function: ' + label)\n }\n\n fn.label = label\n lunr.Pipeline.registeredFunctions[fn.label] = fn\n}\n\n/**\n * Warns if the function is not registered as a Pipeline function.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @private\n */\nlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {\n var isRegistered = fn.label && (fn.label in this.registeredFunctions)\n\n if (!isRegistered) {\n lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\\n', fn)\n }\n}\n\n/**\n * Loads a previously serialised pipeline.\n *\n * All functions to be loaded must already be registered with lunr.Pipeline.\n * If any function from the serialised data has not been registered then an\n * error will be thrown.\n *\n * @param {Object} serialised - The serialised pipeline to load.\n * @returns {lunr.Pipeline}\n */\nlunr.Pipeline.load = function (serialised) {\n var pipeline = new lunr.Pipeline\n\n serialised.forEach(function (fnName) {\n var fn = lunr.Pipeline.registeredFunctions[fnName]\n\n if (fn) {\n pipeline.add(fn)\n } else {\n throw new Error('Cannot load unregistered function: ' + fnName)\n }\n })\n\n return pipeline\n}\n\n/**\n * Adds new functions to the end of the pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline.\n */\nlunr.Pipeline.prototype.add = function () {\n var fns = Array.prototype.slice.call(arguments)\n\n fns.forEach(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n this._stack.push(fn)\n }, this)\n}\n\n/**\n * Adds a single function after a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.after = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n pos = pos + 1\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Adds a single function before a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.before = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Removes a function from the pipeline.\n *\n * @param {lunr.PipelineFunction} fn The function to remove from the pipeline.\n */\nlunr.Pipeline.prototype.remove = function (fn) {\n var pos = this._stack.indexOf(fn)\n if (pos == -1) {\n return\n }\n\n this._stack.splice(pos, 1)\n}\n\n/**\n * Runs the current list of functions that make up the pipeline against the\n * passed tokens.\n *\n * @param {Array} tokens The tokens to run through the pipeline.\n * @returns {Array}\n */\nlunr.Pipeline.prototype.run = function (tokens) {\n var stackLength = this._stack.length\n\n for (var i = 0; i < stackLength; i++) {\n var fn = this._stack[i]\n var memo = []\n\n for (var j = 0; j < tokens.length; j++) {\n var result = fn(tokens[j], j, tokens)\n\n if (result === null || result === void 0 || result === '') continue\n\n if (Array.isArray(result)) {\n for (var k = 0; k < result.length; k++) {\n memo.push(result[k])\n }\n } else {\n memo.push(result)\n }\n }\n\n tokens = memo\n }\n\n return tokens\n}\n\n/**\n * Convenience method for passing a string through a pipeline and getting\n * strings out. This method takes care of wrapping the passed string in a\n * token and mapping the resulting tokens back to strings.\n *\n * @param {string} str - The string to pass through the pipeline.\n * @param {?object} metadata - Optional metadata to associate with the token\n * passed to the pipeline.\n * @returns {string[]}\n */\nlunr.Pipeline.prototype.runString = function (str, metadata) {\n var token = new lunr.Token (str, metadata)\n\n return this.run([token]).map(function (t) {\n return t.toString()\n })\n}\n\n/**\n * Resets the pipeline by removing any existing processors.\n *\n */\nlunr.Pipeline.prototype.reset = function () {\n this._stack = []\n}\n\n/**\n * Returns a representation of the pipeline ready for serialisation.\n *\n * Logs a warning if the function has not been registered.\n *\n * @returns {Array}\n */\nlunr.Pipeline.prototype.toJSON = function () {\n return this._stack.map(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n\n return fn.label\n })\n}\n/*!\n * lunr.Vector\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A vector is used to construct the vector space of documents and queries. These\n * vectors support operations to determine the similarity between two documents or\n * a document and a query.\n *\n * Normally no parameters are required for initializing a vector, but in the case of\n * loading a previously dumped vector the raw elements can be provided to the constructor.\n *\n * For performance reasons vectors are implemented with a flat array, where an elements\n * index is immediately followed by its value. E.g. [index, value, index, value]. This\n * allows the underlying array to be as sparse as possible and still offer decent\n * performance when being used for vector calculations.\n *\n * @constructor\n * @param {Number[]} [elements] - The flat list of element index and element value pairs.\n */\nlunr.Vector = function (elements) {\n this._magnitude = 0\n this.elements = elements || []\n}\n\n\n/**\n * Calculates the position within the vector to insert a given index.\n *\n * This is used internally by insert and upsert. If there are duplicate indexes then\n * the position is returned as if the value for that index were to be updated, but it\n * is the callers responsibility to check whether there is a duplicate at that index\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @returns {Number}\n */\nlunr.Vector.prototype.positionForIndex = function (index) {\n // For an empty vector the tuple can be inserted at the beginning\n if (this.elements.length == 0) {\n return 0\n }\n\n var start = 0,\n end = this.elements.length / 2,\n sliceLength = end - start,\n pivotPoint = Math.floor(sliceLength / 2),\n pivotIndex = this.elements[pivotPoint * 2]\n\n while (sliceLength > 1) {\n if (pivotIndex < index) {\n start = pivotPoint\n }\n\n if (pivotIndex > index) {\n end = pivotPoint\n }\n\n if (pivotIndex == index) {\n break\n }\n\n sliceLength = end - start\n pivotPoint = start + Math.floor(sliceLength / 2)\n pivotIndex = this.elements[pivotPoint * 2]\n }\n\n if (pivotIndex == index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex > index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex < index) {\n return (pivotPoint + 1) * 2\n }\n}\n\n/**\n * Inserts an element at an index within the vector.\n *\n * Does not allow duplicates, will throw an error if there is already an entry\n * for this index.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n */\nlunr.Vector.prototype.insert = function (insertIdx, val) {\n this.upsert(insertIdx, val, function () {\n throw \"duplicate index\"\n })\n}\n\n/**\n * Inserts or updates an existing index within the vector.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n * @param {function} fn - A function that is called for updates, the existing value and the\n * requested value are passed as arguments\n */\nlunr.Vector.prototype.upsert = function (insertIdx, val, fn) {\n this._magnitude = 0\n var position = this.positionForIndex(insertIdx)\n\n if (this.elements[position] == insertIdx) {\n this.elements[position + 1] = fn(this.elements[position + 1], val)\n } else {\n this.elements.splice(position, 0, insertIdx, val)\n }\n}\n\n/**\n * Calculates the magnitude of this vector.\n *\n * @returns {Number}\n */\nlunr.Vector.prototype.magnitude = function () {\n if (this._magnitude) return this._magnitude\n\n var sumOfSquares = 0,\n elementsLength = this.elements.length\n\n for (var i = 1; i < elementsLength; i += 2) {\n var val = this.elements[i]\n sumOfSquares += val * val\n }\n\n return this._magnitude = Math.sqrt(sumOfSquares)\n}\n\n/**\n * Calculates the dot product of this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The vector to compute the dot product with.\n * @returns {Number}\n */\nlunr.Vector.prototype.dot = function (otherVector) {\n var dotProduct = 0,\n a = this.elements, b = otherVector.elements,\n aLen = a.length, bLen = b.length,\n aVal = 0, bVal = 0,\n i = 0, j = 0\n\n while (i < aLen && j < bLen) {\n aVal = a[i], bVal = b[j]\n if (aVal < bVal) {\n i += 2\n } else if (aVal > bVal) {\n j += 2\n } else if (aVal == bVal) {\n dotProduct += a[i + 1] * b[j + 1]\n i += 2\n j += 2\n }\n }\n\n return dotProduct\n}\n\n/**\n * Calculates the similarity between this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The other vector to calculate the\n * similarity with.\n * @returns {Number}\n */\nlunr.Vector.prototype.similarity = function (otherVector) {\n return this.dot(otherVector) / this.magnitude() || 0\n}\n\n/**\n * Converts the vector to an array of the elements within the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toArray = function () {\n var output = new Array (this.elements.length / 2)\n\n for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) {\n output[j] = this.elements[i]\n }\n\n return output\n}\n\n/**\n * A JSON serializable representation of the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toJSON = function () {\n return this.elements\n}\n/* eslint-disable */\n/*!\n * lunr.stemmer\n * Copyright (C) 2020 Oliver Nightingale\n * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt\n */\n\n/**\n * lunr.stemmer is an english language stemmer, this is a JavaScript\n * implementation of the PorterStemmer taken from http://tartarus.org/~martin\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token - The string to stem\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n * @function\n */\nlunr.stemmer = (function(){\n var step2list = {\n \"ational\" : \"ate\",\n \"tional\" : \"tion\",\n \"enci\" : \"ence\",\n \"anci\" : \"ance\",\n \"izer\" : \"ize\",\n \"bli\" : \"ble\",\n \"alli\" : \"al\",\n \"entli\" : \"ent\",\n \"eli\" : \"e\",\n \"ousli\" : \"ous\",\n \"ization\" : \"ize\",\n \"ation\" : \"ate\",\n \"ator\" : \"ate\",\n \"alism\" : \"al\",\n \"iveness\" : \"ive\",\n \"fulness\" : \"ful\",\n \"ousness\" : \"ous\",\n \"aliti\" : \"al\",\n \"iviti\" : \"ive\",\n \"biliti\" : \"ble\",\n \"logi\" : \"log\"\n },\n\n step3list = {\n \"icate\" : \"ic\",\n \"ative\" : \"\",\n \"alize\" : \"al\",\n \"iciti\" : \"ic\",\n \"ical\" : \"ic\",\n \"ful\" : \"\",\n \"ness\" : \"\"\n },\n\n c = \"[^aeiou]\", // consonant\n v = \"[aeiouy]\", // vowel\n C = c + \"[^aeiouy]*\", // consonant sequence\n V = v + \"[aeiou]*\", // vowel sequence\n\n mgr0 = \"^(\" + C + \")?\" + V + C, // [C]VC... is m>0\n meq1 = \"^(\" + C + \")?\" + V + C + \"(\" + V + \")?$\", // [C]VC[V] is m=1\n mgr1 = \"^(\" + C + \")?\" + V + C + V + C, // [C]VCVC... is m>1\n s_v = \"^(\" + C + \")?\" + v; // vowel in stem\n\n var re_mgr0 = new RegExp(mgr0);\n var re_mgr1 = new RegExp(mgr1);\n var re_meq1 = new RegExp(meq1);\n var re_s_v = new RegExp(s_v);\n\n var re_1a = /^(.+?)(ss|i)es$/;\n var re2_1a = /^(.+?)([^s])s$/;\n var re_1b = /^(.+?)eed$/;\n var re2_1b = /^(.+?)(ed|ing)$/;\n var re_1b_2 = /.$/;\n var re2_1b_2 = /(at|bl|iz)$/;\n var re3_1b_2 = new RegExp(\"([^aeiouylsz])\\\\1$\");\n var re4_1b_2 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var re_1c = /^(.+?[^aeiou])y$/;\n var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;\n\n var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;\n\n var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;\n var re2_4 = /^(.+?)(s|t)(ion)$/;\n\n var re_5 = /^(.+?)e$/;\n var re_5_1 = /ll$/;\n var re3_5 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var porterStemmer = function porterStemmer(w) {\n var stem,\n suffix,\n firstch,\n re,\n re2,\n re3,\n re4;\n\n if (w.length < 3) { return w; }\n\n firstch = w.substr(0,1);\n if (firstch == \"y\") {\n w = firstch.toUpperCase() + w.substr(1);\n }\n\n // Step 1a\n re = re_1a\n re2 = re2_1a;\n\n if (re.test(w)) { w = w.replace(re,\"$1$2\"); }\n else if (re2.test(w)) { w = w.replace(re2,\"$1$2\"); }\n\n // Step 1b\n re = re_1b;\n re2 = re2_1b;\n if (re.test(w)) {\n var fp = re.exec(w);\n re = re_mgr0;\n if (re.test(fp[1])) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1];\n re2 = re_s_v;\n if (re2.test(stem)) {\n w = stem;\n re2 = re2_1b_2;\n re3 = re3_1b_2;\n re4 = re4_1b_2;\n if (re2.test(w)) { w = w + \"e\"; }\n else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,\"\"); }\n else if (re4.test(w)) { w = w + \"e\"; }\n }\n }\n\n // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)\n re = re_1c;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n w = stem + \"i\";\n }\n\n // Step 2\n re = re_2;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step2list[suffix];\n }\n }\n\n // Step 3\n re = re_3;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step3list[suffix];\n }\n }\n\n // Step 4\n re = re_4;\n re2 = re2_4;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n if (re.test(stem)) {\n w = stem;\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1] + fp[2];\n re2 = re_mgr1;\n if (re2.test(stem)) {\n w = stem;\n }\n }\n\n // Step 5\n re = re_5;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n re2 = re_meq1;\n re3 = re3_5;\n if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {\n w = stem;\n }\n }\n\n re = re_5_1;\n re2 = re_mgr1;\n if (re.test(w) && re2.test(w)) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n\n // and turn initial Y back to y\n\n if (firstch == \"y\") {\n w = firstch.toLowerCase() + w.substr(1);\n }\n\n return w;\n };\n\n return function (token) {\n return token.update(porterStemmer);\n }\n})();\n\nlunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')\n/*!\n * lunr.stopWordFilter\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.generateStopWordFilter builds a stopWordFilter function from the provided\n * list of stop words.\n *\n * The built in lunr.stopWordFilter is built using this generator and can be used\n * to generate custom stopWordFilters for applications or non English languages.\n *\n * @function\n * @param {Array} token The token to pass through the filter\n * @returns {lunr.PipelineFunction}\n * @see lunr.Pipeline\n * @see lunr.stopWordFilter\n */\nlunr.generateStopWordFilter = function (stopWords) {\n var words = stopWords.reduce(function (memo, stopWord) {\n memo[stopWord] = stopWord\n return memo\n }, {})\n\n return function (token) {\n if (token && words[token.toString()] !== token.toString()) return token\n }\n}\n\n/**\n * lunr.stopWordFilter is an English language stop word list filter, any words\n * contained in the list will not be passed through the filter.\n *\n * This is intended to be used in the Pipeline. If the token does not pass the\n * filter then undefined will be returned.\n *\n * @function\n * @implements {lunr.PipelineFunction}\n * @params {lunr.Token} token - A token to check for being a stop word.\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n */\nlunr.stopWordFilter = lunr.generateStopWordFilter([\n 'a',\n 'able',\n 'about',\n 'across',\n 'after',\n 'all',\n 'almost',\n 'also',\n 'am',\n 'among',\n 'an',\n 'and',\n 'any',\n 'are',\n 'as',\n 'at',\n 'be',\n 'because',\n 'been',\n 'but',\n 'by',\n 'can',\n 'cannot',\n 'could',\n 'dear',\n 'did',\n 'do',\n 'does',\n 'either',\n 'else',\n 'ever',\n 'every',\n 'for',\n 'from',\n 'get',\n 'got',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'hers',\n 'him',\n 'his',\n 'how',\n 'however',\n 'i',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'its',\n 'just',\n 'least',\n 'let',\n 'like',\n 'likely',\n 'may',\n 'me',\n 'might',\n 'most',\n 'must',\n 'my',\n 'neither',\n 'no',\n 'nor',\n 'not',\n 'of',\n 'off',\n 'often',\n 'on',\n 'only',\n 'or',\n 'other',\n 'our',\n 'own',\n 'rather',\n 'said',\n 'say',\n 'says',\n 'she',\n 'should',\n 'since',\n 'so',\n 'some',\n 'than',\n 'that',\n 'the',\n 'their',\n 'them',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'tis',\n 'to',\n 'too',\n 'twas',\n 'us',\n 'wants',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'while',\n 'who',\n 'whom',\n 'why',\n 'will',\n 'with',\n 'would',\n 'yet',\n 'you',\n 'your'\n])\n\nlunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')\n/*!\n * lunr.trimmer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.trimmer is a pipeline function for trimming non word\n * characters from the beginning and end of tokens before they\n * enter the index.\n *\n * This implementation may not work correctly for non latin\n * characters and should either be removed or adapted for use\n * with languages with non-latin characters.\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token The token to pass through the filter\n * @returns {lunr.Token}\n * @see lunr.Pipeline\n */\nlunr.trimmer = function (token) {\n return token.update(function (s) {\n return s.replace(/^\\W+/, '').replace(/\\W+$/, '')\n })\n}\n\nlunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')\n/*!\n * lunr.TokenSet\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A token set is used to store the unique list of all tokens\n * within an index. Token sets are also used to represent an\n * incoming query to the index, this query token set and index\n * token set are then intersected to find which tokens to look\n * up in the inverted index.\n *\n * A token set can hold multiple tokens, as in the case of the\n * index token set, or it can hold a single token as in the\n * case of a simple query token set.\n *\n * Additionally token sets are used to perform wildcard matching.\n * Leading, contained and trailing wildcards are supported, and\n * from this edit distance matching can also be provided.\n *\n * Token sets are implemented as a minimal finite state automata,\n * where both common prefixes and suffixes are shared between tokens.\n * This helps to reduce the space used for storing the token set.\n *\n * @constructor\n */\nlunr.TokenSet = function () {\n this.final = false\n this.edges = {}\n this.id = lunr.TokenSet._nextId\n lunr.TokenSet._nextId += 1\n}\n\n/**\n * Keeps track of the next, auto increment, identifier to assign\n * to a new tokenSet.\n *\n * TokenSets require a unique identifier to be correctly minimised.\n *\n * @private\n */\nlunr.TokenSet._nextId = 1\n\n/**\n * Creates a TokenSet instance from the given sorted array of words.\n *\n * @param {String[]} arr - A sorted array of strings to create the set from.\n * @returns {lunr.TokenSet}\n * @throws Will throw an error if the input array is not sorted.\n */\nlunr.TokenSet.fromArray = function (arr) {\n var builder = new lunr.TokenSet.Builder\n\n for (var i = 0, len = arr.length; i < len; i++) {\n builder.insert(arr[i])\n }\n\n builder.finish()\n return builder.root\n}\n\n/**\n * Creates a token set from a query clause.\n *\n * @private\n * @param {Object} clause - A single clause from lunr.Query.\n * @param {string} clause.term - The query clause term.\n * @param {number} [clause.editDistance] - The optional edit distance for the term.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromClause = function (clause) {\n if ('editDistance' in clause) {\n return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance)\n } else {\n return lunr.TokenSet.fromString(clause.term)\n }\n}\n\n/**\n * Creates a token set representing a single string with a specified\n * edit distance.\n *\n * Insertions, deletions, substitutions and transpositions are each\n * treated as an edit distance of 1.\n *\n * Increasing the allowed edit distance will have a dramatic impact\n * on the performance of both creating and intersecting these TokenSets.\n * It is advised to keep the edit distance less than 3.\n *\n * @param {string} str - The string to create the token set from.\n * @param {number} editDistance - The allowed edit distance to match.\n * @returns {lunr.Vector}\n */\nlunr.TokenSet.fromFuzzyString = function (str, editDistance) {\n var root = new lunr.TokenSet\n\n var stack = [{\n node: root,\n editsRemaining: editDistance,\n str: str\n }]\n\n while (stack.length) {\n var frame = stack.pop()\n\n // no edit\n if (frame.str.length > 0) {\n var char = frame.str.charAt(0),\n noEditNode\n\n if (char in frame.node.edges) {\n noEditNode = frame.node.edges[char]\n } else {\n noEditNode = new lunr.TokenSet\n frame.node.edges[char] = noEditNode\n }\n\n if (frame.str.length == 1) {\n noEditNode.final = true\n }\n\n stack.push({\n node: noEditNode,\n editsRemaining: frame.editsRemaining,\n str: frame.str.slice(1)\n })\n }\n\n if (frame.editsRemaining == 0) {\n continue\n }\n\n // insertion\n if (\"*\" in frame.node.edges) {\n var insertionNode = frame.node.edges[\"*\"]\n } else {\n var insertionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = insertionNode\n }\n\n if (frame.str.length == 0) {\n insertionNode.final = true\n }\n\n stack.push({\n node: insertionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str\n })\n\n // deletion\n // can only do a deletion if we have enough edits remaining\n // and if there are characters left to delete in the string\n if (frame.str.length > 1) {\n stack.push({\n node: frame.node,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // deletion\n // just removing the last character from the str\n if (frame.str.length == 1) {\n frame.node.final = true\n }\n\n // substitution\n // can only do a substitution if we have enough edits remaining\n // and if there are characters left to substitute\n if (frame.str.length >= 1) {\n if (\"*\" in frame.node.edges) {\n var substitutionNode = frame.node.edges[\"*\"]\n } else {\n var substitutionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = substitutionNode\n }\n\n if (frame.str.length == 1) {\n substitutionNode.final = true\n }\n\n stack.push({\n node: substitutionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // transposition\n // can only do a transposition if there are edits remaining\n // and there are enough characters to transpose\n if (frame.str.length > 1) {\n var charA = frame.str.charAt(0),\n charB = frame.str.charAt(1),\n transposeNode\n\n if (charB in frame.node.edges) {\n transposeNode = frame.node.edges[charB]\n } else {\n transposeNode = new lunr.TokenSet\n frame.node.edges[charB] = transposeNode\n }\n\n if (frame.str.length == 1) {\n transposeNode.final = true\n }\n\n stack.push({\n node: transposeNode,\n editsRemaining: frame.editsRemaining - 1,\n str: charA + frame.str.slice(2)\n })\n }\n }\n\n return root\n}\n\n/**\n * Creates a TokenSet from a string.\n *\n * The string may contain one or more wildcard characters (*)\n * that will allow wildcard matching when intersecting with\n * another TokenSet.\n *\n * @param {string} str - The string to create a TokenSet from.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromString = function (str) {\n var node = new lunr.TokenSet,\n root = node\n\n /*\n * Iterates through all characters within the passed string\n * appending a node for each character.\n *\n * When a wildcard character is found then a self\n * referencing edge is introduced to continually match\n * any number of any characters.\n */\n for (var i = 0, len = str.length; i < len; i++) {\n var char = str[i],\n final = (i == len - 1)\n\n if (char == \"*\") {\n node.edges[char] = node\n node.final = final\n\n } else {\n var next = new lunr.TokenSet\n next.final = final\n\n node.edges[char] = next\n node = next\n }\n }\n\n return root\n}\n\n/**\n * Converts this TokenSet into an array of strings\n * contained within the TokenSet.\n *\n * This is not intended to be used on a TokenSet that\n * contains wildcards, in these cases the results are\n * undefined and are likely to cause an infinite loop.\n *\n * @returns {string[]}\n */\nlunr.TokenSet.prototype.toArray = function () {\n var words = []\n\n var stack = [{\n prefix: \"\",\n node: this\n }]\n\n while (stack.length) {\n var frame = stack.pop(),\n edges = Object.keys(frame.node.edges),\n len = edges.length\n\n if (frame.node.final) {\n /* In Safari, at this point the prefix is sometimes corrupted, see:\n * https://github.com/olivernn/lunr.js/issues/279 Calling any\n * String.prototype method forces Safari to \"cast\" this string to what\n * it's supposed to be, fixing the bug. */\n frame.prefix.charAt(0)\n words.push(frame.prefix)\n }\n\n for (var i = 0; i < len; i++) {\n var edge = edges[i]\n\n stack.push({\n prefix: frame.prefix.concat(edge),\n node: frame.node.edges[edge]\n })\n }\n }\n\n return words\n}\n\n/**\n * Generates a string representation of a TokenSet.\n *\n * This is intended to allow TokenSets to be used as keys\n * in objects, largely to aid the construction and minimisation\n * of a TokenSet. As such it is not designed to be a human\n * friendly representation of the TokenSet.\n *\n * @returns {string}\n */\nlunr.TokenSet.prototype.toString = function () {\n // NOTE: Using Object.keys here as this.edges is very likely\n // to enter 'hash-mode' with many keys being added\n //\n // avoiding a for-in loop here as it leads to the function\n // being de-optimised (at least in V8). From some simple\n // benchmarks the performance is comparable, but allowing\n // V8 to optimize may mean easy performance wins in the future.\n\n if (this._str) {\n return this._str\n }\n\n var str = this.final ? '1' : '0',\n labels = Object.keys(this.edges).sort(),\n len = labels.length\n\n for (var i = 0; i < len; i++) {\n var label = labels[i],\n node = this.edges[label]\n\n str = str + label + node.id\n }\n\n return str\n}\n\n/**\n * Returns a new TokenSet that is the intersection of\n * this TokenSet and the passed TokenSet.\n *\n * This intersection will take into account any wildcards\n * contained within the TokenSet.\n *\n * @param {lunr.TokenSet} b - An other TokenSet to intersect with.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.prototype.intersect = function (b) {\n var output = new lunr.TokenSet,\n frame = undefined\n\n var stack = [{\n qNode: b,\n output: output,\n node: this\n }]\n\n while (stack.length) {\n frame = stack.pop()\n\n // NOTE: As with the #toString method, we are using\n // Object.keys and a for loop instead of a for-in loop\n // as both of these objects enter 'hash' mode, causing\n // the function to be de-optimised in V8\n var qEdges = Object.keys(frame.qNode.edges),\n qLen = qEdges.length,\n nEdges = Object.keys(frame.node.edges),\n nLen = nEdges.length\n\n for (var q = 0; q < qLen; q++) {\n var qEdge = qEdges[q]\n\n for (var n = 0; n < nLen; n++) {\n var nEdge = nEdges[n]\n\n if (nEdge == qEdge || qEdge == '*') {\n var node = frame.node.edges[nEdge],\n qNode = frame.qNode.edges[qEdge],\n final = node.final && qNode.final,\n next = undefined\n\n if (nEdge in frame.output.edges) {\n // an edge already exists for this character\n // no need to create a new node, just set the finality\n // bit unless this node is already final\n next = frame.output.edges[nEdge]\n next.final = next.final || final\n\n } else {\n // no edge exists yet, must create one\n // set the finality bit and insert it\n // into the output\n next = new lunr.TokenSet\n next.final = final\n frame.output.edges[nEdge] = next\n }\n\n stack.push({\n qNode: qNode,\n output: next,\n node: node\n })\n }\n }\n }\n }\n\n return output\n}\nlunr.TokenSet.Builder = function () {\n this.previousWord = \"\"\n this.root = new lunr.TokenSet\n this.uncheckedNodes = []\n this.minimizedNodes = {}\n}\n\nlunr.TokenSet.Builder.prototype.insert = function (word) {\n var node,\n commonPrefix = 0\n\n if (word < this.previousWord) {\n throw new Error (\"Out of order word insertion\")\n }\n\n for (var i = 0; i < word.length && i < this.previousWord.length; i++) {\n if (word[i] != this.previousWord[i]) break\n commonPrefix++\n }\n\n this.minimize(commonPrefix)\n\n if (this.uncheckedNodes.length == 0) {\n node = this.root\n } else {\n node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child\n }\n\n for (var i = commonPrefix; i < word.length; i++) {\n var nextNode = new lunr.TokenSet,\n char = word[i]\n\n node.edges[char] = nextNode\n\n this.uncheckedNodes.push({\n parent: node,\n char: char,\n child: nextNode\n })\n\n node = nextNode\n }\n\n node.final = true\n this.previousWord = word\n}\n\nlunr.TokenSet.Builder.prototype.finish = function () {\n this.minimize(0)\n}\n\nlunr.TokenSet.Builder.prototype.minimize = function (downTo) {\n for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) {\n var node = this.uncheckedNodes[i],\n childKey = node.child.toString()\n\n if (childKey in this.minimizedNodes) {\n node.parent.edges[node.char] = this.minimizedNodes[childKey]\n } else {\n // Cache the key for this node since\n // we know it can't change anymore\n node.child._str = childKey\n\n this.minimizedNodes[childKey] = node.child\n }\n\n this.uncheckedNodes.pop()\n }\n}\n/*!\n * lunr.Index\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * An index contains the built index of all documents and provides a query interface\n * to the index.\n *\n * Usually instances of lunr.Index will not be created using this constructor, instead\n * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be\n * used to load previously built and serialized indexes.\n *\n * @constructor\n * @param {Object} attrs - The attributes of the built search index.\n * @param {Object} attrs.invertedIndex - An index of term/field to document reference.\n * @param {Object} attrs.fieldVectors - Field vectors\n * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.\n * @param {string[]} attrs.fields - The names of indexed document fields.\n * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.\n */\nlunr.Index = function (attrs) {\n this.invertedIndex = attrs.invertedIndex\n this.fieldVectors = attrs.fieldVectors\n this.tokenSet = attrs.tokenSet\n this.fields = attrs.fields\n this.pipeline = attrs.pipeline\n}\n\n/**\n * A result contains details of a document matching a search query.\n * @typedef {Object} lunr.Index~Result\n * @property {string} ref - The reference of the document this result represents.\n * @property {number} score - A number between 0 and 1 representing how similar this document is to the query.\n * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match.\n */\n\n/**\n * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple\n * query language which itself is parsed into an instance of lunr.Query.\n *\n * For programmatically building queries it is advised to directly use lunr.Query, the query language\n * is best used for human entered text rather than program generated text.\n *\n * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported\n * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello'\n * or 'world', though those that contain both will rank higher in the results.\n *\n * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can\n * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding\n * wildcards will increase the number of documents that will be found but can also have a negative\n * impact on query performance, especially with wildcards at the beginning of a term.\n *\n * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term\n * hello in the title field will match this query. Using a field not present in the index will lead\n * to an error being thrown.\n *\n * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term\n * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported\n * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.\n * Avoid large values for edit distance to improve query performance.\n *\n * Each term also supports a presence modifier. By default a term's presence in document is optional, however\n * this can be changed to either required or prohibited. For a term's presence to be required in a document the\n * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and\n * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not\n * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.\n *\n * To escape special characters the backslash character '\\' can be used, this allows searches to include\n * characters that would normally be considered modifiers, e.g. `foo\\~2` will search for a term \"foo~2\" instead\n * of attempting to apply a boost of 2 to the search term \"foo\".\n *\n * @typedef {string} lunr.Index~QueryString\n * @example Simple single term query\n * hello\n * @example Multiple term query\n * hello world\n * @example term scoped to a field\n * title:hello\n * @example term with a boost of 10\n * hello^10\n * @example term with an edit distance of 2\n * hello~2\n * @example terms with presence modifiers\n * -foo +bar baz\n */\n\n/**\n * Performs a search against the index using lunr query syntax.\n *\n * Results will be returned sorted by their score, the most relevant results\n * will be returned first. For details on how the score is calculated, please see\n * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.\n *\n * For more programmatic querying use lunr.Index#query.\n *\n * @param {lunr.Index~QueryString} queryString - A string containing a lunr query.\n * @throws {lunr.QueryParseError} If the passed query string cannot be parsed.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.search = function (queryString) {\n return this.query(function (query) {\n var parser = new lunr.QueryParser(queryString, query)\n parser.parse()\n })\n}\n\n/**\n * A query builder callback provides a query object to be used to express\n * the query to perform on the index.\n *\n * @callback lunr.Index~queryBuilder\n * @param {lunr.Query} query - The query object to build up.\n * @this lunr.Query\n */\n\n/**\n * Performs a query against the index using the yielded lunr.Query object.\n *\n * If performing programmatic queries against the index, this method is preferred\n * over lunr.Index#search so as to avoid the additional query parsing overhead.\n *\n * A query object is yielded to the supplied function which should be used to\n * express the query to be run against the index.\n *\n * Note that although this function takes a callback parameter it is _not_ an\n * asynchronous operation, the callback is just yielded a query object to be\n * customized.\n *\n * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.query = function (fn) {\n // for each query clause\n // * process terms\n // * expand terms from token set\n // * find matching documents and metadata\n // * get document vectors\n // * score documents\n\n var query = new lunr.Query(this.fields),\n matchingFields = Object.create(null),\n queryVectors = Object.create(null),\n termFieldCache = Object.create(null),\n requiredMatches = Object.create(null),\n prohibitedMatches = Object.create(null)\n\n /*\n * To support field level boosts a query vector is created per\n * field. An empty vector is eagerly created to support negated\n * queries.\n */\n for (var i = 0; i < this.fields.length; i++) {\n queryVectors[this.fields[i]] = new lunr.Vector\n }\n\n fn.call(query, query)\n\n for (var i = 0; i < query.clauses.length; i++) {\n /*\n * Unless the pipeline has been disabled for this term, which is\n * the case for terms with wildcards, we need to pass the clause\n * term through the search pipeline. A pipeline returns an array\n * of processed terms. Pipeline functions may expand the passed\n * term, which means we may end up performing multiple index lookups\n * for a single query term.\n */\n var clause = query.clauses[i],\n terms = null,\n clauseMatches = lunr.Set.empty\n\n if (clause.usePipeline) {\n terms = this.pipeline.runString(clause.term, {\n fields: clause.fields\n })\n } else {\n terms = [clause.term]\n }\n\n for (var m = 0; m < terms.length; m++) {\n var term = terms[m]\n\n /*\n * Each term returned from the pipeline needs to use the same query\n * clause object, e.g. the same boost and or edit distance. The\n * simplest way to do this is to re-use the clause object but mutate\n * its term property.\n */\n clause.term = term\n\n /*\n * From the term in the clause we create a token set which will then\n * be used to intersect the indexes token set to get a list of terms\n * to lookup in the inverted index\n */\n var termTokenSet = lunr.TokenSet.fromClause(clause),\n expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()\n\n /*\n * If a term marked as required does not exist in the tokenSet it is\n * impossible for the search to return any matches. We set all the field\n * scoped required matches set to empty and stop examining any further\n * clauses.\n */\n if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = lunr.Set.empty\n }\n\n break\n }\n\n for (var j = 0; j < expandedTerms.length; j++) {\n /*\n * For each term get the posting and termIndex, this is required for\n * building the query vector.\n */\n var expandedTerm = expandedTerms[j],\n posting = this.invertedIndex[expandedTerm],\n termIndex = posting._index\n\n for (var k = 0; k < clause.fields.length; k++) {\n /*\n * For each field that this query term is scoped by (by default\n * all fields are in scope) we need to get all the document refs\n * that have this term in that field.\n *\n * The posting is the entry in the invertedIndex for the matching\n * term from above.\n */\n var field = clause.fields[k],\n fieldPosting = posting[field],\n matchingDocumentRefs = Object.keys(fieldPosting),\n termField = expandedTerm + \"/\" + field,\n matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)\n\n /*\n * if the presence of this term is required ensure that the matching\n * documents are added to the set of required matches for this clause.\n *\n */\n if (clause.presence == lunr.Query.presence.REQUIRED) {\n clauseMatches = clauseMatches.union(matchingDocumentsSet)\n\n if (requiredMatches[field] === undefined) {\n requiredMatches[field] = lunr.Set.complete\n }\n }\n\n /*\n * if the presence of this term is prohibited ensure that the matching\n * documents are added to the set of prohibited matches for this field,\n * creating that set if it does not yet exist.\n */\n if (clause.presence == lunr.Query.presence.PROHIBITED) {\n if (prohibitedMatches[field] === undefined) {\n prohibitedMatches[field] = lunr.Set.empty\n }\n\n prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)\n\n /*\n * Prohibited matches should not be part of the query vector used for\n * similarity scoring and no metadata should be extracted so we continue\n * to the next field\n */\n continue\n }\n\n /*\n * The query field vector is populated using the termIndex found for\n * the term and a unit value with the appropriate boost applied.\n * Using upsert because there could already be an entry in the vector\n * for the term we are working with. In that case we just add the scores\n * together.\n */\n queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })\n\n /**\n * If we've already seen this term, field combo then we've already collected\n * the matching documents and metadata, no need to go through all that again\n */\n if (termFieldCache[termField]) {\n continue\n }\n\n for (var l = 0; l < matchingDocumentRefs.length; l++) {\n /*\n * All metadata for this term/field/document triple\n * are then extracted and collected into an instance\n * of lunr.MatchData ready to be returned in the query\n * results\n */\n var matchingDocumentRef = matchingDocumentRefs[l],\n matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field),\n metadata = fieldPosting[matchingDocumentRef],\n fieldMatch\n\n if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) {\n matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata)\n } else {\n fieldMatch.add(expandedTerm, field, metadata)\n }\n\n }\n\n termFieldCache[termField] = true\n }\n }\n }\n\n /**\n * If the presence was required we need to update the requiredMatches field sets.\n * We do this after all fields for the term have collected their matches because\n * the clause terms presence is required in _any_ of the fields not _all_ of the\n * fields.\n */\n if (clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)\n }\n }\n }\n\n /**\n * Need to combine the field scoped required and prohibited\n * matching documents into a global set of required and prohibited\n * matches\n */\n var allRequiredMatches = lunr.Set.complete,\n allProhibitedMatches = lunr.Set.empty\n\n for (var i = 0; i < this.fields.length; i++) {\n var field = this.fields[i]\n\n if (requiredMatches[field]) {\n allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])\n }\n\n if (prohibitedMatches[field]) {\n allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])\n }\n }\n\n var matchingFieldRefs = Object.keys(matchingFields),\n results = [],\n matches = Object.create(null)\n\n /*\n * If the query is negated (contains only prohibited terms)\n * we need to get _all_ fieldRefs currently existing in the\n * index. This is only done when we know that the query is\n * entirely prohibited terms to avoid any cost of getting all\n * fieldRefs unnecessarily.\n *\n * Additionally, blank MatchData must be created to correctly\n * populate the results.\n */\n if (query.isNegated()) {\n matchingFieldRefs = Object.keys(this.fieldVectors)\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n var matchingFieldRef = matchingFieldRefs[i]\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)\n matchingFields[matchingFieldRef] = new lunr.MatchData\n }\n }\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n /*\n * Currently we have document fields that match the query, but we\n * need to return documents. The matchData and scores are combined\n * from multiple fields belonging to the same document.\n *\n * Scores are calculated by field, using the query vectors created\n * above, and combined into a final document score using addition.\n */\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),\n docRef = fieldRef.docRef\n\n if (!allRequiredMatches.contains(docRef)) {\n continue\n }\n\n if (allProhibitedMatches.contains(docRef)) {\n continue\n }\n\n var fieldVector = this.fieldVectors[fieldRef],\n score = queryVectors[fieldRef.fieldName].similarity(fieldVector),\n docMatch\n\n if ((docMatch = matches[docRef]) !== undefined) {\n docMatch.score += score\n docMatch.matchData.combine(matchingFields[fieldRef])\n } else {\n var match = {\n ref: docRef,\n score: score,\n matchData: matchingFields[fieldRef]\n }\n matches[docRef] = match\n results.push(match)\n }\n }\n\n /*\n * Sort the results objects by score, highest first.\n */\n return results.sort(function (a, b) {\n return b.score - a.score\n })\n}\n\n/**\n * Prepares the index for JSON serialization.\n *\n * The schema for this JSON blob will be described in a\n * separate JSON schema file.\n *\n * @returns {Object}\n */\nlunr.Index.prototype.toJSON = function () {\n var invertedIndex = Object.keys(this.invertedIndex)\n .sort()\n .map(function (term) {\n return [term, this.invertedIndex[term]]\n }, this)\n\n var fieldVectors = Object.keys(this.fieldVectors)\n .map(function (ref) {\n return [ref, this.fieldVectors[ref].toJSON()]\n }, this)\n\n return {\n version: lunr.version,\n fields: this.fields,\n fieldVectors: fieldVectors,\n invertedIndex: invertedIndex,\n pipeline: this.pipeline.toJSON()\n }\n}\n\n/**\n * Loads a previously serialized lunr.Index\n *\n * @param {Object} serializedIndex - A previously serialized lunr.Index\n * @returns {lunr.Index}\n */\nlunr.Index.load = function (serializedIndex) {\n var attrs = {},\n fieldVectors = {},\n serializedVectors = serializedIndex.fieldVectors,\n invertedIndex = Object.create(null),\n serializedInvertedIndex = serializedIndex.invertedIndex,\n tokenSetBuilder = new lunr.TokenSet.Builder,\n pipeline = lunr.Pipeline.load(serializedIndex.pipeline)\n\n if (serializedIndex.version != lunr.version) {\n lunr.utils.warn(\"Version mismatch when loading serialised index. Current version of lunr '\" + lunr.version + \"' does not match serialized index '\" + serializedIndex.version + \"'\")\n }\n\n for (var i = 0; i < serializedVectors.length; i++) {\n var tuple = serializedVectors[i],\n ref = tuple[0],\n elements = tuple[1]\n\n fieldVectors[ref] = new lunr.Vector(elements)\n }\n\n for (var i = 0; i < serializedInvertedIndex.length; i++) {\n var tuple = serializedInvertedIndex[i],\n term = tuple[0],\n posting = tuple[1]\n\n tokenSetBuilder.insert(term)\n invertedIndex[term] = posting\n }\n\n tokenSetBuilder.finish()\n\n attrs.fields = serializedIndex.fields\n\n attrs.fieldVectors = fieldVectors\n attrs.invertedIndex = invertedIndex\n attrs.tokenSet = tokenSetBuilder.root\n attrs.pipeline = pipeline\n\n return new lunr.Index(attrs)\n}\n/*!\n * lunr.Builder\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Builder performs indexing on a set of documents and\n * returns instances of lunr.Index ready for querying.\n *\n * All configuration of the index is done via the builder, the\n * fields to index, the document reference, the text processing\n * pipeline and document scoring parameters are all set on the\n * builder before indexing.\n *\n * @constructor\n * @property {string} _ref - Internal reference to the document reference field.\n * @property {string[]} _fields - Internal reference to the document fields to index.\n * @property {object} invertedIndex - The inverted index maps terms to document fields.\n * @property {object} documentTermFrequencies - Keeps track of document term frequencies.\n * @property {object} documentLengths - Keeps track of the length of documents added to the index.\n * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing.\n * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing.\n * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index.\n * @property {number} documentCount - Keeps track of the total number of documents indexed.\n * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75.\n * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2.\n * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space.\n * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index.\n */\nlunr.Builder = function () {\n this._ref = \"id\"\n this._fields = Object.create(null)\n this._documents = Object.create(null)\n this.invertedIndex = Object.create(null)\n this.fieldTermFrequencies = {}\n this.fieldLengths = {}\n this.tokenizer = lunr.tokenizer\n this.pipeline = new lunr.Pipeline\n this.searchPipeline = new lunr.Pipeline\n this.documentCount = 0\n this._b = 0.75\n this._k1 = 1.2\n this.termIndex = 0\n this.metadataWhitelist = []\n}\n\n/**\n * Sets the document field used as the document reference. Every document must have this field.\n * The type of this field in the document should be a string, if it is not a string it will be\n * coerced into a string by calling toString.\n *\n * The default ref is 'id'.\n *\n * The ref should _not_ be changed during indexing, it should be set before any documents are\n * added to the index. Changing it during indexing can lead to inconsistent results.\n *\n * @param {string} ref - The name of the reference field in the document.\n */\nlunr.Builder.prototype.ref = function (ref) {\n this._ref = ref\n}\n\n/**\n * A function that is used to extract a field from a document.\n *\n * Lunr expects a field to be at the top level of a document, if however the field\n * is deeply nested within a document an extractor function can be used to extract\n * the right field for indexing.\n *\n * @callback fieldExtractor\n * @param {object} doc - The document being added to the index.\n * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.\n * @example Extracting a nested field\n * function (doc) { return doc.nested.field }\n */\n\n/**\n * Adds a field to the list of document fields that will be indexed. Every document being\n * indexed should have this field. Null values for this field in indexed documents will\n * not cause errors but will limit the chance of that document being retrieved by searches.\n *\n * All fields should be added before adding documents to the index. Adding fields after\n * a document has been indexed will have no effect on already indexed documents.\n *\n * Fields can be boosted at build time. This allows terms within that field to have more\n * importance when ranking search results. Use a field boost to specify that matches within\n * one field are more important than other fields.\n *\n * @param {string} fieldName - The name of a field to index in all documents.\n * @param {object} attributes - Optional attributes associated with this field.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.\n * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.\n * @throws {RangeError} fieldName cannot contain unsupported characters '/'\n */\nlunr.Builder.prototype.field = function (fieldName, attributes) {\n if (/\\//.test(fieldName)) {\n throw new RangeError (\"Field '\" + fieldName + \"' contains illegal character '/'\")\n }\n\n this._fields[fieldName] = attributes || {}\n}\n\n/**\n * A parameter to tune the amount of field length normalisation that is applied when\n * calculating relevance scores. A value of 0 will completely disable any normalisation\n * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b\n * will be clamped to the range 0 - 1.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.b = function (number) {\n if (number < 0) {\n this._b = 0\n } else if (number > 1) {\n this._b = 1\n } else {\n this._b = number\n }\n}\n\n/**\n * A parameter that controls the speed at which a rise in term frequency results in term\n * frequency saturation. The default value is 1.2. Setting this to a higher value will give\n * slower saturation levels, a lower value will result in quicker saturation.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.k1 = function (number) {\n this._k1 = number\n}\n\n/**\n * Adds a document to the index.\n *\n * Before adding fields to the index the index should have been fully setup, with the document\n * ref and all fields to index already having been specified.\n *\n * The document must have a field name as specified by the ref (by default this is 'id') and\n * it should have all fields defined for indexing, though null or undefined values will not\n * cause errors.\n *\n * Entire documents can be boosted at build time. Applying a boost to a document indicates that\n * this document should rank higher in search results than other documents.\n *\n * @param {object} doc - The document to add to the index.\n * @param {object} attributes - Optional attributes associated with this document.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.\n */\nlunr.Builder.prototype.add = function (doc, attributes) {\n var docRef = doc[this._ref],\n fields = Object.keys(this._fields)\n\n this._documents[docRef] = attributes || {}\n this.documentCount += 1\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i],\n extractor = this._fields[fieldName].extractor,\n field = extractor ? extractor(doc) : doc[fieldName],\n tokens = this.tokenizer(field, {\n fields: [fieldName]\n }),\n terms = this.pipeline.run(tokens),\n fieldRef = new lunr.FieldRef (docRef, fieldName),\n fieldTerms = Object.create(null)\n\n this.fieldTermFrequencies[fieldRef] = fieldTerms\n this.fieldLengths[fieldRef] = 0\n\n // store the length of this field for this document\n this.fieldLengths[fieldRef] += terms.length\n\n // calculate term frequencies for this field\n for (var j = 0; j < terms.length; j++) {\n var term = terms[j]\n\n if (fieldTerms[term] == undefined) {\n fieldTerms[term] = 0\n }\n\n fieldTerms[term] += 1\n\n // add to inverted index\n // create an initial posting if one doesn't exist\n if (this.invertedIndex[term] == undefined) {\n var posting = Object.create(null)\n posting[\"_index\"] = this.termIndex\n this.termIndex += 1\n\n for (var k = 0; k < fields.length; k++) {\n posting[fields[k]] = Object.create(null)\n }\n\n this.invertedIndex[term] = posting\n }\n\n // add an entry for this term/fieldName/docRef to the invertedIndex\n if (this.invertedIndex[term][fieldName][docRef] == undefined) {\n this.invertedIndex[term][fieldName][docRef] = Object.create(null)\n }\n\n // store all whitelisted metadata about this token in the\n // inverted index\n for (var l = 0; l < this.metadataWhitelist.length; l++) {\n var metadataKey = this.metadataWhitelist[l],\n metadata = term.metadata[metadataKey]\n\n if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) {\n this.invertedIndex[term][fieldName][docRef][metadataKey] = []\n }\n\n this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata)\n }\n }\n\n }\n}\n\n/**\n * Calculates the average document length for this index\n *\n * @private\n */\nlunr.Builder.prototype.calculateAverageFieldLengths = function () {\n\n var fieldRefs = Object.keys(this.fieldLengths),\n numberOfFields = fieldRefs.length,\n accumulator = {},\n documentsWithField = {}\n\n for (var i = 0; i < numberOfFields; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n field = fieldRef.fieldName\n\n documentsWithField[field] || (documentsWithField[field] = 0)\n documentsWithField[field] += 1\n\n accumulator[field] || (accumulator[field] = 0)\n accumulator[field] += this.fieldLengths[fieldRef]\n }\n\n var fields = Object.keys(this._fields)\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i]\n accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]\n }\n\n this.averageFieldLength = accumulator\n}\n\n/**\n * Builds a vector space model of every document using lunr.Vector\n *\n * @private\n */\nlunr.Builder.prototype.createFieldVectors = function () {\n var fieldVectors = {},\n fieldRefs = Object.keys(this.fieldTermFrequencies),\n fieldRefsLength = fieldRefs.length,\n termIdfCache = Object.create(null)\n\n for (var i = 0; i < fieldRefsLength; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n fieldName = fieldRef.fieldName,\n fieldLength = this.fieldLengths[fieldRef],\n fieldVector = new lunr.Vector,\n termFrequencies = this.fieldTermFrequencies[fieldRef],\n terms = Object.keys(termFrequencies),\n termsLength = terms.length\n\n\n var fieldBoost = this._fields[fieldName].boost || 1,\n docBoost = this._documents[fieldRef.docRef].boost || 1\n\n for (var j = 0; j < termsLength; j++) {\n var term = terms[j],\n tf = termFrequencies[term],\n termIndex = this.invertedIndex[term]._index,\n idf, score, scoreWithPrecision\n\n if (termIdfCache[term] === undefined) {\n idf = lunr.idf(this.invertedIndex[term], this.documentCount)\n termIdfCache[term] = idf\n } else {\n idf = termIdfCache[term]\n }\n\n score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)\n score *= fieldBoost\n score *= docBoost\n scoreWithPrecision = Math.round(score * 1000) / 1000\n // Converts 1.23456789 to 1.234.\n // Reducing the precision so that the vectors take up less\n // space when serialised. Doing it now so that they behave\n // the same before and after serialisation. Also, this is\n // the fastest approach to reducing a number's precision in\n // JavaScript.\n\n fieldVector.insert(termIndex, scoreWithPrecision)\n }\n\n fieldVectors[fieldRef] = fieldVector\n }\n\n this.fieldVectors = fieldVectors\n}\n\n/**\n * Creates a token set of all tokens in the index using lunr.TokenSet\n *\n * @private\n */\nlunr.Builder.prototype.createTokenSet = function () {\n this.tokenSet = lunr.TokenSet.fromArray(\n Object.keys(this.invertedIndex).sort()\n )\n}\n\n/**\n * Builds the index, creating an instance of lunr.Index.\n *\n * This completes the indexing process and should only be called\n * once all documents have been added to the index.\n *\n * @returns {lunr.Index}\n */\nlunr.Builder.prototype.build = function () {\n this.calculateAverageFieldLengths()\n this.createFieldVectors()\n this.createTokenSet()\n\n return new lunr.Index({\n invertedIndex: this.invertedIndex,\n fieldVectors: this.fieldVectors,\n tokenSet: this.tokenSet,\n fields: Object.keys(this._fields),\n pipeline: this.searchPipeline\n })\n}\n\n/**\n * Applies a plugin to the index builder.\n *\n * A plugin is a function that is called with the index builder as its context.\n * Plugins can be used to customise or extend the behaviour of the index\n * in some way. A plugin is just a function, that encapsulated the custom\n * behaviour that should be applied when building the index.\n *\n * The plugin function will be called with the index builder as its argument, additional\n * arguments can also be passed when calling use. The function will be called\n * with the index builder as its context.\n *\n * @param {Function} plugin The plugin to apply.\n */\nlunr.Builder.prototype.use = function (fn) {\n var args = Array.prototype.slice.call(arguments, 1)\n args.unshift(this)\n fn.apply(this, args)\n}\n/**\n * Contains and collects metadata about a matching document.\n * A single instance of lunr.MatchData is returned as part of every\n * lunr.Index~Result.\n *\n * @constructor\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n * @property {object} metadata - A cloned collection of metadata associated with this document.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData = function (term, field, metadata) {\n var clonedMetadata = Object.create(null),\n metadataKeys = Object.keys(metadata || {})\n\n // Cloning the metadata to prevent the original\n // being mutated during match data combination.\n // Metadata is kept in an array within the inverted\n // index so cloning the data can be done with\n // Array#slice\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n clonedMetadata[key] = metadata[key].slice()\n }\n\n this.metadata = Object.create(null)\n\n if (term !== undefined) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = clonedMetadata\n }\n}\n\n/**\n * An instance of lunr.MatchData will be created for every term that matches a\n * document. However only one instance is required in a lunr.Index~Result. This\n * method combines metadata from another instance of lunr.MatchData with this\n * objects metadata.\n *\n * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData.prototype.combine = function (otherMatchData) {\n var terms = Object.keys(otherMatchData.metadata)\n\n for (var i = 0; i < terms.length; i++) {\n var term = terms[i],\n fields = Object.keys(otherMatchData.metadata[term])\n\n if (this.metadata[term] == undefined) {\n this.metadata[term] = Object.create(null)\n }\n\n for (var j = 0; j < fields.length; j++) {\n var field = fields[j],\n keys = Object.keys(otherMatchData.metadata[term][field])\n\n if (this.metadata[term][field] == undefined) {\n this.metadata[term][field] = Object.create(null)\n }\n\n for (var k = 0; k < keys.length; k++) {\n var key = keys[k]\n\n if (this.metadata[term][field][key] == undefined) {\n this.metadata[term][field][key] = otherMatchData.metadata[term][field][key]\n } else {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key])\n }\n\n }\n }\n }\n}\n\n/**\n * Add metadata for a term/field pair to this instance of match data.\n *\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n */\nlunr.MatchData.prototype.add = function (term, field, metadata) {\n if (!(term in this.metadata)) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = metadata\n return\n }\n\n if (!(field in this.metadata[term])) {\n this.metadata[term][field] = metadata\n return\n }\n\n var metadataKeys = Object.keys(metadata)\n\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n\n if (key in this.metadata[term][field]) {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key])\n } else {\n this.metadata[term][field][key] = metadata[key]\n }\n }\n}\n/**\n * A lunr.Query provides a programmatic way of defining queries to be performed\n * against a {@link lunr.Index}.\n *\n * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method\n * so the query object is pre-initialized with the right index fields.\n *\n * @constructor\n * @property {lunr.Query~Clause[]} clauses - An array of query clauses.\n * @property {string[]} allFields - An array of all available fields in a lunr.Index.\n */\nlunr.Query = function (allFields) {\n this.clauses = []\n this.allFields = allFields\n}\n\n/**\n * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause.\n *\n * This allows wildcards to be added to the beginning and end of a term without having to manually do any string\n * concatenation.\n *\n * The wildcard constants can be bitwise combined to select both leading and trailing wildcards.\n *\n * @constant\n * @default\n * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour\n * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists\n * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with trailing wildcard\n * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING })\n * @example query term with leading and trailing wildcard\n * query.term('foo', {\n * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING\n * })\n */\n\nlunr.Query.wildcard = new String (\"*\")\nlunr.Query.wildcard.NONE = 0\nlunr.Query.wildcard.LEADING = 1\nlunr.Query.wildcard.TRAILING = 2\n\n/**\n * Constants for indicating what kind of presence a term must have in matching documents.\n *\n * @constant\n * @enum {number}\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with required presence\n * query.term('foo', { presence: lunr.Query.presence.REQUIRED })\n */\nlunr.Query.presence = {\n /**\n * Term's presence in a document is optional, this is the default value.\n */\n OPTIONAL: 1,\n\n /**\n * Term's presence in a document is required, documents that do not contain\n * this term will not be returned.\n */\n REQUIRED: 2,\n\n /**\n * Term's presence in a document is prohibited, documents that do contain\n * this term will not be returned.\n */\n PROHIBITED: 3\n}\n\n/**\n * A single clause in a {@link lunr.Query} contains a term and details on how to\n * match that term against a {@link lunr.Index}.\n *\n * @typedef {Object} lunr.Query~Clause\n * @property {string[]} fields - The fields in an index this clause should be matched against.\n * @property {number} [boost=1] - Any boost that should be applied when matching this clause.\n * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.\n * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.\n * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.\n * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.\n */\n\n/**\n * Adds a {@link lunr.Query~Clause} to this query.\n *\n * Unless the clause contains the fields to be matched all fields will be matched. In addition\n * a default boost of 1 is applied to the clause.\n *\n * @param {lunr.Query~Clause} clause - The clause to add to this query.\n * @see lunr.Query~Clause\n * @returns {lunr.Query}\n */\nlunr.Query.prototype.clause = function (clause) {\n if (!('fields' in clause)) {\n clause.fields = this.allFields\n }\n\n if (!('boost' in clause)) {\n clause.boost = 1\n }\n\n if (!('usePipeline' in clause)) {\n clause.usePipeline = true\n }\n\n if (!('wildcard' in clause)) {\n clause.wildcard = lunr.Query.wildcard.NONE\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) {\n clause.term = \"*\" + clause.term\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) {\n clause.term = \"\" + clause.term + \"*\"\n }\n\n if (!('presence' in clause)) {\n clause.presence = lunr.Query.presence.OPTIONAL\n }\n\n this.clauses.push(clause)\n\n return this\n}\n\n/**\n * A negated query is one in which every clause has a presence of\n * prohibited. These queries require some special processing to return\n * the expected results.\n *\n * @returns boolean\n */\nlunr.Query.prototype.isNegated = function () {\n for (var i = 0; i < this.clauses.length; i++) {\n if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}\n * to the list of clauses that make up this query.\n *\n * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion\n * to a token or token-like string should be done before calling this method.\n *\n * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an\n * array, each term in the array will share the same options.\n *\n * @param {object|object[]} term - The term(s) to add to the query.\n * @param {object} [options] - Any additional properties to add to the query clause.\n * @returns {lunr.Query}\n * @see lunr.Query#clause\n * @see lunr.Query~Clause\n * @example adding a single term to a query\n * query.term(\"foo\")\n * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard\n * query.term(\"foo\", {\n * fields: [\"title\"],\n * boost: 10,\n * wildcard: lunr.Query.wildcard.TRAILING\n * })\n * @example using lunr.tokenizer to convert a string to tokens before using them as terms\n * query.term(lunr.tokenizer(\"foo bar\"))\n */\nlunr.Query.prototype.term = function (term, options) {\n if (Array.isArray(term)) {\n term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this)\n return this\n }\n\n var clause = options || {}\n clause.term = term.toString()\n\n this.clause(clause)\n\n return this\n}\nlunr.QueryParseError = function (message, start, end) {\n this.name = \"QueryParseError\"\n this.message = message\n this.start = start\n this.end = end\n}\n\nlunr.QueryParseError.prototype = new Error\nlunr.QueryLexer = function (str) {\n this.lexemes = []\n this.str = str\n this.length = str.length\n this.pos = 0\n this.start = 0\n this.escapeCharPositions = []\n}\n\nlunr.QueryLexer.prototype.run = function () {\n var state = lunr.QueryLexer.lexText\n\n while (state) {\n state = state(this)\n }\n}\n\nlunr.QueryLexer.prototype.sliceString = function () {\n var subSlices = [],\n sliceStart = this.start,\n sliceEnd = this.pos\n\n for (var i = 0; i < this.escapeCharPositions.length; i++) {\n sliceEnd = this.escapeCharPositions[i]\n subSlices.push(this.str.slice(sliceStart, sliceEnd))\n sliceStart = sliceEnd + 1\n }\n\n subSlices.push(this.str.slice(sliceStart, this.pos))\n this.escapeCharPositions.length = 0\n\n return subSlices.join('')\n}\n\nlunr.QueryLexer.prototype.emit = function (type) {\n this.lexemes.push({\n type: type,\n str: this.sliceString(),\n start: this.start,\n end: this.pos\n })\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.escapeCharacter = function () {\n this.escapeCharPositions.push(this.pos - 1)\n this.pos += 1\n}\n\nlunr.QueryLexer.prototype.next = function () {\n if (this.pos >= this.length) {\n return lunr.QueryLexer.EOS\n }\n\n var char = this.str.charAt(this.pos)\n this.pos += 1\n return char\n}\n\nlunr.QueryLexer.prototype.width = function () {\n return this.pos - this.start\n}\n\nlunr.QueryLexer.prototype.ignore = function () {\n if (this.start == this.pos) {\n this.pos += 1\n }\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.backup = function () {\n this.pos -= 1\n}\n\nlunr.QueryLexer.prototype.acceptDigitRun = function () {\n var char, charCode\n\n do {\n char = this.next()\n charCode = char.charCodeAt(0)\n } while (charCode > 47 && charCode < 58)\n\n if (char != lunr.QueryLexer.EOS) {\n this.backup()\n }\n}\n\nlunr.QueryLexer.prototype.more = function () {\n return this.pos < this.length\n}\n\nlunr.QueryLexer.EOS = 'EOS'\nlunr.QueryLexer.FIELD = 'FIELD'\nlunr.QueryLexer.TERM = 'TERM'\nlunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE'\nlunr.QueryLexer.BOOST = 'BOOST'\nlunr.QueryLexer.PRESENCE = 'PRESENCE'\n\nlunr.QueryLexer.lexField = function (lexer) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.FIELD)\n lexer.ignore()\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexTerm = function (lexer) {\n if (lexer.width() > 1) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.TERM)\n }\n\n lexer.ignore()\n\n if (lexer.more()) {\n return lunr.QueryLexer.lexText\n }\n}\n\nlunr.QueryLexer.lexEditDistance = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.EDIT_DISTANCE)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexBoost = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.BOOST)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexEOS = function (lexer) {\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n}\n\n// This matches the separator used when tokenising fields\n// within a document. These should match otherwise it is\n// not possible to search for some tokens within a document.\n//\n// It is possible for the user to change the separator on the\n// tokenizer so it _might_ clash with any other of the special\n// characters already used within the search string, e.g. :.\n//\n// This means that it is possible to change the separator in\n// such a way that makes some words unsearchable using a search\n// string.\nlunr.QueryLexer.termSeparator = lunr.tokenizer.separator\n\nlunr.QueryLexer.lexText = function (lexer) {\n while (true) {\n var char = lexer.next()\n\n if (char == lunr.QueryLexer.EOS) {\n return lunr.QueryLexer.lexEOS\n }\n\n // Escape character is '\\'\n if (char.charCodeAt(0) == 92) {\n lexer.escapeCharacter()\n continue\n }\n\n if (char == \":\") {\n return lunr.QueryLexer.lexField\n }\n\n if (char == \"~\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexEditDistance\n }\n\n if (char == \"^\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexBoost\n }\n\n // \"+\" indicates term presence is required\n // checking for length to ensure that only\n // leading \"+\" are considered\n if (char == \"+\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n // \"-\" indicates term presence is prohibited\n // checking for length to ensure that only\n // leading \"-\" are considered\n if (char == \"-\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n if (char.match(lunr.QueryLexer.termSeparator)) {\n return lunr.QueryLexer.lexTerm\n }\n }\n}\n\nlunr.QueryParser = function (str, query) {\n this.lexer = new lunr.QueryLexer (str)\n this.query = query\n this.currentClause = {}\n this.lexemeIdx = 0\n}\n\nlunr.QueryParser.prototype.parse = function () {\n this.lexer.run()\n this.lexemes = this.lexer.lexemes\n\n var state = lunr.QueryParser.parseClause\n\n while (state) {\n state = state(this)\n }\n\n return this.query\n}\n\nlunr.QueryParser.prototype.peekLexeme = function () {\n return this.lexemes[this.lexemeIdx]\n}\n\nlunr.QueryParser.prototype.consumeLexeme = function () {\n var lexeme = this.peekLexeme()\n this.lexemeIdx += 1\n return lexeme\n}\n\nlunr.QueryParser.prototype.nextClause = function () {\n var completedClause = this.currentClause\n this.query.clause(completedClause)\n this.currentClause = {}\n}\n\nlunr.QueryParser.parseClause = function (parser) {\n var lexeme = parser.peekLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.type) {\n case lunr.QueryLexer.PRESENCE:\n return lunr.QueryParser.parsePresence\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expected either a field or a term, found \" + lexeme.type\n\n if (lexeme.str.length >= 1) {\n errorMessage += \" with value '\" + lexeme.str + \"'\"\n }\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n}\n\nlunr.QueryParser.parsePresence = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.str) {\n case \"-\":\n parser.currentClause.presence = lunr.Query.presence.PROHIBITED\n break\n case \"+\":\n parser.currentClause.presence = lunr.Query.presence.REQUIRED\n break\n default:\n var errorMessage = \"unrecognised presence operator'\" + lexeme.str + \"'\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term or field, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term or field, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseField = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n if (parser.query.allFields.indexOf(lexeme.str) == -1) {\n var possibleFields = parser.query.allFields.map(function (f) { return \"'\" + f + \"'\" }).join(', '),\n errorMessage = \"unrecognised field '\" + lexeme.str + \"', possible fields: \" + possibleFields\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.fields = [lexeme.str]\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseTerm = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n parser.currentClause.term = lexeme.str.toLowerCase()\n\n if (lexeme.str.indexOf(\"*\") != -1) {\n parser.currentClause.usePipeline = false\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseEditDistance = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var editDistance = parseInt(lexeme.str, 10)\n\n if (isNaN(editDistance)) {\n var errorMessage = \"edit distance must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.editDistance = editDistance\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseBoost = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var boost = parseInt(lexeme.str, 10)\n\n if (isNaN(boost)) {\n var errorMessage = \"boost must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.boost = boost\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\n /**\n * export the module via AMD, CommonJS or as a browser global\n * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js\n */\n ;(function (root, factory) {\n if (true) {\n // AMD. Register as an anonymous module.\n !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :\n\t\t__WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__))\n } else {}\n }(this, function () {\n /**\n * Just return a value to define the module export.\n * This example returns an object, but the module\n * can return a function as the exported value.\n */\n return lunr\n }))\n})();\n\n\n//# sourceURL=webpack:///../node_modules/lunr/lunr.js?"); + +/***/ }), + +/***/ "./default/assets/css/main.sass": +/*!**************************************!*\ + !*** ./default/assets/css/main.sass ***! + \**************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extract-plugin\n\n\n//# sourceURL=webpack:///./default/assets/css/main.sass?"); + +/***/ }), + +/***/ "./default/assets/js/src/bootstrap.ts": +/*!********************************************!*\ + !*** ./default/assets/js/src/bootstrap.ts ***! + \********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _typedoc_Application__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./typedoc/Application */ \"./default/assets/js/src/typedoc/Application.ts\");\n/* harmony import */ var _typedoc_components_MenuHighlight__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./typedoc/components/MenuHighlight */ \"./default/assets/js/src/typedoc/components/MenuHighlight.ts\");\n/* harmony import */ var _typedoc_components_Search__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./typedoc/components/Search */ \"./default/assets/js/src/typedoc/components/Search.ts\");\n/* harmony import */ var _typedoc_components_Signature__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./typedoc/components/Signature */ \"./default/assets/js/src/typedoc/components/Signature.ts\");\n/* harmony import */ var _typedoc_components_Toggle__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./typedoc/components/Toggle */ \"./default/assets/js/src/typedoc/components/Toggle.ts\");\n/* harmony import */ var _typedoc_components_Filter__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./typedoc/components/Filter */ \"./default/assets/js/src/typedoc/components/Filter.ts\");\n/* harmony import */ var _css_main_sass__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../css/main.sass */ \"./default/assets/css/main.sass\");\n\n\n\n\n\n\n\n(0,_typedoc_components_Search__WEBPACK_IMPORTED_MODULE_2__.initSearch)();\n(0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_MenuHighlight__WEBPACK_IMPORTED_MODULE_1__.MenuHighlight, \".menu-highlight\");\n(0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_Signature__WEBPACK_IMPORTED_MODULE_3__.Signature, \".tsd-signatures\");\n(0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_Toggle__WEBPACK_IMPORTED_MODULE_4__.Toggle, \"a[data-toggle]\");\nif (_typedoc_components_Filter__WEBPACK_IMPORTED_MODULE_5__.Filter.isSupported()) {\n (0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_Filter__WEBPACK_IMPORTED_MODULE_5__.Filter, \"#tsd-filter\");\n}\nelse {\n document.documentElement.classList.add(\"no-filter\");\n}\nvar app = new _typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.Application();\nObject.defineProperty(window, \"app\", { value: app });\n\n\n//# sourceURL=webpack:///./default/assets/js/src/bootstrap.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/Application.ts": +/*!******************************************************!*\ + !*** ./default/assets/js/src/typedoc/Application.ts ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"registerComponent\": () => /* binding */ registerComponent,\n/* harmony export */ \"Application\": () => /* binding */ Application\n/* harmony export */ });\n/**\n * List of all known components.\n */\nvar components = [];\n/**\n * Register a new component.\n */\nfunction registerComponent(constructor, selector) {\n components.push({\n selector: selector,\n constructor: constructor,\n });\n}\n/**\n * TypeDoc application class.\n */\nvar Application = /** @class */ (function () {\n /**\n * Create a new Application instance.\n */\n function Application() {\n this.createComponents(document.body);\n }\n /**\n * Create all components beneath the given jQuery element.\n */\n Application.prototype.createComponents = function (context) {\n components.forEach(function (c) {\n context.querySelectorAll(c.selector).forEach(function (el) {\n if (!el.dataset.hasInstance) {\n new c.constructor({ el: el });\n el.dataset.hasInstance = String(true);\n }\n });\n });\n };\n return Application;\n}());\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/Application.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/Component.ts": +/*!****************************************************!*\ + !*** ./default/assets/js/src/typedoc/Component.ts ***! + \****************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Component\": () => /* binding */ Component\n/* harmony export */ });\n/**\n * TypeDoc component class.\n */\nvar Component = /** @class */ (function () {\n function Component(options) {\n this.el = options.el;\n }\n return Component;\n}());\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/Component.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/EventTarget.ts": +/*!******************************************************!*\ + !*** ./default/assets/js/src/typedoc/EventTarget.ts ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"EventTarget\": () => /* binding */ EventTarget\n/* harmony export */ });\n/**\n * TypeDoc event target class.\n */\nvar EventTarget = /** @class */ (function () {\n function EventTarget() {\n this.listeners = {};\n }\n EventTarget.prototype.addEventListener = function (type, callback) {\n if (!(type in this.listeners)) {\n this.listeners[type] = [];\n }\n this.listeners[type].push(callback);\n };\n EventTarget.prototype.removeEventListener = function (type, callback) {\n if (!(type in this.listeners)) {\n return;\n }\n var stack = this.listeners[type];\n for (var i = 0, l = stack.length; i < l; i++) {\n if (stack[i] === callback) {\n stack.splice(i, 1);\n return;\n }\n }\n };\n EventTarget.prototype.dispatchEvent = function (event) {\n if (!(event.type in this.listeners)) {\n return true;\n }\n var stack = this.listeners[event.type].slice();\n for (var i = 0, l = stack.length; i < l; i++) {\n stack[i].call(this, event);\n }\n return !event.defaultPrevented;\n };\n return EventTarget;\n}());\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/EventTarget.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Filter.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Filter.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Filter\": () => /* binding */ Filter\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _utils_pointer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils/pointer */ \"./default/assets/js/src/typedoc/utils/pointer.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\nvar FilterItem = /** @class */ (function () {\n function FilterItem(key, value) {\n this.key = key;\n this.value = value;\n this.defaultValue = value;\n this.initialize();\n if (window.localStorage[this.key]) {\n this.setValue(this.fromLocalStorage(window.localStorage[this.key]));\n }\n }\n FilterItem.prototype.initialize = function () { };\n FilterItem.prototype.setValue = function (value) {\n if (this.value == value)\n return;\n var oldValue = this.value;\n this.value = value;\n window.localStorage[this.key] = this.toLocalStorage(value);\n this.handleValueChange(oldValue, value);\n };\n return FilterItem;\n}());\nvar FilterItemCheckbox = /** @class */ (function (_super) {\n __extends(FilterItemCheckbox, _super);\n function FilterItemCheckbox() {\n return _super !== null && _super.apply(this, arguments) || this;\n }\n FilterItemCheckbox.prototype.initialize = function () {\n var _this = this;\n var checkbox = document.querySelector(\"#tsd-filter-\" + this.key);\n if (!checkbox)\n return;\n this.checkbox = checkbox;\n this.checkbox.addEventListener(\"change\", function () {\n _this.setValue(_this.checkbox.checked);\n });\n };\n FilterItemCheckbox.prototype.handleValueChange = function (oldValue, newValue) {\n if (!this.checkbox)\n return;\n this.checkbox.checked = this.value;\n document.documentElement.classList.toggle(\"toggle-\" + this.key, this.value != this.defaultValue);\n };\n FilterItemCheckbox.prototype.fromLocalStorage = function (value) {\n return value == \"true\";\n };\n FilterItemCheckbox.prototype.toLocalStorage = function (value) {\n return value ? \"true\" : \"false\";\n };\n return FilterItemCheckbox;\n}(FilterItem));\nvar FilterItemSelect = /** @class */ (function (_super) {\n __extends(FilterItemSelect, _super);\n function FilterItemSelect() {\n return _super !== null && _super.apply(this, arguments) || this;\n }\n FilterItemSelect.prototype.initialize = function () {\n var _this = this;\n document.documentElement.classList.add(\"toggle-\" + this.key + this.value);\n var select = document.querySelector(\"#tsd-filter-\" + this.key);\n if (!select)\n return;\n this.select = select;\n var onActivate = function () {\n _this.select.classList.add(\"active\");\n };\n var onDeactivate = function () {\n _this.select.classList.remove(\"active\");\n };\n this.select.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerDown, onActivate);\n this.select.addEventListener(\"mouseover\", onActivate);\n this.select.addEventListener(\"mouseleave\", onDeactivate);\n this.select.querySelectorAll(\"li\").forEach(function (el) {\n el.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerUp, function (e) {\n select.classList.remove(\"active\");\n _this.setValue(e.target.dataset.value || \"\");\n });\n });\n document.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerDown, function (e) {\n if (_this.select.contains(e.target))\n return;\n _this.select.classList.remove(\"active\");\n });\n };\n FilterItemSelect.prototype.handleValueChange = function (oldValue, newValue) {\n this.select.querySelectorAll(\"li.selected\").forEach(function (el) {\n el.classList.remove(\"selected\");\n });\n var selected = this.select.querySelector('li[data-value=\"' + newValue + '\"]');\n var label = this.select.querySelector(\".tsd-select-label\");\n if (selected && label) {\n selected.classList.add(\"selected\");\n label.textContent = selected.textContent;\n }\n document.documentElement.classList.remove(\"toggle-\" + oldValue);\n document.documentElement.classList.add(\"toggle-\" + newValue);\n };\n FilterItemSelect.prototype.fromLocalStorage = function (value) {\n return value;\n };\n FilterItemSelect.prototype.toLocalStorage = function (value) {\n return value;\n };\n return FilterItemSelect;\n}(FilterItem));\nvar Filter = /** @class */ (function (_super) {\n __extends(Filter, _super);\n function Filter(options) {\n var _this = _super.call(this, options) || this;\n _this.optionVisibility = new FilterItemSelect(\"visibility\", \"private\");\n _this.optionInherited = new FilterItemCheckbox(\"inherited\", true);\n _this.optionExternals = new FilterItemCheckbox(\"externals\", true);\n return _this;\n }\n Filter.isSupported = function () {\n try {\n return typeof window.localStorage != \"undefined\";\n }\n catch (e) {\n return false;\n }\n };\n return Filter;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Filter.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/MenuHighlight.ts": +/*!*******************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/MenuHighlight.ts ***! + \*******************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"MenuHighlight\": () => /* binding */ MenuHighlight\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _services_Viewport__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../services/Viewport */ \"./default/assets/js/src/typedoc/services/Viewport.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\n/**\n * Manages the sticky state of the navigation and moves the highlight\n * to the current navigation item.\n */\nvar MenuHighlight = /** @class */ (function (_super) {\n __extends(MenuHighlight, _super);\n /**\n * Create a new MenuHighlight instance.\n *\n * @param options Backbone view constructor options.\n */\n function MenuHighlight(options) {\n var _this = _super.call(this, options) || this;\n /**\n * List of all discovered anchors.\n */\n _this.anchors = [];\n /**\n * Index of the currently highlighted anchor.\n */\n _this.index = -1;\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.addEventListener(\"resize\", function () { return _this.onResize(); });\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.addEventListener(\"scroll\", function (e) { return _this.onScroll(e); });\n _this.createAnchors();\n return _this;\n }\n /**\n * Find all anchors on the current page.\n */\n MenuHighlight.prototype.createAnchors = function () {\n var _this = this;\n var base = window.location.href;\n if (base.indexOf(\"#\") != -1) {\n base = base.substr(0, base.indexOf(\"#\"));\n }\n this.el.querySelectorAll(\"a\").forEach(function (el) {\n var href = el.href;\n if (href.indexOf(\"#\") == -1)\n return;\n if (href.substr(0, base.length) != base)\n return;\n var hash = href.substr(href.indexOf(\"#\") + 1);\n var anchor = document.querySelector(\"a.tsd-anchor[name=\" + hash + \"]\");\n var link = el.parentNode;\n if (!anchor || !link)\n return;\n _this.anchors.push({\n link: link,\n anchor: anchor,\n position: 0,\n });\n });\n this.onResize();\n };\n /**\n * Triggered after the viewport was resized.\n */\n MenuHighlight.prototype.onResize = function () {\n var anchor;\n for (var index = 0, count = this.anchors.length; index < count; index++) {\n anchor = this.anchors[index];\n var rect = anchor.anchor.getBoundingClientRect();\n anchor.position = rect.top + document.body.scrollTop;\n }\n this.anchors.sort(function (a, b) {\n return a.position - b.position;\n });\n var event = new CustomEvent(\"scroll\", {\n detail: {\n scrollTop: _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.scrollTop,\n },\n });\n this.onScroll(event);\n };\n /**\n * Triggered after the viewport was scrolled.\n *\n * @param event The custom event with the current vertical scroll position.\n */\n MenuHighlight.prototype.onScroll = function (event) {\n var scrollTop = event.detail.scrollTop + 5;\n var anchors = this.anchors;\n var count = anchors.length - 1;\n var index = this.index;\n while (index > -1 && anchors[index].position > scrollTop) {\n index -= 1;\n }\n while (index < count && anchors[index + 1].position < scrollTop) {\n index += 1;\n }\n if (this.index != index) {\n if (this.index > -1)\n this.anchors[this.index].link.classList.remove(\"focus\");\n this.index = index;\n if (this.index > -1)\n this.anchors[this.index].link.classList.add(\"focus\");\n }\n };\n return MenuHighlight;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/MenuHighlight.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Search.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Search.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"initSearch\": () => /* binding */ initSearch\n/* harmony export */ });\n/* harmony import */ var _utils_debounce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../utils/debounce */ \"./default/assets/js/src/typedoc/utils/debounce.ts\");\n/* harmony import */ var lunr__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! lunr */ \"../node_modules/lunr/lunr.js\");\n/* harmony import */ var lunr__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(lunr__WEBPACK_IMPORTED_MODULE_1__);\n\n\nfunction initSearch() {\n var searchEl = document.getElementById(\"tsd-search\");\n if (!searchEl)\n return;\n var searchScript = document.getElementById(\"search-script\");\n searchEl.classList.add(\"loading\");\n if (searchScript) {\n searchScript.addEventListener(\"error\", function () {\n searchEl.classList.remove(\"loading\");\n searchEl.classList.add(\"failure\");\n });\n searchScript.addEventListener(\"load\", function () {\n searchEl.classList.remove(\"loading\");\n searchEl.classList.add(\"ready\");\n });\n if (window.searchData) {\n searchEl.classList.remove(\"loading\");\n }\n }\n var field = document.querySelector(\"#tsd-search-field\");\n var results = document.querySelector(\".results\");\n if (!field || !results) {\n throw new Error(\"The input field or the result list wrapper was not found\");\n }\n var resultClicked = false;\n results.addEventListener(\"mousedown\", function () { return (resultClicked = true); });\n results.addEventListener(\"mouseup\", function () {\n resultClicked = false;\n searchEl.classList.remove(\"has-focus\");\n });\n field.addEventListener(\"focus\", function () { return searchEl.classList.add(\"has-focus\"); });\n field.addEventListener(\"blur\", function () {\n if (!resultClicked) {\n resultClicked = false;\n searchEl.classList.remove(\"has-focus\");\n }\n });\n var state = {\n base: searchEl.dataset.base + \"/\",\n };\n bindEvents(searchEl, results, field, state);\n}\nfunction bindEvents(searchEl, results, field, state) {\n field.addEventListener(\"input\", (0,_utils_debounce__WEBPACK_IMPORTED_MODULE_0__.debounce)(function () {\n updateResults(searchEl, results, field, state);\n }, 200));\n var preventPress = false;\n field.addEventListener(\"keydown\", function (e) {\n preventPress = true;\n if (e.key == \"Enter\") {\n gotoCurrentResult(results, field);\n }\n else if (e.key == \"Escape\") {\n field.blur();\n }\n else if (e.key == \"ArrowUp\") {\n setCurrentResult(results, -1);\n }\n else if (e.key === \"ArrowDown\") {\n setCurrentResult(results, 1);\n }\n else {\n preventPress = false;\n }\n });\n field.addEventListener(\"keypress\", function (e) {\n if (preventPress)\n e.preventDefault();\n });\n /**\n * Start searching by pressing slash.\n */\n document.body.addEventListener(\"keydown\", function (e) {\n if (e.altKey || e.ctrlKey || e.metaKey)\n return;\n if (!field.matches(\":focus\") && e.key === \"/\") {\n field.focus();\n e.preventDefault();\n }\n });\n}\nfunction checkIndex(state, searchEl) {\n if (state.index)\n return;\n if (window.searchData) {\n searchEl.classList.remove(\"loading\");\n searchEl.classList.add(\"ready\");\n state.data = window.searchData;\n state.index = lunr__WEBPACK_IMPORTED_MODULE_1__.Index.load(window.searchData.index);\n }\n}\nfunction updateResults(searchEl, results, query, state) {\n checkIndex(state, searchEl);\n // Don't clear results if loading state is not ready,\n // because loading or error message can be removed.\n if (!state.index || !state.data)\n return;\n results.textContent = \"\";\n var searchText = query.value.trim();\n // Perform a wildcard search\n var res = state.index.search(\"*\" + searchText + \"*\");\n for (var i = 0, c = Math.min(10, res.length); i < c; i++) {\n var row = state.data.rows[Number(res[i].ref)];\n // Bold the matched part of the query in the search results\n var name_1 = boldMatches(row.name, searchText);\n if (row.parent) {\n name_1 = \"\" + boldMatches(row.parent, searchText) + \".\" + name_1;\n }\n var item = document.createElement(\"li\");\n item.classList.value = row.classes;\n var anchor = document.createElement(\"a\");\n anchor.href = state.base + row.url;\n anchor.classList.add(\"tsd-kind-icon\");\n anchor.innerHTML = name_1;\n item.append(anchor);\n results.appendChild(item);\n }\n}\n/**\n * Move the highlight within the result set.\n */\nfunction setCurrentResult(results, dir) {\n var current = results.querySelector(\".current\");\n if (!current) {\n current = results.querySelector(dir == 1 ? \"li:first-child\" : \"li:last-child\");\n if (current) {\n current.classList.add(\"current\");\n }\n }\n else {\n var rel = dir == 1\n ? current.nextElementSibling\n : current.previousElementSibling;\n if (rel) {\n current.classList.remove(\"current\");\n rel.classList.add(\"current\");\n }\n }\n}\n/**\n * Navigate to the highlighted result.\n */\nfunction gotoCurrentResult(results, field) {\n var current = results.querySelector(\".current\");\n if (!current) {\n current = results.querySelector(\"li:first-child\");\n }\n if (current) {\n var link = current.querySelector(\"a\");\n if (link) {\n window.location.href = link.href;\n }\n field.blur();\n }\n}\nfunction boldMatches(text, search) {\n if (search === \"\") {\n return text;\n }\n var lowerText = text.toLocaleLowerCase();\n var lowerSearch = search.toLocaleLowerCase();\n var parts = [];\n var lastIndex = 0;\n var index = lowerText.indexOf(lowerSearch);\n while (index != -1) {\n parts.push(escapeHtml(text.substring(lastIndex, index)), \"\" + escapeHtml(text.substring(index, index + lowerSearch.length)) + \"\");\n lastIndex = index + lowerSearch.length;\n index = lowerText.indexOf(lowerSearch, lastIndex);\n }\n parts.push(escapeHtml(text.substring(lastIndex)));\n return parts.join(\"\");\n}\nvar SPECIAL_HTML = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \"'\": \"'\",\n '\"': \""\",\n};\nfunction escapeHtml(text) {\n return text.replace(/[&<>\"'\"]/g, function (match) { return SPECIAL_HTML[match]; });\n}\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Search.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Signature.ts": +/*!***************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Signature.ts ***! + \***************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Signature\": () => /* binding */ Signature\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _services_Viewport__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../services/Viewport */ \"./default/assets/js/src/typedoc/services/Viewport.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\n/**\n * Holds a signature and its description.\n */\nvar SignatureGroup = /** @class */ (function () {\n /**\n * Create a new SignatureGroup instance.\n *\n * @param signature The target signature.\n * @param description The description for the signature.\n */\n function SignatureGroup(signature, description) {\n this.signature = signature;\n this.description = description;\n }\n /**\n * Add the given class to all elements of the group.\n *\n * @param className The class name to add.\n */\n SignatureGroup.prototype.addClass = function (className) {\n this.signature.classList.add(className);\n this.description.classList.add(className);\n return this;\n };\n /**\n * Remove the given class from all elements of the group.\n *\n * @param className The class name to remove.\n */\n SignatureGroup.prototype.removeClass = function (className) {\n this.signature.classList.remove(className);\n this.description.classList.remove(className);\n return this;\n };\n return SignatureGroup;\n}());\n/**\n * Controls the tab like behaviour of methods and functions with multiple signatures.\n */\nvar Signature = /** @class */ (function (_super) {\n __extends(Signature, _super);\n /**\n * Create a new Signature instance.\n *\n * @param options Backbone view constructor options.\n */\n function Signature(options) {\n var _this = _super.call(this, options) || this;\n /**\n * List of found signature groups.\n */\n _this.groups = [];\n /**\n * The index of the currently displayed signature.\n */\n _this.index = -1;\n _this.createGroups();\n if (_this.container) {\n _this.el.classList.add(\"active\");\n Array.from(_this.el.children).forEach(function (signature) {\n signature.addEventListener(\"touchstart\", function (event) {\n return _this.onClick(event);\n });\n signature.addEventListener(\"click\", function (event) {\n return _this.onClick(event);\n });\n });\n _this.container.classList.add(\"active\");\n _this.setIndex(0);\n }\n return _this;\n }\n /**\n * Set the index of the active signature.\n *\n * @param index The index of the signature to activate.\n */\n Signature.prototype.setIndex = function (index) {\n if (index < 0)\n index = 0;\n if (index > this.groups.length - 1)\n index = this.groups.length - 1;\n if (this.index == index)\n return;\n var to = this.groups[index];\n if (this.index > -1) {\n var from_1 = this.groups[this.index];\n from_1.removeClass(\"current\").addClass(\"fade-out\");\n to.addClass(\"current\");\n to.addClass(\"fade-in\");\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.triggerResize();\n setTimeout(function () {\n from_1.removeClass(\"fade-out\");\n to.removeClass(\"fade-in\");\n }, 300);\n }\n else {\n to.addClass(\"current\");\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.triggerResize();\n }\n this.index = index;\n };\n /**\n * Find all signature/description groups.\n */\n Signature.prototype.createGroups = function () {\n var signatures = this.el.children;\n if (signatures.length < 2)\n return;\n this.container = this.el.nextElementSibling;\n var descriptions = this.container.children;\n this.groups = [];\n for (var index = 0; index < signatures.length; index++) {\n this.groups.push(new SignatureGroup(signatures[index], descriptions[index]));\n }\n };\n /**\n * Triggered when the user clicks onto a signature header.\n *\n * @param e The related event object.\n */\n Signature.prototype.onClick = function (e) {\n var _this = this;\n this.groups.forEach(function (group, index) {\n if (group.signature === e.currentTarget) {\n _this.setIndex(index);\n }\n });\n };\n return Signature;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Signature.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Toggle.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Toggle.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Toggle\": () => /* binding */ Toggle\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _utils_pointer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils/pointer */ \"./default/assets/js/src/typedoc/utils/pointer.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\nvar Toggle = /** @class */ (function (_super) {\n __extends(Toggle, _super);\n function Toggle(options) {\n var _this = _super.call(this, options) || this;\n _this.className = _this.el.dataset.toggle || \"\";\n _this.el.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerUp, function (e) { return _this.onPointerUp(e); });\n _this.el.addEventListener(\"click\", function (e) { return e.preventDefault(); });\n document.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerDown, function (e) {\n return _this.onDocumentPointerDown(e);\n });\n document.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerUp, function (e) {\n return _this.onDocumentPointerUp(e);\n });\n return _this;\n }\n Toggle.prototype.setActive = function (value) {\n if (this.active == value)\n return;\n this.active = value;\n document.documentElement.classList.toggle(\"has-\" + this.className, value);\n this.el.classList.toggle(\"active\", value);\n var transition = (this.active ? \"to-has-\" : \"from-has-\") + this.className;\n document.documentElement.classList.add(transition);\n setTimeout(function () { return document.documentElement.classList.remove(transition); }, 500);\n };\n Toggle.prototype.onPointerUp = function (event) {\n if (_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.hasPointerMoved)\n return;\n this.setActive(true);\n event.preventDefault();\n };\n Toggle.prototype.onDocumentPointerDown = function (e) {\n if (this.active) {\n if (e.target.closest(\".col-menu, .tsd-filter-group\")) {\n return;\n }\n this.setActive(false);\n }\n };\n Toggle.prototype.onDocumentPointerUp = function (e) {\n var _this = this;\n if (_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.hasPointerMoved)\n return;\n if (this.active) {\n if (e.target.closest(\".col-menu\")) {\n var link = e.target.closest(\"a\");\n if (link) {\n var href = window.location.href;\n if (href.indexOf(\"#\") != -1) {\n href = href.substr(0, href.indexOf(\"#\"));\n }\n if (link.href.substr(0, href.length) == href) {\n setTimeout(function () { return _this.setActive(false); }, 250);\n }\n }\n }\n }\n };\n return Toggle;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Toggle.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/services/Viewport.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/services/Viewport.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Viewport\": () => /* binding */ Viewport\n/* harmony export */ });\n/* harmony import */ var _EventTarget__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../EventTarget */ \"./default/assets/js/src/typedoc/EventTarget.ts\");\n/* harmony import */ var _utils_trottle__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils/trottle */ \"./default/assets/js/src/typedoc/utils/trottle.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\n/**\n * A global service that monitors the window size and scroll position.\n */\nvar Viewport = /** @class */ (function (_super) {\n __extends(Viewport, _super);\n /**\n * Create new Viewport instance.\n */\n function Viewport() {\n var _this = _super.call(this) || this;\n /**\n * The current scroll position.\n */\n _this.scrollTop = 0;\n /**\n * The previous scrollTop.\n */\n _this.lastY = 0;\n /**\n * The width of the window.\n */\n _this.width = 0;\n /**\n * The height of the window.\n */\n _this.height = 0;\n /**\n * Boolean indicating whether the toolbar is shown.\n */\n _this.showToolbar = true;\n _this.toolbar = (document.querySelector(\".tsd-page-toolbar\"));\n _this.secondaryNav = (document.querySelector(\".tsd-navigation.secondary\"));\n window.addEventListener(\"scroll\", (0,_utils_trottle__WEBPACK_IMPORTED_MODULE_1__.throttle)(function () { return _this.onScroll(); }, 10));\n window.addEventListener(\"resize\", (0,_utils_trottle__WEBPACK_IMPORTED_MODULE_1__.throttle)(function () { return _this.onResize(); }, 10));\n _this.onResize();\n _this.onScroll();\n return _this;\n }\n /**\n * Trigger a resize event.\n */\n Viewport.prototype.triggerResize = function () {\n var event = new CustomEvent(\"resize\", {\n detail: {\n width: this.width,\n height: this.height,\n },\n });\n this.dispatchEvent(event);\n };\n /**\n * Triggered when the size of the window has changed.\n */\n Viewport.prototype.onResize = function () {\n this.width = window.innerWidth || 0;\n this.height = window.innerHeight || 0;\n var event = new CustomEvent(\"resize\", {\n detail: {\n width: this.width,\n height: this.height,\n },\n });\n this.dispatchEvent(event);\n };\n /**\n * Triggered when the user scrolled the viewport.\n */\n Viewport.prototype.onScroll = function () {\n this.scrollTop = window.scrollY || 0;\n var event = new CustomEvent(\"scroll\", {\n detail: {\n scrollTop: this.scrollTop,\n },\n });\n this.dispatchEvent(event);\n this.hideShowToolbar();\n };\n /**\n * Handle hiding/showing of the toolbar.\n */\n Viewport.prototype.hideShowToolbar = function () {\n var isShown = this.showToolbar;\n this.showToolbar = this.lastY >= this.scrollTop || this.scrollTop <= 0;\n if (isShown !== this.showToolbar) {\n this.toolbar.classList.toggle(\"tsd-page-toolbar--hide\");\n this.secondaryNav.classList.toggle(\"tsd-navigation--toolbar-hide\");\n }\n this.lastY = this.scrollTop;\n };\n Viewport.instance = new Viewport();\n return Viewport;\n}(_EventTarget__WEBPACK_IMPORTED_MODULE_0__.EventTarget));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/services/Viewport.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/utils/debounce.ts": +/*!*********************************************************!*\ + !*** ./default/assets/js/src/typedoc/utils/debounce.ts ***! + \*********************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"debounce\": () => /* binding */ debounce\n/* harmony export */ });\nvar debounce = function (fn, wait) {\n if (wait === void 0) { wait = 100; }\n var timeout;\n return function () {\n var args = [];\n for (var _i = 0; _i < arguments.length; _i++) {\n args[_i] = arguments[_i];\n }\n clearTimeout(timeout);\n timeout = setTimeout(function () { return fn(args); }, wait);\n };\n};\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/utils/debounce.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/utils/pointer.ts": +/*!********************************************************!*\ + !*** ./default/assets/js/src/typedoc/utils/pointer.ts ***! + \********************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"pointerDown\": () => /* binding */ pointerDown,\n/* harmony export */ \"pointerMove\": () => /* binding */ pointerMove,\n/* harmony export */ \"pointerUp\": () => /* binding */ pointerUp,\n/* harmony export */ \"pointerDownPosition\": () => /* binding */ pointerDownPosition,\n/* harmony export */ \"preventNextClick\": () => /* binding */ preventNextClick,\n/* harmony export */ \"isPointerDown\": () => /* binding */ isPointerDown,\n/* harmony export */ \"isPointerTouch\": () => /* binding */ isPointerTouch,\n/* harmony export */ \"hasPointerMoved\": () => /* binding */ hasPointerMoved,\n/* harmony export */ \"isMobile\": () => /* binding */ isMobile\n/* harmony export */ });\n/**\n * Event name of the pointer down event.\n */\nvar pointerDown = \"mousedown\";\n/**\n * Event name of the pointer move event.\n */\nvar pointerMove = \"mousemove\";\n/**\n * Event name of the pointer up event.\n */\nvar pointerUp = \"mouseup\";\n/**\n * Position the pointer was pressed at.\n */\nvar pointerDownPosition = { x: 0, y: 0 };\n/**\n * Should the next click on the document be supressed?\n */\nvar preventNextClick = false;\n/**\n * Is the pointer down?\n */\nvar isPointerDown = false;\n/**\n * Is the pointer a touch point?\n */\nvar isPointerTouch = false;\n/**\n * Did the pointer move since the last down event?\n */\nvar hasPointerMoved = false;\n/**\n * Is the user agent a mobile agent?\n */\nvar isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\ndocument.documentElement.classList.add(isMobile ? \"is-mobile\" : \"not-mobile\");\nif (isMobile && \"ontouchstart\" in document.documentElement) {\n isPointerTouch = true;\n pointerDown = \"touchstart\";\n pointerMove = \"touchmove\";\n pointerUp = \"touchend\";\n}\ndocument.addEventListener(pointerDown, function (e) {\n isPointerDown = true;\n hasPointerMoved = false;\n var t = pointerDown == \"touchstart\"\n ? e.targetTouches[0]\n : e;\n pointerDownPosition.y = t.pageY || 0;\n pointerDownPosition.x = t.pageX || 0;\n});\ndocument.addEventListener(pointerMove, function (e) {\n if (!isPointerDown)\n return;\n if (!hasPointerMoved) {\n var t = pointerDown == \"touchstart\"\n ? e.targetTouches[0]\n : e;\n var x = pointerDownPosition.x - (t.pageX || 0);\n var y = pointerDownPosition.y - (t.pageY || 0);\n hasPointerMoved = Math.sqrt(x * x + y * y) > 10;\n }\n});\ndocument.addEventListener(pointerUp, function () {\n isPointerDown = false;\n});\ndocument.addEventListener(\"click\", function (e) {\n if (preventNextClick) {\n e.preventDefault();\n e.stopImmediatePropagation();\n preventNextClick = false;\n }\n});\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/utils/pointer.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/utils/trottle.ts": +/*!********************************************************!*\ + !*** ./default/assets/js/src/typedoc/utils/trottle.ts ***! + \********************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"throttle\": () => /* binding */ throttle\n/* harmony export */ });\nvar throttle = function (fn, wait) {\n if (wait === void 0) { wait = 100; }\n var time = Date.now();\n return function () {\n var args = [];\n for (var _i = 0; _i < arguments.length; _i++) {\n args[_i] = arguments[_i];\n }\n if (time + wait - Date.now() < 0) {\n fn.apply(void 0, args);\n time = Date.now();\n }\n };\n};\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/utils/trottle.ts?"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ if(__webpack_module_cache__[moduleId]) { +/******/ return __webpack_module_cache__[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => module['default'] : +/******/ () => module; +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +/******/ // startup +/******/ // Load entry module +/******/ __webpack_require__("./default/assets/js/src/bootstrap.ts"); +/******/ // This entry module used 'exports' so it can't be inlined +/******/ })() +; \ No newline at end of file diff --git a/docs/assets/js/search.js b/docs/assets/js/search.js new file mode 100644 index 000000000..9cac81346 --- /dev/null +++ b/docs/assets/js/search.js @@ -0,0 +1 @@ +window.searchData = {"kinds":{"1":"Module","4":"Enumeration","16":"Enumeration member","64":"Function","128":"Class","256":"Interface","512":"Constructor","1024":"Property","65536":"Type literal","4194304":"Type alias"},"rows":[{"id":0,"kind":1,"name":"SelfServe","url":"modules/selfserve.html","classes":"tsd-kind-module"},{"id":1,"kind":1,"name":"SelfServe - What is currently supported?","url":"modules/selfserve___what_is_currently_supported_.html","classes":"tsd-kind-module"},{"id":2,"kind":1,"name":"SelfServe/Decorators","url":"modules/selfserve_decorators.html","classes":"tsd-kind-module"},{"id":3,"kind":256,"name":"NumberInputOptions","url":"interfaces/selfserve_decorators.numberinputoptions.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":4,"kind":1024,"name":"min","url":"interfaces/selfserve_decorators.numberinputoptions.html#min","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.NumberInputOptions"},{"id":5,"kind":1024,"name":"max","url":"interfaces/selfserve_decorators.numberinputoptions.html#max","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.NumberInputOptions"},{"id":6,"kind":1024,"name":"step","url":"interfaces/selfserve_decorators.numberinputoptions.html#step","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.NumberInputOptions"},{"id":7,"kind":1024,"name":"uiType","url":"interfaces/selfserve_decorators.numberinputoptions.html#uitype","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.NumberInputOptions"},{"id":8,"kind":1024,"name":"labelTKey","url":"interfaces/selfserve_decorators.numberinputoptions.html#labeltkey","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"SelfServe/Decorators.NumberInputOptions"},{"id":9,"kind":256,"name":"StringInputOptions","url":"interfaces/selfserve_decorators.stringinputoptions.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":10,"kind":1024,"name":"placeholderTKey","url":"interfaces/selfserve_decorators.stringinputoptions.html#placeholdertkey","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.StringInputOptions"},{"id":11,"kind":1024,"name":"labelTKey","url":"interfaces/selfserve_decorators.stringinputoptions.html#labeltkey","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"SelfServe/Decorators.StringInputOptions"},{"id":12,"kind":256,"name":"BooleanInputOptions","url":"interfaces/selfserve_decorators.booleaninputoptions.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":13,"kind":1024,"name":"trueLabelTKey","url":"interfaces/selfserve_decorators.booleaninputoptions.html#truelabeltkey","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.BooleanInputOptions"},{"id":14,"kind":1024,"name":"falseLabelTKey","url":"interfaces/selfserve_decorators.booleaninputoptions.html#falselabeltkey","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.BooleanInputOptions"},{"id":15,"kind":1024,"name":"labelTKey","url":"interfaces/selfserve_decorators.booleaninputoptions.html#labeltkey","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"SelfServe/Decorators.BooleanInputOptions"},{"id":16,"kind":256,"name":"ChoiceInputOptions","url":"interfaces/selfserve_decorators.choiceinputoptions.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":17,"kind":1024,"name":"choices","url":"interfaces/selfserve_decorators.choiceinputoptions.html#choices","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.ChoiceInputOptions"},{"id":18,"kind":1024,"name":"placeholderTKey","url":"interfaces/selfserve_decorators.choiceinputoptions.html#placeholdertkey","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.ChoiceInputOptions"},{"id":19,"kind":1024,"name":"labelTKey","url":"interfaces/selfserve_decorators.choiceinputoptions.html#labeltkey","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"SelfServe/Decorators.ChoiceInputOptions"},{"id":20,"kind":256,"name":"DescriptionDisplayOptions","url":"interfaces/selfserve_decorators.descriptiondisplayoptions.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":21,"kind":1024,"name":"labelTKey","url":"interfaces/selfserve_decorators.descriptiondisplayoptions.html#labeltkey","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.DescriptionDisplayOptions"},{"id":22,"kind":1024,"name":"description","url":"interfaces/selfserve_decorators.descriptiondisplayoptions.html#description","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.DescriptionDisplayOptions"},{"id":23,"kind":1024,"name":"isDynamicDescription","url":"interfaces/selfserve_decorators.descriptiondisplayoptions.html#isdynamicdescription","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/Decorators.DescriptionDisplayOptions"},{"id":24,"kind":4194304,"name":"InputOptions","url":"modules/selfserve_decorators.html#inputoptions","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":25,"kind":64,"name":"OnChange","url":"modules/selfserve_decorators.html#onchange","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":26,"kind":64,"name":"PropertyInfo","url":"modules/selfserve_decorators.html#propertyinfo","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":27,"kind":64,"name":"Values","url":"modules/selfserve_decorators.html#values","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":28,"kind":64,"name":"IsDisplayable","url":"modules/selfserve_decorators.html#isdisplayable","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":29,"kind":64,"name":"RefreshOptions","url":"modules/selfserve_decorators.html#refreshoptions","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/Decorators"},{"id":30,"kind":1,"name":"SelfServe/SelfServeTypes","url":"modules/selfserve_selfservetypes.html","classes":"tsd-kind-module"},{"id":31,"kind":4194304,"name":"initializeCallback","url":"modules/selfserve_selfservetypes.html#initializecallback","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":32,"kind":65536,"name":"__type","url":"modules/selfserve_selfservetypes.html#initializecallback.__type-2","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"SelfServe/SelfServeTypes.initializeCallback"},{"id":33,"kind":4194304,"name":"onSaveCallback","url":"modules/selfserve_selfservetypes.html#onsavecallback","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":34,"kind":65536,"name":"__type","url":"modules/selfserve_selfservetypes.html#onsavecallback.__type-3","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"SelfServe/SelfServeTypes.onSaveCallback"},{"id":35,"kind":128,"name":"SelfServeBaseClass","url":"classes/selfserve_selfservetypes.selfservebaseclass.html","classes":"tsd-kind-class tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":36,"kind":512,"name":"constructor","url":"classes/selfserve_selfservetypes.selfservebaseclass.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"SelfServe/SelfServeTypes.SelfServeBaseClass"},{"id":37,"kind":1024,"name":"initialize","url":"classes/selfserve_selfservetypes.selfservebaseclass.html#initialize","classes":"tsd-kind-property tsd-parent-kind-class","parent":"SelfServe/SelfServeTypes.SelfServeBaseClass"},{"id":38,"kind":1024,"name":"onSave","url":"classes/selfserve_selfservetypes.selfservebaseclass.html#onsave","classes":"tsd-kind-property tsd-parent-kind-class","parent":"SelfServe/SelfServeTypes.SelfServeBaseClass"},{"id":39,"kind":1024,"name":"onRefresh","url":"classes/selfserve_selfservetypes.selfservebaseclass.html#onrefresh","classes":"tsd-kind-property tsd-parent-kind-class","parent":"SelfServe/SelfServeTypes.SelfServeBaseClass"},{"id":40,"kind":65536,"name":"__type","url":"classes/selfserve_selfservetypes.selfservebaseclass.html#__type","classes":"tsd-kind-type-literal tsd-parent-kind-class","parent":"SelfServe/SelfServeTypes.SelfServeBaseClass"},{"id":41,"kind":4194304,"name":"OnChangeCallback","url":"modules/selfserve_selfservetypes.html#onchangecallback","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":42,"kind":65536,"name":"__type","url":"modules/selfserve_selfservetypes.html#onchangecallback.__type-1","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"SelfServe/SelfServeTypes.OnChangeCallback"},{"id":43,"kind":4,"name":"NumberUiType","url":"enums/selfserve_selfservetypes.numberuitype.html","classes":"tsd-kind-enum tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":44,"kind":16,"name":"Spinner","url":"enums/selfserve_selfservetypes.numberuitype.html#spinner","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeTypes.NumberUiType"},{"id":45,"kind":16,"name":"Slider","url":"enums/selfserve_selfservetypes.numberuitype.html#slider","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeTypes.NumberUiType"},{"id":46,"kind":4194304,"name":"ChoiceItem","url":"modules/selfserve_selfservetypes.html#choiceitem","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":47,"kind":65536,"name":"__type","url":"modules/selfserve_selfservetypes.html#choiceitem.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"SelfServe/SelfServeTypes.ChoiceItem"},{"id":48,"kind":1024,"name":"labelTKey","url":"modules/selfserve_selfservetypes.html#choiceitem.__type.labeltkey","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.ChoiceItem.__type"},{"id":49,"kind":1024,"name":"key","url":"modules/selfserve_selfservetypes.html#choiceitem.__type.key","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.ChoiceItem.__type"},{"id":50,"kind":4194304,"name":"InputType","url":"modules/selfserve_selfservetypes.html#inputtype","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":51,"kind":256,"name":"Info","url":"interfaces/selfserve_selfservetypes.info.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":52,"kind":1024,"name":"messageTKey","url":"interfaces/selfserve_selfservetypes.info.html#messagetkey","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.Info"},{"id":53,"kind":1024,"name":"link","url":"interfaces/selfserve_selfservetypes.info.html#link","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.Info"},{"id":54,"kind":65536,"name":"__type","url":"interfaces/selfserve_selfservetypes.info.html#__type","classes":"tsd-kind-type-literal tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.Info"},{"id":55,"kind":1024,"name":"href","url":"interfaces/selfserve_selfservetypes.info.html#__type.href","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.Info.__type"},{"id":56,"kind":1024,"name":"textTKey","url":"interfaces/selfserve_selfservetypes.info.html#__type.texttkey","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.Info.__type"},{"id":57,"kind":4,"name":"DescriptionType","url":"enums/selfserve_selfservetypes.descriptiontype.html","classes":"tsd-kind-enum tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":58,"kind":16,"name":"Text","url":"enums/selfserve_selfservetypes.descriptiontype.html#text","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeTypes.DescriptionType"},{"id":59,"kind":16,"name":"InfoMessageBar","url":"enums/selfserve_selfservetypes.descriptiontype.html#infomessagebar","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeTypes.DescriptionType"},{"id":60,"kind":16,"name":"WarningMessageBar","url":"enums/selfserve_selfservetypes.descriptiontype.html#warningmessagebar","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeTypes.DescriptionType"},{"id":61,"kind":256,"name":"Description","url":"interfaces/selfserve_selfservetypes.description.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":62,"kind":1024,"name":"textTKey","url":"interfaces/selfserve_selfservetypes.description.html#texttkey-1","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.Description"},{"id":63,"kind":1024,"name":"type","url":"interfaces/selfserve_selfservetypes.description.html#type","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.Description"},{"id":64,"kind":1024,"name":"link","url":"interfaces/selfserve_selfservetypes.description.html#link","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.Description"},{"id":65,"kind":65536,"name":"__type","url":"interfaces/selfserve_selfservetypes.description.html#__type","classes":"tsd-kind-type-literal tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.Description"},{"id":66,"kind":1024,"name":"href","url":"interfaces/selfserve_selfservetypes.description.html#__type.href","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.Description.__type"},{"id":67,"kind":1024,"name":"textTKey","url":"interfaces/selfserve_selfservetypes.description.html#__type.texttkey","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.Description.__type"},{"id":68,"kind":256,"name":"SmartUiInput","url":"interfaces/selfserve_selfservetypes.smartuiinput.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":69,"kind":1024,"name":"value","url":"interfaces/selfserve_selfservetypes.smartuiinput.html#value","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.SmartUiInput"},{"id":70,"kind":1024,"name":"hidden","url":"interfaces/selfserve_selfservetypes.smartuiinput.html#hidden","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.SmartUiInput"},{"id":71,"kind":1024,"name":"disabled","url":"interfaces/selfserve_selfservetypes.smartuiinput.html#disabled","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.SmartUiInput"},{"id":72,"kind":256,"name":"OnSaveResult","url":"interfaces/selfserve_selfservetypes.onsaveresult.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":73,"kind":1024,"name":"operationStatusUrl","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#operationstatusurl","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.OnSaveResult"},{"id":74,"kind":1024,"name":"portalNotification","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#portalnotification","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.OnSaveResult"},{"id":75,"kind":65536,"name":"__type","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type","classes":"tsd-kind-type-literal tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.OnSaveResult"},{"id":76,"kind":1024,"name":"initialize","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.initialize","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type"},{"id":77,"kind":65536,"name":"__type","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-2","classes":"tsd-kind-type-literal tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type"},{"id":78,"kind":1024,"name":"titleTKey","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-2.titletkey-1","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type.__type"},{"id":79,"kind":1024,"name":"messageTKey","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-2.messagetkey-1","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type.__type"},{"id":80,"kind":1024,"name":"success","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.success","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type"},{"id":81,"kind":65536,"name":"__type","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-3","classes":"tsd-kind-type-literal tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type"},{"id":82,"kind":1024,"name":"titleTKey","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-3.titletkey-2","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type.__type"},{"id":83,"kind":1024,"name":"messageTKey","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-3.messagetkey-2","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type.__type"},{"id":84,"kind":1024,"name":"failure","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.failure","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type"},{"id":85,"kind":65536,"name":"__type","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-1","classes":"tsd-kind-type-literal tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type"},{"id":86,"kind":1024,"name":"titleTKey","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-1.titletkey","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type.__type"},{"id":87,"kind":1024,"name":"messageTKey","url":"interfaces/selfserve_selfservetypes.onsaveresult.html#__type.__type-1.messagetkey","classes":"tsd-kind-property tsd-parent-kind-type-literal","parent":"SelfServe/SelfServeTypes.OnSaveResult.__type.__type"},{"id":88,"kind":256,"name":"RefreshResult","url":"interfaces/selfserve_selfservetypes.refreshresult.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":89,"kind":1024,"name":"isUpdateInProgress","url":"interfaces/selfserve_selfservetypes.refreshresult.html#isupdateinprogress","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.RefreshResult"},{"id":90,"kind":1024,"name":"updateInProgressMessageTKey","url":"interfaces/selfserve_selfservetypes.refreshresult.html#updateinprogressmessagetkey","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.RefreshResult"},{"id":91,"kind":256,"name":"RefreshParams","url":"interfaces/selfserve_selfservetypes.refreshparams.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":92,"kind":1024,"name":"retryIntervalInMs","url":"interfaces/selfserve_selfservetypes.refreshparams.html#retryintervalinms","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.RefreshParams"},{"id":93,"kind":256,"name":"SelfServeTelemetryMessage","url":"interfaces/selfserve_selfservetypes.selfservetelemetrymessage.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"SelfServe/SelfServeTypes"},{"id":94,"kind":1024,"name":"selfServeClassName","url":"interfaces/selfserve_selfservetypes.selfservetelemetrymessage.html#selfserveclassname","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"SelfServe/SelfServeTypes.SelfServeTelemetryMessage"},{"id":95,"kind":1,"name":"SelfServe/SelfServeUtils","url":"modules/selfserve_selfserveutils.html","classes":"tsd-kind-module"},{"id":96,"kind":4,"name":"SelfServeType","url":"enums/selfserve_selfserveutils.selfservetype.html","classes":"tsd-kind-enum tsd-parent-kind-module","parent":"SelfServe/SelfServeUtils"},{"id":97,"kind":16,"name":"invalid","url":"enums/selfserve_selfserveutils.selfservetype.html#invalid","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.SelfServeType"},{"id":98,"kind":16,"name":"example","url":"enums/selfserve_selfserveutils.selfservetype.html#example","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.SelfServeType"},{"id":99,"kind":16,"name":"sqlx","url":"enums/selfserve_selfserveutils.selfservetype.html#sqlx","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.SelfServeType"},{"id":100,"kind":4,"name":"BladeType","url":"enums/selfserve_selfserveutils.bladetype.html","classes":"tsd-kind-enum tsd-parent-kind-module","parent":"SelfServe/SelfServeUtils"},{"id":101,"kind":16,"name":"SqlKeys","url":"enums/selfserve_selfserveutils.bladetype.html#sqlkeys","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.BladeType"},{"id":102,"kind":16,"name":"MongoKeys","url":"enums/selfserve_selfserveutils.bladetype.html#mongokeys","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.BladeType"},{"id":103,"kind":16,"name":"CassandraKeys","url":"enums/selfserve_selfserveutils.bladetype.html#cassandrakeys","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.BladeType"},{"id":104,"kind":16,"name":"GremlinKeys","url":"enums/selfserve_selfserveutils.bladetype.html#gremlinkeys","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.BladeType"},{"id":105,"kind":16,"name":"TableKeys","url":"enums/selfserve_selfserveutils.bladetype.html#tablekeys","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.BladeType"},{"id":106,"kind":16,"name":"Metrics","url":"enums/selfserve_selfserveutils.bladetype.html#metrics","classes":"tsd-kind-enum-member tsd-parent-kind-enum","parent":"SelfServe/SelfServeUtils.BladeType"},{"id":107,"kind":64,"name":"generateBladeLink","url":"modules/selfserve_selfserveutils.html#generatebladelink","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/SelfServeUtils"},{"id":108,"kind":1,"name":"SelfServe/SelfServeTelemetryProcessor","url":"modules/selfserve_selfservetelemetryprocessor.html","classes":"tsd-kind-module"},{"id":109,"kind":64,"name":"selfServeTrace","url":"modules/selfserve_selfservetelemetryprocessor.html#selfservetrace","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/SelfServeTelemetryProcessor"},{"id":110,"kind":64,"name":"selfServeTraceStart","url":"modules/selfserve_selfservetelemetryprocessor.html#selfservetracestart","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/SelfServeTelemetryProcessor"},{"id":111,"kind":64,"name":"selfServeTraceSuccess","url":"modules/selfserve_selfservetelemetryprocessor.html#selfservetracesuccess","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/SelfServeTelemetryProcessor"},{"id":112,"kind":64,"name":"selfServeTraceFailure","url":"modules/selfserve_selfservetelemetryprocessor.html#selfservetracefailure","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/SelfServeTelemetryProcessor"},{"id":113,"kind":64,"name":"selfServeTraceCancel","url":"modules/selfserve_selfservetelemetryprocessor.html#selfservetracecancel","classes":"tsd-kind-function tsd-parent-kind-module","parent":"SelfServe/SelfServeTelemetryProcessor"}],"index":{"version":"2.3.9","fields":["name","parent"],"fieldVectors":[["name/0",[0,38.825]],["parent/0",[]],["name/1",[0,14.915,1,16.905,2,16.905,3,16.905,4,16.905]],["parent/1",[]],["name/2",[5,22.504]],["parent/2",[]],["name/3",[6,44.005]],["parent/3",[5,2.17]],["name/4",[7,44.005]],["parent/4",[8,2.973]],["name/5",[9,44.005]],["parent/5",[8,2.973]],["name/6",[10,44.005]],["parent/6",[8,2.973]],["name/7",[11,44.005]],["parent/7",[8,2.973]],["name/8",[12,29.135]],["parent/8",[8,2.973]],["name/9",[13,44.005]],["parent/9",[5,2.17]],["name/10",[14,38.825]],["parent/10",[15,3.744]],["name/11",[12,29.135]],["parent/11",[15,3.744]],["name/12",[16,44.005]],["parent/12",[5,2.17]],["name/13",[17,44.005]],["parent/13",[18,3.415]],["name/14",[19,44.005]],["parent/14",[18,3.415]],["name/15",[12,29.135]],["parent/15",[18,3.415]],["name/16",[20,44.005]],["parent/16",[5,2.17]],["name/17",[21,44.005]],["parent/17",[22,3.415]],["name/18",[14,38.825]],["parent/18",[22,3.415]],["name/19",[12,29.135]],["parent/19",[22,3.415]],["name/20",[23,44.005]],["parent/20",[5,2.17]],["name/21",[12,29.135]],["parent/21",[24,3.415]],["name/22",[25,38.825]],["parent/22",[24,3.415]],["name/23",[26,44.005]],["parent/23",[24,3.415]],["name/24",[27,44.005]],["parent/24",[5,2.17]],["name/25",[28,44.005]],["parent/25",[5,2.17]],["name/26",[29,44.005]],["parent/26",[5,2.17]],["name/27",[30,44.005]],["parent/27",[5,2.17]],["name/28",[31,44.005]],["parent/28",[5,2.17]],["name/29",[32,44.005]],["parent/29",[5,2.17]],["name/30",[33,19.689]],["parent/30",[]],["name/31",[34,44.005]],["parent/31",[33,1.898]],["name/32",[35,23.35]],["parent/32",[36,4.243]],["name/33",[37,44.005]],["parent/33",[33,1.898]],["name/34",[35,23.35]],["parent/34",[38,4.243]],["name/35",[39,44.005]],["parent/35",[33,1.898]],["name/36",[40,44.005]],["parent/36",[41,2.973]],["name/37",[42,38.825]],["parent/37",[41,2.973]],["name/38",[43,44.005]],["parent/38",[41,2.973]],["name/39",[44,44.005]],["parent/39",[41,2.973]],["name/40",[35,23.35]],["parent/40",[41,2.973]],["name/41",[45,44.005]],["parent/41",[33,1.898]],["name/42",[35,23.35]],["parent/42",[46,4.243]],["name/43",[47,44.005]],["parent/43",[33,1.898]],["name/44",[48,44.005]],["parent/44",[49,3.744]],["name/45",[50,44.005]],["parent/45",[49,3.744]],["name/46",[51,44.005]],["parent/46",[33,1.898]],["name/47",[35,23.35]],["parent/47",[52,4.243]],["name/48",[12,29.135]],["parent/48",[53,3.744]],["name/49",[54,44.005]],["parent/49",[53,3.744]],["name/50",[55,44.005]],["parent/50",[33,1.898]],["name/51",[56,44.005]],["parent/51",[33,1.898]],["name/52",[57,32.864]],["parent/52",[58,3.415]],["name/53",[59,38.825]],["parent/53",[58,3.415]],["name/54",[35,23.35]],["parent/54",[58,3.415]],["name/55",[60,38.825]],["parent/55",[61,3.744]],["name/56",[62,35.413]],["parent/56",[61,3.744]],["name/57",[63,44.005]],["parent/57",[33,1.898]],["name/58",[64,44.005]],["parent/58",[65,3.415]],["name/59",[66,44.005]],["parent/59",[65,3.415]],["name/60",[67,44.005]],["parent/60",[65,3.415]],["name/61",[25,38.825]],["parent/61",[33,1.898]],["name/62",[62,35.413]],["parent/62",[68,3.169]],["name/63",[69,44.005]],["parent/63",[68,3.169]],["name/64",[59,38.825]],["parent/64",[68,3.169]],["name/65",[35,23.35]],["parent/65",[68,3.169]],["name/66",[60,38.825]],["parent/66",[70,3.744]],["name/67",[62,35.413]],["parent/67",[70,3.744]],["name/68",[71,44.005]],["parent/68",[33,1.898]],["name/69",[72,44.005]],["parent/69",[73,3.415]],["name/70",[74,44.005]],["parent/70",[73,3.415]],["name/71",[75,44.005]],["parent/71",[73,3.415]],["name/72",[76,44.005]],["parent/72",[33,1.898]],["name/73",[77,44.005]],["parent/73",[78,3.415]],["name/74",[79,44.005]],["parent/74",[78,3.415]],["name/75",[35,23.35]],["parent/75",[78,3.415]],["name/76",[42,38.825]],["parent/76",[80,2.809]],["name/77",[35,23.35]],["parent/77",[80,2.809]],["name/78",[81,35.413]],["parent/78",[82,2.809]],["name/79",[57,32.864]],["parent/79",[82,2.809]],["name/80",[83,44.005]],["parent/80",[80,2.809]],["name/81",[35,23.35]],["parent/81",[80,2.809]],["name/82",[81,35.413]],["parent/82",[82,2.809]],["name/83",[57,32.864]],["parent/83",[82,2.809]],["name/84",[84,44.005]],["parent/84",[80,2.809]],["name/85",[35,23.35]],["parent/85",[80,2.809]],["name/86",[81,35.413]],["parent/86",[82,2.809]],["name/87",[57,32.864]],["parent/87",[82,2.809]],["name/88",[85,44.005]],["parent/88",[33,1.898]],["name/89",[86,44.005]],["parent/89",[87,3.744]],["name/90",[88,44.005]],["parent/90",[87,3.744]],["name/91",[89,44.005]],["parent/91",[33,1.898]],["name/92",[90,44.005]],["parent/92",[91,4.243]],["name/93",[92,44.005]],["parent/93",[33,1.898]],["name/94",[93,44.005]],["parent/94",[94,4.243]],["name/95",[95,32.864]],["parent/95",[]],["name/96",[96,44.005]],["parent/96",[95,3.169]],["name/97",[97,44.005]],["parent/97",[98,3.415]],["name/98",[99,44.005]],["parent/98",[98,3.415]],["name/99",[100,44.005]],["parent/99",[98,3.415]],["name/100",[101,44.005]],["parent/100",[95,3.169]],["name/101",[102,44.005]],["parent/101",[103,2.809]],["name/102",[104,44.005]],["parent/102",[103,2.809]],["name/103",[105,44.005]],["parent/103",[103,2.809]],["name/104",[106,44.005]],["parent/104",[103,2.809]],["name/105",[107,44.005]],["parent/105",[103,2.809]],["name/106",[108,44.005]],["parent/106",[103,2.809]],["name/107",[109,44.005]],["parent/107",[95,3.169]],["name/108",[110,29.135]],["parent/108",[]],["name/109",[111,44.005]],["parent/109",[110,2.809]],["name/110",[112,44.005]],["parent/110",[110,2.809]],["name/111",[113,44.005]],["parent/111",[110,2.809]],["name/112",[114,44.005]],["parent/112",[110,2.809]],["name/113",[115,44.005]],["parent/113",[110,2.809]]],"invertedIndex":[["__type",{"_index":35,"name":{"32":{},"34":{},"40":{},"42":{},"47":{},"54":{},"65":{},"75":{},"77":{},"81":{},"85":{}},"parent":{}}],["bladetype",{"_index":101,"name":{"100":{}},"parent":{}}],["booleaninputoptions",{"_index":16,"name":{"12":{}},"parent":{}}],["cassandrakeys",{"_index":105,"name":{"103":{}},"parent":{}}],["choiceinputoptions",{"_index":20,"name":{"16":{}},"parent":{}}],["choiceitem",{"_index":51,"name":{"46":{}},"parent":{}}],["choices",{"_index":21,"name":{"17":{}},"parent":{}}],["constructor",{"_index":40,"name":{"36":{}},"parent":{}}],["currently",{"_index":3,"name":{"1":{}},"parent":{}}],["description",{"_index":25,"name":{"22":{},"61":{}},"parent":{}}],["descriptiondisplayoptions",{"_index":23,"name":{"20":{}},"parent":{}}],["descriptiontype",{"_index":63,"name":{"57":{}},"parent":{}}],["disabled",{"_index":75,"name":{"71":{}},"parent":{}}],["example",{"_index":99,"name":{"98":{}},"parent":{}}],["failure",{"_index":84,"name":{"84":{}},"parent":{}}],["falselabeltkey",{"_index":19,"name":{"14":{}},"parent":{}}],["generatebladelink",{"_index":109,"name":{"107":{}},"parent":{}}],["gremlinkeys",{"_index":106,"name":{"104":{}},"parent":{}}],["hidden",{"_index":74,"name":{"70":{}},"parent":{}}],["href",{"_index":60,"name":{"55":{},"66":{}},"parent":{}}],["info",{"_index":56,"name":{"51":{}},"parent":{}}],["infomessagebar",{"_index":66,"name":{"59":{}},"parent":{}}],["initialize",{"_index":42,"name":{"37":{},"76":{}},"parent":{}}],["initializecallback",{"_index":34,"name":{"31":{}},"parent":{}}],["inputoptions",{"_index":27,"name":{"24":{}},"parent":{}}],["inputtype",{"_index":55,"name":{"50":{}},"parent":{}}],["invalid",{"_index":97,"name":{"97":{}},"parent":{}}],["is",{"_index":2,"name":{"1":{}},"parent":{}}],["isdisplayable",{"_index":31,"name":{"28":{}},"parent":{}}],["isdynamicdescription",{"_index":26,"name":{"23":{}},"parent":{}}],["isupdateinprogress",{"_index":86,"name":{"89":{}},"parent":{}}],["key",{"_index":54,"name":{"49":{}},"parent":{}}],["labeltkey",{"_index":12,"name":{"8":{},"11":{},"15":{},"19":{},"21":{},"48":{}},"parent":{}}],["link",{"_index":59,"name":{"53":{},"64":{}},"parent":{}}],["max",{"_index":9,"name":{"5":{}},"parent":{}}],["messagetkey",{"_index":57,"name":{"52":{},"79":{},"83":{},"87":{}},"parent":{}}],["metrics",{"_index":108,"name":{"106":{}},"parent":{}}],["min",{"_index":7,"name":{"4":{}},"parent":{}}],["mongokeys",{"_index":104,"name":{"102":{}},"parent":{}}],["numberinputoptions",{"_index":6,"name":{"3":{}},"parent":{}}],["numberuitype",{"_index":47,"name":{"43":{}},"parent":{}}],["onchange",{"_index":28,"name":{"25":{}},"parent":{}}],["onchangecallback",{"_index":45,"name":{"41":{}},"parent":{}}],["onrefresh",{"_index":44,"name":{"39":{}},"parent":{}}],["onsave",{"_index":43,"name":{"38":{}},"parent":{}}],["onsavecallback",{"_index":37,"name":{"33":{}},"parent":{}}],["onsaveresult",{"_index":76,"name":{"72":{}},"parent":{}}],["operationstatusurl",{"_index":77,"name":{"73":{}},"parent":{}}],["placeholdertkey",{"_index":14,"name":{"10":{},"18":{}},"parent":{}}],["portalnotification",{"_index":79,"name":{"74":{}},"parent":{}}],["propertyinfo",{"_index":29,"name":{"26":{}},"parent":{}}],["refreshoptions",{"_index":32,"name":{"29":{}},"parent":{}}],["refreshparams",{"_index":89,"name":{"91":{}},"parent":{}}],["refreshresult",{"_index":85,"name":{"88":{}},"parent":{}}],["retryintervalinms",{"_index":90,"name":{"92":{}},"parent":{}}],["selfserve",{"_index":0,"name":{"0":{},"1":{}},"parent":{}}],["selfserve/decorators",{"_index":5,"name":{"2":{}},"parent":{"3":{},"9":{},"12":{},"16":{},"20":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{}}}],["selfserve/decorators.booleaninputoptions",{"_index":18,"name":{},"parent":{"13":{},"14":{},"15":{}}}],["selfserve/decorators.choiceinputoptions",{"_index":22,"name":{},"parent":{"17":{},"18":{},"19":{}}}],["selfserve/decorators.descriptiondisplayoptions",{"_index":24,"name":{},"parent":{"21":{},"22":{},"23":{}}}],["selfserve/decorators.numberinputoptions",{"_index":8,"name":{},"parent":{"4":{},"5":{},"6":{},"7":{},"8":{}}}],["selfserve/decorators.stringinputoptions",{"_index":15,"name":{},"parent":{"10":{},"11":{}}}],["selfserve/selfservetelemetryprocessor",{"_index":110,"name":{"108":{}},"parent":{"109":{},"110":{},"111":{},"112":{},"113":{}}}],["selfserve/selfservetypes",{"_index":33,"name":{"30":{}},"parent":{"31":{},"33":{},"35":{},"41":{},"43":{},"46":{},"50":{},"51":{},"57":{},"61":{},"68":{},"72":{},"88":{},"91":{},"93":{}}}],["selfserve/selfservetypes.choiceitem",{"_index":52,"name":{},"parent":{"47":{}}}],["selfserve/selfservetypes.choiceitem.__type",{"_index":53,"name":{},"parent":{"48":{},"49":{}}}],["selfserve/selfservetypes.description",{"_index":68,"name":{},"parent":{"62":{},"63":{},"64":{},"65":{}}}],["selfserve/selfservetypes.description.__type",{"_index":70,"name":{},"parent":{"66":{},"67":{}}}],["selfserve/selfservetypes.descriptiontype",{"_index":65,"name":{},"parent":{"58":{},"59":{},"60":{}}}],["selfserve/selfservetypes.info",{"_index":58,"name":{},"parent":{"52":{},"53":{},"54":{}}}],["selfserve/selfservetypes.info.__type",{"_index":61,"name":{},"parent":{"55":{},"56":{}}}],["selfserve/selfservetypes.initializecallback",{"_index":36,"name":{},"parent":{"32":{}}}],["selfserve/selfservetypes.numberuitype",{"_index":49,"name":{},"parent":{"44":{},"45":{}}}],["selfserve/selfservetypes.onchangecallback",{"_index":46,"name":{},"parent":{"42":{}}}],["selfserve/selfservetypes.onsavecallback",{"_index":38,"name":{},"parent":{"34":{}}}],["selfserve/selfservetypes.onsaveresult",{"_index":78,"name":{},"parent":{"73":{},"74":{},"75":{}}}],["selfserve/selfservetypes.onsaveresult.__type",{"_index":80,"name":{},"parent":{"76":{},"77":{},"80":{},"81":{},"84":{},"85":{}}}],["selfserve/selfservetypes.onsaveresult.__type.__type",{"_index":82,"name":{},"parent":{"78":{},"79":{},"82":{},"83":{},"86":{},"87":{}}}],["selfserve/selfservetypes.refreshparams",{"_index":91,"name":{},"parent":{"92":{}}}],["selfserve/selfservetypes.refreshresult",{"_index":87,"name":{},"parent":{"89":{},"90":{}}}],["selfserve/selfservetypes.selfservebaseclass",{"_index":41,"name":{},"parent":{"36":{},"37":{},"38":{},"39":{},"40":{}}}],["selfserve/selfservetypes.selfservetelemetrymessage",{"_index":94,"name":{},"parent":{"94":{}}}],["selfserve/selfservetypes.smartuiinput",{"_index":73,"name":{},"parent":{"69":{},"70":{},"71":{}}}],["selfserve/selfserveutils",{"_index":95,"name":{"95":{}},"parent":{"96":{},"100":{},"107":{}}}],["selfserve/selfserveutils.bladetype",{"_index":103,"name":{},"parent":{"101":{},"102":{},"103":{},"104":{},"105":{},"106":{}}}],["selfserve/selfserveutils.selfservetype",{"_index":98,"name":{},"parent":{"97":{},"98":{},"99":{}}}],["selfservebaseclass",{"_index":39,"name":{"35":{}},"parent":{}}],["selfserveclassname",{"_index":93,"name":{"94":{}},"parent":{}}],["selfservetelemetrymessage",{"_index":92,"name":{"93":{}},"parent":{}}],["selfservetrace",{"_index":111,"name":{"109":{}},"parent":{}}],["selfservetracecancel",{"_index":115,"name":{"113":{}},"parent":{}}],["selfservetracefailure",{"_index":114,"name":{"112":{}},"parent":{}}],["selfservetracestart",{"_index":112,"name":{"110":{}},"parent":{}}],["selfservetracesuccess",{"_index":113,"name":{"111":{}},"parent":{}}],["selfservetype",{"_index":96,"name":{"96":{}},"parent":{}}],["slider",{"_index":50,"name":{"45":{}},"parent":{}}],["smartuiinput",{"_index":71,"name":{"68":{}},"parent":{}}],["spinner",{"_index":48,"name":{"44":{}},"parent":{}}],["sqlkeys",{"_index":102,"name":{"101":{}},"parent":{}}],["sqlx",{"_index":100,"name":{"99":{}},"parent":{}}],["step",{"_index":10,"name":{"6":{}},"parent":{}}],["stringinputoptions",{"_index":13,"name":{"9":{}},"parent":{}}],["success",{"_index":83,"name":{"80":{}},"parent":{}}],["supported",{"_index":4,"name":{"1":{}},"parent":{}}],["tablekeys",{"_index":107,"name":{"105":{}},"parent":{}}],["text",{"_index":64,"name":{"58":{}},"parent":{}}],["texttkey",{"_index":62,"name":{"56":{},"62":{},"67":{}},"parent":{}}],["titletkey",{"_index":81,"name":{"78":{},"82":{},"86":{}},"parent":{}}],["truelabeltkey",{"_index":17,"name":{"13":{}},"parent":{}}],["type",{"_index":69,"name":{"63":{}},"parent":{}}],["uitype",{"_index":11,"name":{"7":{}},"parent":{}}],["updateinprogressmessagetkey",{"_index":88,"name":{"90":{}},"parent":{}}],["value",{"_index":72,"name":{"69":{}},"parent":{}}],["values",{"_index":30,"name":{"27":{}},"parent":{}}],["warningmessagebar",{"_index":67,"name":{"60":{}},"parent":{}}],["what",{"_index":1,"name":{"1":{}},"parent":{}}]],"pipeline":[]}} \ No newline at end of file diff --git a/docs/classes/selfserve_selfservetypes.selfservebaseclass.html b/docs/classes/selfserve_selfservetypes.selfservebaseclass.html new file mode 100644 index 000000000..5cd632443 --- /dev/null +++ b/docs/classes/selfserve_selfservetypes.selfservebaseclass.html @@ -0,0 +1,306 @@ + + + + + + SelfServeBaseClass | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Class SelfServeBaseClass

+
+
+
+
+
+
+
+
+
+

All SelfServe feature classes need to derive from the SelfServeBaseClass

+
+
+
+
+

Hierarchy

+
    +
  • + SelfServeBaseClass +
  • +
+
+
+

Index

+
+
+
+

Constructors

+ +
+
+

Properties

+ +
+
+
+
+
+

Constructors

+
+ +

constructor

+ + +
+
+
+

Properties

+
+ +

Abstract initialize

+
initialize: initializeCallback
+ +
+
+

Sets default values for the properties of the Self Serve Class. Typically, you can make rest calls here + to fetch the initial values for the properties. This is also called after the onSave callback, to reinitialize the defaults.

+
+
+
+
+ +

Abstract onRefresh

+
onRefresh: () => Promise<RefreshResult>
+ +
+
+

Callback that is triggered when the refresh button is clicked. Here, you should perform the your rest API + call to check if the update action is completed.

+
+
+
+

Type declaration

+ +
+
+
+ +

Abstract onSave

+ + +
+
+

Callback that is triggerred when the submit button is clicked. You should perform your rest API + calls here using the data from the different inputs passed as a Map to this callback function.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/enums/selfserve_selfservetypes.descriptiontype.html b/docs/enums/selfserve_selfservetypes.descriptiontype.html new file mode 100644 index 000000000..ccce7d7c0 --- /dev/null +++ b/docs/enums/selfserve_selfservetypes.descriptiontype.html @@ -0,0 +1,245 @@ + + + + + + DescriptionType | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Enumeration DescriptionType

+
+
+
+
+
+
+
+

Index

+
+
+
+

Enumeration members

+ +
+
+
+
+
+

Enumeration members

+
+ +

InfoMessageBar

+
InfoMessageBar: = 1
+ +
+
+

Show the description as a Info Message bar.

+
+
+
+
+ +

Text

+
Text: = 0
+ +
+
+

Show the description as a text

+
+
+
+
+ +

WarningMessageBar

+
WarningMessageBar: = 2
+ +
+
+

Show the description as a Warning Message bar.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/enums/selfserve_selfservetypes.numberuitype.html b/docs/enums/selfserve_selfservetypes.numberuitype.html new file mode 100644 index 000000000..28dcadc76 --- /dev/null +++ b/docs/enums/selfserve_selfservetypes.numberuitype.html @@ -0,0 +1,229 @@ + + + + + + NumberUiType | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Enumeration NumberUiType

+
+
+
+
+
+
+
+

Index

+
+
+
+

Enumeration members

+ +
+
+
+
+
+

Enumeration members

+
+ +

Slider

+
Slider: = "Slider"
+ +
+
+

The numeric input UI element corresponding to the property is a Slider

+
+
+
+
+ +

Spinner

+
Spinner: = "Spinner"
+ +
+
+

The numeric input UI element corresponding to the property is a Spinner

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/enums/selfserve_selfserveutils.bladetype.html b/docs/enums/selfserve_selfserveutils.bladetype.html new file mode 100644 index 000000000..9283b498f --- /dev/null +++ b/docs/enums/selfserve_selfserveutils.bladetype.html @@ -0,0 +1,264 @@ + + + + + + BladeType | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Enumeration BladeType

+
+
+
+
+
+
+
+
+
+

Portal Blade types

+
+
+
+
+

Index

+
+
+
+

Enumeration members

+ +
+
+
+
+
+

Enumeration members

+
+ +

CassandraKeys

+
CassandraKeys: = "cassandraDbKeys"
+ +
+
+

Keys blade of a Cassandra API account.

+
+
+
+
+ +

GremlinKeys

+
GremlinKeys: = "keys"
+ +
+
+

Keys blade of a Gremlin API account.

+
+
+
+
+ +

Metrics

+
Metrics: = "metrics"
+ +
+
+

Metrics blade of an Azure Cosmos DB account.

+
+
+
+
+ +

MongoKeys

+
MongoKeys: = "mongoDbKeys"
+ +
+
+

Keys blade of a Mongo API account.

+
+
+
+
+ +

SqlKeys

+
SqlKeys: = "keys"
+ +
+
+

Keys blade of a SQL API account.

+
+
+
+
+ +

TableKeys

+
TableKeys: = "tableKeys"
+ +
+
+

Keys blade of a Table API account.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/enums/selfserve_selfserveutils.selfservetype.html b/docs/enums/selfserve_selfserveutils.selfservetype.html new file mode 100644 index 000000000..b291557ef --- /dev/null +++ b/docs/enums/selfserve_selfserveutils.selfservetype.html @@ -0,0 +1,201 @@ + + + + + + SelfServeType | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Enumeration SelfServeType

+
+
+
+
+
+
+
+
+
+

The type used to identify the Self Serve Class

+
+
+
+
+

Index

+
+
+
+

Enumeration members

+ +
+
+
+
+
+

Enumeration members

+
+ +

example

+
example: = "example"
+ +
+
+ +

invalid

+
invalid: = "invalid"
+ +
+
+ +

sqlx

+
sqlx: = "sqlx"
+ +
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..f62e4c4f2 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,209 @@ + + + + + + cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+

cosmos-explorer

+
+
+
+
+
+
+
+ +

Cosmos DB Explorer

+
+

UI for Azure Cosmos DB. Powers the Azure Portal, https://cosmos.azure.com/, and the Cosmos DB Emulator

+

+ +

Getting Started

+
+
    +
  • npm install
  • +
  • npm run build
  • +
+ +

Developing

+
+ +

Watch mode

+
+

Run npm start to start the development server and automatically rebuild on changes

+ +

Hosted Development (https://cosmos.azure.com)

+ +
    +
  • Visit: https://localhost:1234/hostedExplorer.html
  • +
  • The default webpack dev server configuration will proxy requests to the production portal backend: https://main.documentdb.ext.azure.com. This will allow you to use production connection strings on your local machine.
  • +
+ +

Emulator Development

+
+ + +

Setting up a Remote Emulator

+
+

The Cosmos emulator currently only runs in Windows environments. You can still develop on a non-Windows machine by setting up an emulator on a windows box and exposing its ports publicly:

+
    +
  1. Expose these ports publicly: 8081, 8900, 8979, 10250, 10251, 10252, 10253, 10254, 10255, 10256

    +
  2. +
  3. Download and install the emulator: https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator

    +
  4. +
  5. Start the emulator from PowerShell:

    +
  6. +
+
> cd C:/
+
+> .\CosmosDB.Emulator.exe -AllowNetworkAccess -Key="<EMULATOR MASTER KEY>"
+
+ +

Portal Development

+
+ + +

Testing

+
+ +

Unit Tests

+
+

Unit tests are located adjacent to the code under test and run with Jest:

+

npm run test

+ +

End to End CI Tests

+
+

Jest and Puppeteer are used for end to end browser based tests and are contained in test/. To run these tests locally:

+
    +
  1. Copy .env.example to .env
  2. +
  3. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
  4. +
  5. Make sure all packages are installed npm install
  6. +
  7. Run the server npm run start and wait for it to start
  8. +
  9. Run npm run test:e2e
  10. +
+ +

Releasing

+
+

We generally adhere to the release strategy documented by the Azure SDK Guidelines. Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a release/ or hotfix/ branch. See linked documentation for more details.

+ +

Architecture

+
+

+ +

Contributing

+
+

Please read the contribution guidelines.

+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_decorators.booleaninputoptions.html b/docs/interfaces/selfserve_decorators.booleaninputoptions.html new file mode 100644 index 000000000..d489484dc --- /dev/null +++ b/docs/interfaces/selfserve_decorators.booleaninputoptions.html @@ -0,0 +1,255 @@ + + + + + + BooleanInputOptions | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface BooleanInputOptions

+
+
+
+
+
+
+
+
+
+

Toggle is rendered.

+
+
+
+
+

Hierarchy

+
    +
  • + InputOptionsBase +
      +
    • + BooleanInputOptions +
    • +
    +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

falseLabelTKey

+
falseLabelTKey: string | (() => Promise<string>)
+ +
+
+

Key used to pickup the string corresponding to the false label of the toggle, from the strings JSON file.

+
+
+
+
+ +

labelTKey

+
labelTKey: string
+ +
+
+

Key used to pickup the string corresponding to the label of the UI element, from the strings JSON file.

+
+
+
+
+ +

trueLabelTKey

+
trueLabelTKey: string | (() => Promise<string>)
+ +
+
+

Key used to pickup the string corresponding to the true label of the toggle, from the strings JSON file.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_decorators.choiceinputoptions.html b/docs/interfaces/selfserve_decorators.choiceinputoptions.html new file mode 100644 index 000000000..583e71512 --- /dev/null +++ b/docs/interfaces/selfserve_decorators.choiceinputoptions.html @@ -0,0 +1,255 @@ + + + + + + ChoiceInputOptions | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface ChoiceInputOptions

+
+
+
+
+
+
+
+
+
+

Dropdown is rendered.

+
+
+
+
+

Hierarchy

+
    +
  • + InputOptionsBase +
      +
    • + ChoiceInputOptions +
    • +
    +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

choices

+
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[]
+ +
+
+

Choices to be shown in the dropdown

+
+
+
+
+ +

labelTKey

+
labelTKey: string
+ +
+
+

Key used to pickup the string corresponding to the label of the UI element, from the strings JSON file.

+
+
+
+
+ +

Optional placeholderTKey

+
placeholderTKey: string | (() => Promise<string>)
+ +
+
+

Key used to pickup the string corresponding to the placeholder text of the dropdown, from the strings JSON file.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_decorators.descriptiondisplayoptions.html b/docs/interfaces/selfserve_decorators.descriptiondisplayoptions.html new file mode 100644 index 000000000..1347ad4d1 --- /dev/null +++ b/docs/interfaces/selfserve_decorators.descriptiondisplayoptions.html @@ -0,0 +1,249 @@ + + + + + + DescriptionDisplayOptions | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface DescriptionDisplayOptions

+
+
+
+
+
+
+
+
+
+

Text is rendered.

+
+
+
+
+

Hierarchy

+
    +
  • + DescriptionDisplayOptions +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

Optional description

+
description: (() => Promise<Description>) | Description
+ +
+
+

Static description to be shown as text.

+
+
+
+
+ +

Optional isDynamicDescription

+
isDynamicDescription: boolean
+ +
+
+

If true, Indicates that the Description will be populated dynamically and that it may not be present in some scenarios.

+
+
+
+
+ +

Optional labelTKey

+
labelTKey: string
+ +
+
+

Optional heading for the text displayed by this description element.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_decorators.numberinputoptions.html b/docs/interfaces/selfserve_decorators.numberinputoptions.html new file mode 100644 index 000000000..d99ddd07f --- /dev/null +++ b/docs/interfaces/selfserve_decorators.numberinputoptions.html @@ -0,0 +1,287 @@ + + + + + + NumberInputOptions | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface NumberInputOptions

+
+
+
+
+
+
+
+
+
+

Numeric input UI element is rendered. The current options are to render it as a slider or a spinner.

+
+
+
+
+

Hierarchy

+
    +
  • + InputOptionsBase +
      +
    • + NumberInputOptions +
    • +
    +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

labelTKey

+
labelTKey: string
+ +
+
+

Key used to pickup the string corresponding to the label of the UI element, from the strings JSON file.

+
+
+
+
+ +

max

+
max: number | (() => Promise<number>)
+ +
+
+

Max value of the numeric input UI element

+
+
+
+
+ +

min

+
min: number | (() => Promise<number>)
+ +
+
+

Min value of the numeric input UI element

+
+
+
+
+ +

step

+
step: number | (() => Promise<number>)
+ +
+
+

Value by which the numeric input is incremented or decremented in the UI.

+
+
+
+
+ +

uiType

+
uiType: NumberUiType
+ +
+
+

The type of the numeric input UI element

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_decorators.stringinputoptions.html b/docs/interfaces/selfserve_decorators.stringinputoptions.html new file mode 100644 index 000000000..a0424a0e6 --- /dev/null +++ b/docs/interfaces/selfserve_decorators.stringinputoptions.html @@ -0,0 +1,239 @@ + + + + + + StringInputOptions | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface StringInputOptions

+
+
+
+
+
+
+
+
+
+

Text box is rendered.

+
+
+
+
+

Hierarchy

+
    +
  • + InputOptionsBase +
      +
    • + StringInputOptions +
    • +
    +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

labelTKey

+
labelTKey: string
+ +
+
+

Key used to pickup the string corresponding to the label of the UI element, from the strings JSON file.

+
+
+
+
+ +

Optional placeholderTKey

+
placeholderTKey: string | (() => Promise<string>)
+ +
+
+

Key used to pickup the string corresponding to the place holder text of the text box, from the strings JSON file.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_selfservetypes.description.html b/docs/interfaces/selfserve_selfservetypes.description.html new file mode 100644 index 000000000..f95fdcfcf --- /dev/null +++ b/docs/interfaces/selfserve_selfservetypes.description.html @@ -0,0 +1,277 @@ + + + + + + Description | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface Description

+
+
+
+
+
+
+
+
+
+

Data to be shown as a description.

+
+
+
+
+

Hierarchy

+
    +
  • + Description +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

Optional link

+
link: { href: string; textTKey: string }
+ +
+
+

Optional link to be shown as part of the description, after the text.

+
+
+
+

Type declaration

+
    +
  • +
    href: string
    +
    +
    +

    The URL of the link

    +
    +
    +
  • +
  • +
    textTKey: string
    +
    +
    +

    Key used to pickup the string corresponding to the text of the link, from the strings JSON file.

    +
    +
    +
  • +
+
+
+
+ +

textTKey

+
textTKey: string
+ +
+
+

Key used to pickup the string corresponding to the text to be shown as part of the description, from the strings JSON file.

+
+
+
+
+ +

type

+ + +
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_selfservetypes.info.html b/docs/interfaces/selfserve_selfservetypes.info.html new file mode 100644 index 000000000..c774dcc52 --- /dev/null +++ b/docs/interfaces/selfserve_selfservetypes.info.html @@ -0,0 +1,266 @@ + + + + + + Info | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface Info

+
+
+
+
+
+
+
+
+
+

Data to be shown within the info bubble of the property.

+
+
+
+
+

Hierarchy

+
    +
  • + Info +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

Optional link

+
link: { href: string; textTKey: string }
+ +
+
+

Optional link to be shown within the info bubble, after the text.

+
+
+
+

Type declaration

+
    +
  • +
    href: string
    +
    +
    +

    The URL of the link

    +
    +
    +
  • +
  • +
    textTKey: string
    +
    +
    +

    Key used to pickup the string corresponding to the text of the link, from the strings JSON file.

    +
    +
    +
  • +
+
+
+
+ +

messageTKey

+
messageTKey: string
+ +
+
+

Key used to pickup the string corresponding to the text to be shown within the info bubble, from the strings JSON file.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_selfservetypes.onsaveresult.html b/docs/interfaces/selfserve_selfservetypes.onsaveresult.html new file mode 100644 index 000000000..45f6ffb36 --- /dev/null +++ b/docs/interfaces/selfserve_selfservetypes.onsaveresult.html @@ -0,0 +1,321 @@ + + + + + + OnSaveResult | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface OnSaveResult

+
+
+
+
+
+
+
+

Hierarchy

+
    +
  • + OnSaveResult +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

operationStatusUrl

+
operationStatusUrl: string
+ +
+
+

The polling url returned by the RP call.

+
+
+
+
+ +

Optional portalNotification

+
portalNotification: { failure: { messageTKey: string; titleTKey: string }; initialize: { messageTKey: string; titleTKey: string }; success: { messageTKey: string; titleTKey: string } }
+ +
+
+

Notifications that need to be shown on the portal for different stages of a scenario (initialized, success/failure).

+
+
+
+

Type declaration

+
    +
  • +
    failure: { messageTKey: string; titleTKey: string }
    +
    +
    +

    Notification that need to be shown when the save operation failed.

    +
    +
    +
      +
    • +
      messageTKey: string
      +
      +
      +

      Key used to pickup the string corresponding to the notification message, from the strings JSON file.

      +
      +
      +
    • +
    • +
      titleTKey: string
      +
      +
      +

      Key used to pickup the string corresponding to the notification title, from the strings JSON file.

      +
      +
      +
    • +
    +
  • +
  • +
    initialize: { messageTKey: string; titleTKey: string }
    +
    +
    +

    Notification that need to be shown when the save operation has been triggered.

    +
    +
    +
      +
    • +
      messageTKey: string
      +
      +
      +

      Key used to pickup the string corresponding to the notification message, from the strings JSON file.

      +
      +
      +
    • +
    • +
      titleTKey: string
      +
      +
      +

      Key used to pickup the string corresponding to the notification title, from the strings JSON file.

      +
      +
      +
    • +
    +
  • +
  • +
    success: { messageTKey: string; titleTKey: string }
    +
    +
    +

    Notification that need to be shown when the save operation has successfully completed.

    +
    +
    +
      +
    • +
      messageTKey: string
      +
      +
      +

      Key used to pickup the string corresponding to the notification message, from the strings JSON file.

      +
      +
      +
    • +
    • +
      titleTKey: string
      +
      +
      +

      Key used to pickup the string corresponding to the notification title, from the strings JSON file.

      +
      +
      +
    • +
    +
  • +
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_selfservetypes.refreshparams.html b/docs/interfaces/selfserve_selfservetypes.refreshparams.html new file mode 100644 index 000000000..c7815c97c --- /dev/null +++ b/docs/interfaces/selfserve_selfservetypes.refreshparams.html @@ -0,0 +1,222 @@ + + + + + + RefreshParams | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface RefreshParams

+
+
+
+
+
+
+
+

Hierarchy

+
    +
  • + RefreshParams +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

retryIntervalInMs

+
retryIntervalInMs: number
+ +
+
+

The time interval between refresh attempts when an update in ongoing

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_selfservetypes.refreshresult.html b/docs/interfaces/selfserve_selfservetypes.refreshresult.html new file mode 100644 index 000000000..41159e1de --- /dev/null +++ b/docs/interfaces/selfserve_selfservetypes.refreshresult.html @@ -0,0 +1,239 @@ + + + + + + RefreshResult | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface RefreshResult

+
+
+
+
+
+
+
+

Hierarchy

+
    +
  • + RefreshResult +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

isUpdateInProgress

+
isUpdateInProgress: boolean
+ +
+
+

Indicate if the update is still ongoing

+
+
+
+
+ +

updateInProgressMessageTKey

+
updateInProgressMessageTKey: string
+ +
+
+

Key used to pickup the string corresponding to the message that will be shown on the UI if the update is still ongoing, from the strings JSON file. + Will be shown only if isUpdateInProgress is true.

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_selfservetypes.selfservetelemetrymessage.html b/docs/interfaces/selfserve_selfservetypes.selfservetelemetrymessage.html new file mode 100644 index 000000000..200ea0c44 --- /dev/null +++ b/docs/interfaces/selfserve_selfservetypes.selfservetelemetrymessage.html @@ -0,0 +1,227 @@ + + + + + + SelfServeTelemetryMessage | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface SelfServeTelemetryMessage

+
+
+
+
+
+
+
+

Hierarchy

+
    +
  • + TelemetryData +
      +
    • + SelfServeTelemetryMessage +
    • +
    +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

selfServeClassName

+
selfServeClassName: string
+ +
+
+

The className used to identify a SelfServe telemetry record

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/interfaces/selfserve_selfservetypes.smartuiinput.html b/docs/interfaces/selfserve_selfservetypes.smartuiinput.html new file mode 100644 index 000000000..ad6416a34 --- /dev/null +++ b/docs/interfaces/selfserve_selfservetypes.smartuiinput.html @@ -0,0 +1,254 @@ + + + + + + SmartUiInput | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Interface SmartUiInput

+
+
+
+
+
+
+
+

Hierarchy

+
    +
  • + SmartUiInput +
  • +
+
+
+

Index

+
+
+
+

Properties

+ +
+
+
+
+
+

Properties

+
+ +

Optional disabled

+
disabled: boolean
+ +
+
+

Indicates whether the UI element corresponding to the property is disabled

+
+
+
+
+ +

Optional hidden

+
hidden: boolean
+ +
+
+

Indicates whether the UI element corresponding to the property is hidden

+
+
+
+
+ +

value

+
value: InputType
+ +
+
+

The value to be set for the UI element corresponding to the property

+
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html new file mode 100644 index 000000000..923454972 --- /dev/null +++ b/docs/modules.html @@ -0,0 +1,138 @@ + + + + + + cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+

cosmos-explorer

+
+
+
+
+ +
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/modules/selfserve.html b/docs/modules/selfserve.html new file mode 100644 index 000000000..6e5a4c6a6 --- /dev/null +++ b/docs/modules/selfserve.html @@ -0,0 +1,498 @@ + + + + + + SelfServe | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Module SelfServe

+
+
+
+
+
+
+
+
+
+ +

Self Serve Model

+
+

The Self Serve Model allows you to write classes that auto generate UI components for your feature. The idea is to allow developers from other feature teams, who may not be familiar with writing UI, to develop and own UX components. This is accomplished by just writing simpler TypeScript classes for their features.

+

What this means for the feature team

+
    +
  • Can concentrate just on the logic behind showing, hiding and disabling UI components
  • +
  • Need not worry about specifics of the UI language or UX requirements (Accessibility, Localization, Themes, etc.)
  • +
  • Can own the REST API calls made as part of the feature, which can change in the future
  • +
  • Quicker turn around time for development and bug fixes since they have deeper knowledge of the feature
  • +
+

What this means for the UI team

+
    +
  • No need to ramp up on the intricacies of every feature which requires UI changes
  • +
  • Own only the framework and not every feature, giving more bandwidth to prioritize inhouse features as well
  • +
+ +

Getting Started

+
+

Clone the cosmos-explorer repo and run

+
    +
  • npm install
  • +
  • npm run build
  • +
+

Click here for more info on setting up the cosmos-explorer repo.

+ +

Code Changes

+
+

Code changes need to be made only in the following files

+
    +
  • A JSON file - for strings to be displayed
  • +
  • A Types File - for defining the data models
  • +
  • A RP file - for defining the REST calls
  • +
  • A Class file - for defining the UI
  • +
  • SelfServeUtils.tsx and SelfServe.tsx - for defning the entrypoint for the UI
  • +
+ +

1. JSON file for UI strings

+
+ +

Naming Convention

+
+

Localization/en/<FEATURE_NAME>.json
Please place your files only under "Localization/en" folder. If not, the UI strings will not be picked up by the framework.

+ +

Example

+
+

SelfServeExample.json

+ +

Description

+
+

This is a JSON file where the values are the strings that needs to be displayed in the UI. These strings are referenced using their corresponding unique keys.

+

For example, If your class file defines properties as follows

+
  @Values({
+    labelTKey: "stringPropertylabel"
+  })
+  stringProperty: string;
+
+  @Values({
+    labelTKey: "booleanPropertyLabel",
+    trueLabelTKey: "trueLabel",
+    falseLabelTKey: "falseLabel",
+  })
+  booleanProperty: boolean;
+
+

Then the content of Localization/en/FeatureName.json should be

+
{
+    stringPropertyLabel: "string property",
+    booleanPropertyLabel: "boolean property",
+    trueLabel: "Enable",
+    falseLabel: "Disable"
+}
+
+

You can learn more on how to define the class file here.

+ +

2. Types file

+
+ +

Naming Convention

+
+

<FEATURE_NAME>.types.ts

+ +

Example

+
+

SelfServeExample.types.ts

+ +

Description

+
+

This file contains the definitions of all the data models to be used in your Class file and RP file.

+

For example, if your RP call takes/returns the stringProperty and booleanProperty of your SelfServe class, then you can define an interface in your FeatureName.types.ts file like this.

+
export RpDataModel {
+  stringProperty: string,
+  booleanProperty: boolean
+}
+
+ +

3. RP file

+
+ +

Naming Convention

+
+

<FEATURE_NAME>.rp.ts

+ +

Example

+
+

SelfServeExample.rp.ts

+ +

Description

+
+

The RP file will host the REST calls needed for the initialize, save and refresh functions. This decouples the view and the model of the feature.

+

To make the ARM call, we need some information about the Azure Cosmos DB databaseAccount - the subscription id, resource group name and database account name. These are readily available through the userContext object, exposed through

+
    +
  • userContext.subscriptionId
  • +
  • userContext.resourceGroup
  • +
  • userContext.databaseAccount.name
  • +
+

You can use the armRequestWithoutPolling function to make the ARM api call.

+

Your FeatureName.rp.ts file can look like the following.

+
import { userContext } from "../../UserContext";
+import { armRequestWithoutPolling } from "../../Utils/arm/request";
+import { configContext } from "../../ConfigContext";
+
+const apiVersion = "2020-06-01-preview";
+
+export const saveData = async (properties: RpDataModel): Promise<string> => {
+  const path = `/subscriptions/${userContext.subscriptionId}/resourceGroups/${userContext.resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/<REST_OF_THE_PATH>`
+  const body = {
+      data : properties
+  }
+  const armRequestResult = await armRequestWithoutPolling({
+    host: configContext.ARM_ENDPOINT,
+    path,
+    method: "PUT",
+    apiVersion,
+    body,
+  });
+
+  return armRequestResult.operationStatusUrl;
+};
+
+
+ +

4. Class file

+
+ +

Naming Convention

+
+

<FEATURE_NAME>.tsx

+ +

Example

+
+

SelfServeExample.tsx

+ +

Description

+
+

This file will contain the actual code that is translated into the UI component by the Self Serve framework.

+
    +
  • Each Self Serve class

    +
      +
    • Needs to extends the SelfServeBase class.
    • +
    • Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class.
    • +
    • Needs to define an initialize() function, to set default values for the inputs.
    • +
    • Needs to define an onSave() function, a callback for when the save button is clicked.
    • +
    • Needs to define an onRefresh() function, a callback for when the refresh button is clicked.
    • +
    • Can have an optional @RefreshOptions() decorator that determines how often the auto refresh of the UI component should take place.
    • +
    +
  • +
  • For every UI element needed, add a property to the Self Serve class. Each of these properties

    +
      +
    • Needs to have a @Values() decorator.
    • +
    • Can have an optional @PropertyInfo() decorator that describes it's info bubble.
    • +
    • Can have an optional @OnChange() decorator that dictates the effects of the change of the UI element tied to this property.
    • +
    +
  • +
+

Your FeatureName.tsx file will look like the following.

+
@IsDisplayable()
+@RefreshOptions({ retryIntervalInMs: 2000 })
+export default class FeatureName extends SelfServeBaseClass {
+
+  public initialize = async (): Promise<Map<string, SmartUiInput>> => {
+      // initialize RP call and processing logic
+  }
+
+  public onSave = async (
+    currentValues: Map<string, SmartUiInput>,
+    baselineValues: ReadonlyMap<string, SmartUiInput>
+  ): Promise<OnSaveResult> => {
+      // onSave RP call and processing logic
+  }
+
+  public onRefresh = async (): Promise<RefreshResult> => {
+      // refresh RP call and processing logic
+  };
+
+  @Values(...)
+  stringProperty: string;
+
+  @OnChange(...)
+  @PropertyInfo(...)
+  @Values(...)
+  booleanProperty: boolean;
+}
+
+ +

5. Update SelfServeType

+
+

Once you have written your Self Serve Class, add a corresponding type to SelfServeType

+
export enum SelfServeType {
+  invalid = "invalid",
+  example = "example",
+  ...
+  // Add the type for your new feature
+  featureName = "featurename"
+}
+
+ +

6. Update SelfServe.tsx (landing page)

+
+

Once the SelfServeType has been updated, update SelfServe.tsx for your feature. This ensures that the framework picks up your SelfServe Class.

+
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
+  switch (selfServeType) {
+    case SelfServeType.example: {
+        ....
+    }
+    ...
+    ...
+    ...
+    // Add this for your new feature
+    case SelfServeType.featureName: {
+      // The 'webpackChunkName' is used during debugging, to identify if the correct class has been loaded
+      const FeatureName = await import(/* webpackChunkName: "FeatureName" */ "./FeatureName/FeatureName");
+      const featureName = new FeatureName.default();
+      await loadTranslations(featureName.constructor.name);
+      return featureName.toSelfServeDescriptor();
+    }
+    ...
+    ...
+    default:
+      return undefined;
+  }
+};
+
+
+ +

Telemetry

+
+

You can add telemetry for your feature using the functions in SelfServeTelemetryProcessor

+

For example, in your SelfServe class, you can call the trace method in your onSave function.

+
import { saveData } from "./FeatureName.rp"
+import { RpDataModel } from "./FeatureName.types"
+
+@IsDisplayable()
+export default class FeatureName extends SelfServeBaseClass {
+
+  .
+  .
+  .
+
+  public onSave = async (
+    currentValues: Map<string, SmartUiInput>,
+    baselineValues: ReadonlyMap<string, SmartUiInput>
+  ): Promise<OnSaveResult> => {
+
+    stringPropertyValue = currentValues.get("stringProperty")
+    booleanPropertyValue = currentValues.get("booleanProperty")
+    
+    const propertiesToSave : RpDataModel = { 
+      stringProperty: stringPropertyValue,
+      booleanProperty: booleanPropertyValue
+    }
+    const telemetryData = { ...propertiesToSave, selfServeClassName: FeatureName.name }
+    const onSaveTimeStamp = selfServeTraceStart(telemetryData)
+
+    await saveData(propertiesToSave)
+
+    selfServeTraceSuccess(telemetryData, onSaveTimeStamp)
+
+    // return required values
+  }
+
+  .
+  .
+  .
+
+  @Values(...)
+  stringProperty: string;
+
+  @Values(...)
+  booleanProperty: boolean;
+}
+
+ +

Portal Notifications

+
+

You can enable portal notifications for your feature by passing in the required strings as part of the portalNotification property of the onSaveResult.

+
@IsDisplayable()
+export default class SqlX extends SelfServeBaseClass {
+
+.
+.
+.
+
+  public onSave = async (
+      currentValues: Map<string, SmartUiInput>,
+      baselineValues: Map<string, SmartUiInput>
+  ): Promise<OnSaveResult> => {
+
+    stringPropertyValue = currentValues.get("stringProperty")
+    booleanPropertyValue = currentValues.get("booleanProperty")
+    
+    const propertiesToSave : RpDataModel = { 
+      stringProperty: stringPropertyValue,
+      booleanProperty: booleanPropertyValue
+    }
+
+    const operationStatusUrl = await saveData(propertiesToSave);
+    return {
+      operationStatusUrl: operationStatusUrl,
+      portalNotification: {
+        initialize: {
+          titleTKey: "DeleteInitializeTitle",
+          messageTKey: "DeleteInitializeMessage",
+        },
+        success: {
+          titleTKey: "DeleteSuccessTitle",
+          messageTKey: "DeleteSuccesseMessage",
+        },
+        failure: {
+          titleTKey: "DeleteFailureTitle",
+          messageTKey: "DeleteFailureMessage",
+        },
+      },
+    };
+  }
+
+  .
+  .
+  .
+
+  @Values(...)
+  stringProperty: string;
+
+  @Values(...)
+  booleanProperty: boolean;
+}
+
+ +

Execution

+
+ +

Watch mode

+
+

Run npm start to start the development server and automatically rebuild on changes

+ +

Local Development

+
+

Ensure that you have made the Code changes.

+
    +
  • Go to https://ms.portal.azure.com/
  • +
  • Add the query string feature.showSelfServeExample=true&feature.selfServeSource=https://localhost:1234/selfServe.html?selfServeType%3D<SELF_SERVE_TYPE>
  • +
  • Click on the Self Serve Example menu item on the left panel.
  • +
+

For example, if you want to open up the the UI of a class with the type sqlx, then visit https://ms.portal.azure.com/?feature.showSelfServeExample=true&feature.selfServeSource=https://localhost:1234/selfServe.html?selfServeType%3Dsqlx

+

+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/modules/selfserve___what_is_currently_supported_.html b/docs/modules/selfserve___what_is_currently_supported_.html new file mode 100644 index 000000000..e044d593f --- /dev/null +++ b/docs/modules/selfserve___what_is_currently_supported_.html @@ -0,0 +1,149 @@ + + + + + + SelfServe - What is currently supported? | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Module SelfServe - What is currently supported?

+
+
+
+
+
+
+
+
+
+

The Self Serve framework has integrated support for

+
    +
  1. Portal Notifications
  2. +
  3. Telemetry
  4. +
  5. the following UI controls: +
  6. +
+
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/modules/selfserve_decorators.html b/docs/modules/selfserve_decorators.html new file mode 100644 index 000000000..2605197ce --- /dev/null +++ b/docs/modules/selfserve_decorators.html @@ -0,0 +1,341 @@ + + + + + + SelfServe/Decorators | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Module SelfServe/Decorators

+
+
+
+
+
+
+
+

Index

+
+ +
+
+
+

Type aliases

+
+ +

InputOptions

+ + +
+
+

Interprets the type of the UI element and correspondingly renders

+
    +
  • slider or spinner
  • +
  • text box
  • +
  • toggle
  • +
  • drop down
  • +
  • plain text or message bar
  • +
+
+
+
+
+
+

Functions

+
+ +

Const IsDisplayable

+
    +
  • IsDisplayable(): ClassDecorator
  • +
+
    +
  • + +
    +
    +

    Indicates to the compiler that UI should be generated from this class.

    +
    +
    +

    Returns ClassDecorator

    +
  • +
+
+
+ +

Const OnChange

+ +
    +
  • + +
    +
    +

    Indicates the callback to be fired when the UI element corresponding to the property is changed.

    +
    +
    +

    Parameters

    + +

    Returns PropertyDecorator

    +
  • +
+
+
+ +

Const PropertyInfo

+
    +
  • PropertyInfo(info: Info | (() => Promise<Info>)): PropertyDecorator
  • +
+
    +
  • + +
    +
    +

    Indicates that the UI element corresponding to the property should have an Info bubble. The Info + bubble is the icon that looks like an "i" which users click on to get more information about the UI element.

    +
    +
    +

    Parameters

    +
      +
    • +
      info: Info | (() => Promise<Info>)
      +
    • +
    +

    Returns PropertyDecorator

    +
  • +
+
+
+ +

Const RefreshOptions

+
    +
  • RefreshOptions(refreshParams: RefreshParams): ClassDecorator
  • +
+
    +
  • + +
    +
    +

    If there is a long running operation in your page after the onSave action, the page can + optionally auto refresh itself using the onRefresh action. The 'RefreshOptions' indicate + how often the auto refresh of the page occurs.

    +
    +
    +

    Parameters

    + +

    Returns ClassDecorator

    +
  • +
+
+
+ +

Const Values

+ +
    +
  • + +
    +
    +

    Indicates that this property should correspond to a UI element with the given parameters.

    +
    +
    +

    Parameters

    + +

    Returns PropertyDecorator

    +
  • +
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/modules/selfserve_selfservetelemetryprocessor.html b/docs/modules/selfserve_selfservetelemetryprocessor.html new file mode 100644 index 000000000..b70d893a9 --- /dev/null +++ b/docs/modules/selfserve_selfservetelemetryprocessor.html @@ -0,0 +1,323 @@ + + + + + + SelfServe/SelfServeTelemetryProcessor | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Module SelfServe/SelfServeTelemetryProcessor

+
+
+
+
+
+
+
+

Index

+
+ +
+
+
+

Functions

+
+ +

Const selfServeTrace

+ +
    +
  • + +
    +
    +

    Log an action.

    +
    +
    +

    Parameters

    + +

    Returns void

    +
  • +
+
+
+ +

Const selfServeTraceCancel

+ +
    +
  • + +
    +
    +

    Log an action as cancelled.

    +
    +
    +

    Parameters

    +
      +
    • +
      data: SelfServeTelemetryMessage
      +
      +

      Data to be sent as part of the Self Serve Telemetry.

      +
      +
    • +
    • +
      Optional timestamp: number
      +
      +

      Timestamp of the action's start trace.

      +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

Const selfServeTraceFailure

+ +
    +
  • + +
    +
    +

    Log an action as a failure.

    +
    +
    +

    Parameters

    +
      +
    • +
      data: SelfServeTelemetryMessage
      +
      +

      Data to be sent as part of the Self Serve Telemetry.

      +
      +
    • +
    • +
      Optional timestamp: number
      +
      +

      Timestamp of the action's start trace.

      +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+ +

Const selfServeTraceStart

+ +
    +
  • + +
    +
    +

    Start logging an action.

    +
    +
    +

    Parameters

    + +

    Returns number

    +

    Timestamp of the trace start, that can be used in other trace actions. + The timestamp is used to identify all the logs associated with an action.

    +
  • +
+
+
+ +

Const selfServeTraceSuccess

+ +
    +
  • + +
    +
    +

    Log an action as a success.

    +
    +
    +

    Parameters

    +
      +
    • +
      data: SelfServeTelemetryMessage
      +
      +

      Data to be sent as part of the Self Serve Telemetry.

      +
      +
    • +
    • +
      Optional timestamp: number
      +
      +

      Timestamp of the action's start trace.

      +
      +
    • +
    +

    Returns void

    +
  • +
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/modules/selfserve_selfservetypes.html b/docs/modules/selfserve_selfservetypes.html new file mode 100644 index 000000000..7edda26ee --- /dev/null +++ b/docs/modules/selfserve_selfservetypes.html @@ -0,0 +1,363 @@ + + + + + + SelfServe/SelfServeTypes | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Module SelfServe/SelfServeTypes

+
+
+
+
+
+
+
+

Index

+
+ +
+
+
+

Type aliases

+
+ +

ChoiceItem

+
ChoiceItem: { key: string; labelTKey: string }
+ +
+

Type declaration

+
    +
  • +
    key: string
    +
    +
    +

    Key used to pickup the string that uniquely identifies the dropdown choice item, from the strings JSON file.

    +
    +
    +
  • +
  • +
    labelTKey: string
    +
    +
    +

    Key used to pickup the string corresponding to the label of the dropdown choice item, from the strings JSON file.

    +
    +
    +
  • +
+
+
+
+ +

InputType

+
InputType: number | string | boolean | ChoiceItem | Description
+ +
+
+ +

OnChangeCallback

+
OnChangeCallback: (newValue: InputType, currentValues: Map<string, SmartUiInput>, baselineValues: ReadonlyMap<string, SmartUiInput>) => Map<string, SmartUiInput>
+ +
+
+

Function that dictates how the overall UI should transform when the UI element corresponding to a property, say prop1, is changed. + The callback can be used to
* Change the value (and reflect it in the UI) for another property, say prop2
* Change the visibility for prop2 in the UI
* Disable or enable the UI element corresponding to prop2
depending on logic based on the newValue of prop1, the currentValues Map and baselineValues Map.

+
+
+
+

Type declaration

+
    +
  • + +
      +
    • +

      Parameters

      +
        +
      • +
        newValue: InputType
        +
        +

        The newValue that the property needs to be set to, after the change in the UI element corresponding to this property.

        +
        +
      • +
      • +
        currentValues: Map<string, SmartUiInput>
        +
        +

        The map of propertyName => SmartUiInput corresponding to the current state of the UI.

        +
        +
      • +
      • +
        baselineValues: ReadonlyMap<string, SmartUiInput>
        +
        +

        The map of propertyName => SmartUiInput corresponding to the initial state of the UI.

        +
        +
      • +
      +

      Returns Map<string, SmartUiInput>

      +

      A new Map of propertyName => SmartUiInput corresponding to the new state of the overall UI

      +
    • +
    +
  • +
+
+
+
+ +

initializeCallback

+
initializeCallback: () => Promise<Map<string, SmartUiInput>>
+ +
+

Type declaration

+
    +
  • + +
      +
    • +

      Returns Promise<Map<string, SmartUiInput>>

      +

      Promise of Map of propertyName => SmartUiInput which will become the current state of the UI.

      +
    • +
    +
  • +
+
+
+
+ +

onSaveCallback

+
onSaveCallback: (currentValues: Map<string, SmartUiInput>, baselineValues: ReadonlyMap<string, SmartUiInput>) => Promise<OnSaveResult>
+ +
+

Type declaration

+ +
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/docs/modules/selfserve_selfserveutils.html b/docs/modules/selfserve_selfserveutils.html new file mode 100644 index 000000000..6dcb66f55 --- /dev/null +++ b/docs/modules/selfserve_selfserveutils.html @@ -0,0 +1,185 @@ + + + + + + SelfServe/SelfServeUtils | cosmos-explorer + + + + + + +
+
+
+
+ +
+
+ Options +
+
+ All +
    +
  • Public
  • +
  • Public/Protected
  • +
  • All
  • +
+
+ + + + +
+
+ Menu +
+
+
+
+
+
+ +

Module SelfServe/SelfServeUtils

+
+
+
+
+
+
+
+

Index

+
+
+
+

Enumerations

+ +
+
+

Functions

+ +
+
+
+
+
+

Functions

+
+ +

Const generateBladeLink

+
    +
  • generateBladeLink(blade: BladeType): string
  • +
+
    +
  • + +
    +
    +

    Generate the URL corresponding to the portal blade for the current Azure Cosmos DB account

    +
    +
    +

    Parameters

    + +

    Returns string

    +
  • +
+
+
+
+ +
+
+ +
+

Generated using TypeDoc

+
+
+ + + \ No newline at end of file diff --git a/less/documentDB.less b/less/documentDB.less index 5fc2f8571..271f53992 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -724,45 +724,24 @@ execute-sproc-params-pane { .results-container, .errors-container { - padding: @MediumSpace 0px 0px @MediumSpace; height: 100%; .flex-display(); .flex-direction(); overflow: hidden; - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin-left: @MediumSpace; - - &:focus { - .focus(); - } - - .tab { - margin-right: @MediumSpace; - } - - .toggleSwitch { - .toggleSwitch(); - } - - .selectedToggle { - .selectedToggle(); - } - - .unselectedToggle { - .unselectedToggle(); - } - } - .enterInputParameters { padding: @LargeSpace @MediumSpace; } + + div[role="tabpanel"] { + height: 100%; + padding-bottom: 50px; + } } .errors-container { padding-left: (2 * @MediumSpace); + padding: @MediumSpace 0px 0px @MediumSpace; .errors-header { font-weight: 700; font-size: @DefaultFontSize; @@ -2099,7 +2078,7 @@ a:link { display: flex; flex: 1 1 auto; overflow-x: auto; - overflow-y: hidden; + overflow-y: auto; height: 100%; } @@ -3086,6 +3065,13 @@ settings-pane { } } .hiddenMain { - visibility: hidden; + display: none; height: 0px; -} \ No newline at end of file +} +.spinner { + width: 100%; + position: absolute; + z-index: 1; + background: white; + height: 100%; +} diff --git a/less/forms.less b/less/forms.less index ba771a108..572134c26 100644 --- a/less/forms.less +++ b/less/forms.less @@ -200,4 +200,12 @@ .migration:disabled { background-color: #ccc; +} + +.trigger-field { + width: 40%; + margin-top: 10px +} +.trigger-form { + padding: 10px 30px 10px 30px; } \ No newline at end of file diff --git a/less/resourceTree.less b/less/resourceTree.less index cac3f049f..39bced9da 100644 --- a/less/resourceTree.less +++ b/less/resourceTree.less @@ -2,6 +2,7 @@ .dataResourceTree { margin-left: @MediumSpace; + overflow: auto; .databaseHeader { font-size: 14px; diff --git a/less/tree.less b/less/tree.less index e60bcf69c..ed0fbf71f 100644 --- a/less/tree.less +++ b/less/tree.less @@ -1,273 +1,270 @@ @import "./Common/Constants"; - .resourceTree { + height: 100%; + flex: 0 0 auto; + .main { height: 100%; - width: 20%; - flex: 0 0 auto; - .main { - height: 100%; - } + } } .resourceTreeScroll { - height: 100%; - display: flex; - overflow-y: auto; - overflow-x: hidden; - padding-right: 10px; + height: 100%; + display: flex; + overflow-y: auto; + overflow-x: hidden; + padding-right: 10px; } .userSelectNone { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; } .treeHovermargin { - margin-left: 16px; + margin-left: 16px; } .highlight { - padding: @SmallSpace 2px; - outline: 0; + padding: @SmallSpace 2px; + outline: 0; - &:hover { - .hover(); - } + &:hover { + .hover(); + } - &:active { - .active(); - } + &:active { + .active(); + } - &:focus { - .focus(); - } + &:focus { + .focus(); + } } .contextmenushowing { - background-color: #EEE; + background-color: #eee; } .collectionstree { - width: 100%; - margin-top: @DefaultSpace; + width: 100%; + margin-top: @DefaultSpace; + .databaseList { + list-style-type: none; + padding-left: 0px; - .databaseList { - list-style-type: none; - padding-left: 0px; - - .collectionList { - padding-left:(2 * @MediumSpace); - } - - .collectionChildList { - padding-left: @LargeSpace; - } - - .databaseDocuments { - padding-left: (5 * @MediumSpace); - } + .collectionList { + padding-left: (2 * @MediumSpace); } + + .collectionChildList { + padding-left: @LargeSpace; + } + + .databaseDocuments { + padding-left: (5 * @MediumSpace); + } + } } .pointerCursor { - cursor: pointer; + cursor: pointer; } .menuEllipsis { - padding-right: 6px; - font-weight: bold; - font-size: 18px; - position: relative; - top: -5px; - left: 0px; - float: right; - display: none; - padding-left: 6px!important; - line-height: @TreeLineHeight; + padding-right: 6px; + font-weight: bold; + font-size: 18px; + position: relative; + top: -5px; + left: 0px; + float: right; + display: none; + padding-left: 6px !important; + line-height: @TreeLineHeight; } .databaseMenu { - .flex-display(); + .flex-display(); } .databaseMenu:hover .menuEllipsis, .databaseMenu:focus .menuEllipsis { - display: block; + display: block; } .databaseCollChildTextOverflow { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + flex: 1; } .collectionMenu { - .flex-display(); + .flex-display(); } .collectionMenu:hover .menuEllipsis, .collectionMenu:focus .menuEllipsis { - display: block; + display: block; } .documentsMenu:hover .menuEllipsis, .documentsMenu:focus .menuEllipsis { - display: block; + display: block; } .treeChildMenu { - display: flex; + display: flex; } .storedProcedureMenu:hover .menuEllipsis, .storedProcedureMenu:focus .menuEllipsis { - display: block; + display: block; } .childMenu { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-left: (6 * @MediumSpace); - width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: (6 * @MediumSpace); + width: 100%; } .storedChildMenu:hover .menuEllipsis, .storedChildMenu:focus .menuEllipsis { - display: block; + display: block; } .contextmenu6 { - top: -29px; + top: -29px; } .userDefinedMenu:hover .contextmenu6 { - display: block; + display: block; } .userDefinedchildMenu:hover .menuEllipsis, .userDefinedchildMenu:focus .menuEllipsis { - display: block; + display: block; } .triggersMenu:hover .menuEllipsis, .triggersMenu:focus .menuEllipsis { - display: block; + display: block; } .triggersChildMenu:hover .menuEllipsis, .triggersChildMenu:focus .menuEllipsis { - display: block; + display: block; } .databaseId { - font-size: 14px; + font-size: 14px; } .storedUdfTriggerMenu { - padding-left: 0px; + padding-left: 0px; } .collectionstree img { - width: 16px; - height: 16px; - vertical-align: text-top; + width: 16px; + height: 16px; + vertical-align: text-top; } img.collectionsTreeCollapseExpand { - width: 10px; - height: 10px; - vertical-align: middle; - margin-bottom: 5px; + width: 10px; + height: 10px; + vertical-align: middle; + margin-bottom: 5px; } .collapsed::before { - content: "\23F5"; - margin-left: 0px; - font-size: 15px; + content: "\23F5"; + margin-left: 0px; + font-size: 15px; } .expanded::before { - content: '\23F7'; - margin-left: 0px; - font-size: 15px; + content: "\23F7"; + margin-left: 0px; + font-size: 15px; } .collectionMenuChildren { - padding-left: 42px; + padding-left: 42px; } .main-nav { - width: 100vh; - height: 40px; - background: white; - transform-origin: left top; - -webkit-transform-origin: left top; - -ms-transform-origin: left top; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); - border-bottom: 1px solid #CCC; + width: 100vh; + height: 40px; + background: white; + transform-origin: left top; + -webkit-transform-origin: left top; + -ms-transform-origin: left top; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); + border-bottom: 1px solid #ccc; } .main-nav-img { - width: 16px; - height: 16px; - margin: -32px 0 0 0; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); + width: 16px; + height: 16px; + margin: -32px 0 0 0; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); } .main-nav-img.main-nav-sub-img { - width: 16px; - height: 16px; - margin: 0px 0px 0 0; - transform: rotate(180deg) translateX(0%); - -webkit-transform: rotate(180deg) translateX(0%); - -ms-transform: rotate(180deg) translateX(0%); - position: absolute; - right: -8px; - top: 16px; + width: 16px; + height: 16px; + margin: 0px 0px 0 0; + transform: rotate(180deg) translateX(0%); + -webkit-transform: rotate(180deg) translateX(0%); + -ms-transform: rotate(180deg) translateX(0%); + position: absolute; + right: -8px; + top: 16px; } ul.nav { - margin: 0 auto; - margin-top: 0px; - margin-left: 0px; + margin: 0 auto; + margin-top: 0px; + margin-left: 0px; } .mini ul.nav li { - float: right; - line-height: 25px; - height: auto; - margin-top: 3px; + float: right; + line-height: 25px; + height: auto; + margin-top: 3px; } .spancolchildstyle { - padding: 4px; + padding: 4px; } .contextmenubutton { - float: right; - display: none; + float: right; + display: none; } -.highlight:hover>.contextmenubutton { - display: unset; +.highlight:hover > .contextmenubutton { + display: unset; } -.highlight:hover>.contextmenubutton::after { - content: "\2026"; - font-size: 12px; +.highlight:hover > .contextmenubutton::after { + content: "\2026"; + font-size: 12px; } .showEllipsis { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} \ No newline at end of file + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} diff --git a/package-lock.json b/package-lock.json index c9ed3c821..79d3475da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -275,6 +275,24 @@ "adal-node": "^0.1.28" } }, + "@azure/msal-browser": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.14.2.tgz", + "integrity": "sha512-JKHE9Rer41CI8tweiyE91M8ZbGvQV9P+jOPB4ZtPxyxCi2f7ED3jNfdzyUJ1eGB+hCRnvO56M1Xc61T1R+JfYg==", + "requires": { + "@azure/msal-common": "^4.3.0" + }, + "dependencies": { + "@azure/msal-common": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.3.0.tgz", + "integrity": "sha512-jFqUWe83wVb6O8cNGGBFg2QlKvqM1ezUgJTEV7kIsAPX0RXhGFE4B1DLNt6hCnkTXDbw+KGW0zgxOEr4MJQwLw==", + "requires": { + "debug": "^4.1.1" + } + } + } + }, "@azure/msal-common": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.7.2.tgz", @@ -3691,14 +3709,84 @@ } }, "@nteract/editor": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.2.tgz", - "integrity": "sha512-Wtj0kJUSoBZsWUh82JGt6miqYS0jt0k+3SD3cnW9socayxp2KB0Qbqhh2NtrF9ysxVHWnQT8iUarJjpGIdNyng==", + "version": "10.1.12", + "resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.12.tgz", + "integrity": "sha512-bsUrCctukjWdpKNWQOQmhfxMCQ/SBVIO6+RkazI4y4dVeeP3KMP8nxfhzIbzTMNSkyynps/deZFjpDWqRhG+Dg==", "requires": { - "@nteract/messaging": "^7.0.10", - "@nteract/outputs": "^3.0.9", - "codemirror": "5.57.0", + "@nteract/messaging": "^7.0.19", + "@nteract/outputs": "^3.0.11", + "codemirror": "5.61.1", "rxjs": "^6.3.3" + }, + "dependencies": { + "@nteract/commutable": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@nteract/commutable/-/commutable-7.4.5.tgz", + "integrity": "sha512-RYqyMvkFt/04GQ9T+hGYgr9/LEy0dAYJ2QKn930TFX004KjfBT6Tt8VSLFyHWkXqPwyJ0jKMCJwqLcGOI/atqg==", + "requires": { + "immutable": "^4.0.0-rc.12", + "uuid": "^8.0.0" + } + }, + "@nteract/messaging": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@nteract/messaging/-/messaging-7.0.19.tgz", + "integrity": "sha512-gRPMxJr741/BshrfCcPSbm5iVyRU2TKmAv9jeQzk0MZEGy+Y1A0REO+eptkt4Ma0OXlvDxON6JEDauk8+2xt4w==", + "requires": { + "@nteract/types": "^7.1.9", + "@types/uuid": "^8.0.0", + "lodash.clonedeep": "^4.5.0", + "rxjs": "^6.6.0", + "uuid": "^8.0.0" + } + }, + "@nteract/outputs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@nteract/outputs/-/outputs-3.0.11.tgz", + "integrity": "sha512-LeT9ViBf+fTPSubZ9dMe7128kg0rl1jIG54V0n2GiU5RuYnUz21FU0IOaLMPUfFMO1VyVEOW5jDc3PAQx5/Kwg==", + "requires": { + "@nteract/markdown": "^4.5.2", + "@nteract/mathjax": "^4.0.11", + "ansi-to-react": "^6.0.5", + "react-json-tree": "^0.12.1" + } + }, + "@nteract/types": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@nteract/types/-/types-7.1.9.tgz", + "integrity": "sha512-a7lGMWdjfz2QGlZbAiFHifU9Nhk9ntwg/iKUTMIMRPY1Wfs5UreHSMt+vZ8OY5HGjxicfHozBatGDKXeKXFHMQ==", + "requires": { + "@nteract/commutable": "^7.4.5", + "immutable": "^4.0.0-rc.12", + "rxjs": "^6.6.0", + "uuid": "^8.0.0" + } + }, + "react-base16-styling": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.7.0.tgz", + "integrity": "sha512-lTa/VSFdU6BOAj+FryOe7OTZ0OBP8GXPOnCS0QnZi7G3zhssWgIgwl0eUL77onXx/WqKPFndB3ZeC77QC/l4Dw==", + "requires": { + "base16": "^1.0.0", + "lodash.curry": "^4.1.1", + "lodash.flow": "^3.5.0", + "pure-color": "^1.3.0" + } + }, + "react-json-tree": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.12.1.tgz", + "integrity": "sha512-j6fkRY7ha9XMv1HPVakRCsvyFwHGR5AZuwO8naBBeZXnZbbLor5tpcUxS/8XD01+D1v7ZN5p+7LU+9V1uyASiQ==", + "requires": { + "prop-types": "^15.7.2", + "react-base16-styling": "^0.7.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, "@nteract/epics": { @@ -5495,11 +5583,10 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, - "@types/memoize-one": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/memoize-one/-/memoize-one-4.1.1.tgz", - "integrity": "sha512-+9djKUUn8hOyktLCfCy4hLaIPgDNovaU36fsnZe9trFHr6ddlbIn2q0SEsnkCkNR+pBWEU440Molz/+Mpyf+gQ==", - "dev": true + "@types/lodash": { + "version": "4.14.171", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz", + "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==" }, "@types/minimatch": { "version": "3.0.3", @@ -5571,12 +5658,6 @@ "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", "dev": true }, - "@types/promise.prototype.finally": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/promise.prototype.finally/-/promise.prototype.finally-2.0.3.tgz", - "integrity": "sha512-hQfmCK9Hw8diRIa3KoIDY4aimdxckamHUcmaZeB9tBMyb/Shi1yCBIPfry+nqN4jILNVThY1tnTwdMhQeMjqrw==", - "dev": true - }, "@types/prop-types": { "version": "15.5.8", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz", @@ -5644,6 +5725,15 @@ "redux": "^4.0.0" } }, + "@types/react-splitter-layout": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/react-splitter-layout/-/react-splitter-layout-3.0.1.tgz", + "integrity": "sha512-NsKq32LdG11G/Uj+xo2QmC9S8YSe8JRtxkBhsBE7ODFs0zcnzNEqFAQirP0H7rPe2WFGiu+d/44xbHsew7QAJw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-table": { "version": "6.8.7", "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.7.tgz", @@ -6769,6 +6859,12 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -8046,9 +8142,9 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codemirror": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz", - "integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg==" + "version": "5.61.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz", + "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==" }, "collapse-white-space": { "version": "1.0.6", @@ -8094,6 +8190,12 @@ "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -10716,12 +10818,6 @@ "integrity": "sha512-uoeyx2D5LawJdziMdweOp6cnZzFOOPT9VvPG6gOh6YC7N9pU0k2KpVlRiz/Vc/fFBiGUNNeJq2Aq+9GJ65Nfrw==", "dev": true }, - "expose-loader": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.5.tgz", - "integrity": "sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw==", - "dev": true - }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -12129,6 +12225,27 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -17657,12 +17774,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18466,9 +18577,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.20", @@ -18614,6 +18725,12 @@ "yallist": "^4.0.0" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", @@ -18688,6 +18805,12 @@ "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==" }, + "marked": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.6.tgz", + "integrity": "sha512-S2mYj0FzTQa0dLddssqwRVW4EOJOVJ355Xm2Vcbm+LU7GQRGWvwbO5K87OaPSOux2AwTSgtPPaXmc8sDPrhn2A==", + "dev": true + }, "martinez-polygon-clipping": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz", @@ -19961,6 +20084,32 @@ "mimic-fn": "^2.1.0" } }, + "onigasm": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz", + "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==", + "dev": true, + "requires": { + "lru-cache": "^5.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, "open": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/open/-/open-7.3.1.tgz", @@ -20474,9 +20623,9 @@ } }, "playwright": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.10.0.tgz", - "integrity": "sha512-b7SGBcCPq4W3pb4ImEDmNXtO0ZkJbZMuWiShsaNJd+rGfY/6fqwgllsAojmxGSgFmijYw7WxCoPiAIEDIH16Kw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.13.0.tgz", + "integrity": "sha512-GA5OyEeKx1v/pRcANmYncCT67Y7Y4N5zLRU5E690dn/Id10sooR5hQZmCDYsjXlutZb/1q0R3sITALnvhEjCjg==", "dev": true, "requires": { "commander": "^6.1.0", @@ -20491,7 +20640,8 @@ "proxy-from-env": "^1.1.0", "rimraf": "^3.0.2", "stack-utils": "^2.0.3", - "ws": "^7.3.1" + "ws": "^7.4.6", + "yazl": "^2.5.1" }, "dependencies": { "commander": { @@ -20523,6 +20673,12 @@ "requires": { "escape-string-regexp": "^2.0.0" } + }, + "ws": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "dev": true } } }, @@ -21564,6 +21720,11 @@ "react-is": "^16.9.0" } }, + "react-splitter-layout": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz", + "integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA==" + }, "react-syntax-highlighter": { "version": "12.2.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz", @@ -22765,11 +22926,32 @@ "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", "dev": true }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, + "shiki": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.3.tgz", + "integrity": "sha512-NEjg1mVbAUrzRv2eIcUt3TG7X9svX7l3n3F5/3OdFq+/BxUdmBOeKGiH4icZJBLHy354Shnj6sfBTemea2e7XA==", + "dev": true, + "requires": { + "onigasm": "^2.2.5", + "vscode-textmate": "^5.2.0" + } + }, "shimmer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", @@ -24234,10 +24416,65 @@ "is-typedarray": "^1.0.0" } }, + "typedoc": { + "version": "0.20.36", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.20.36.tgz", + "integrity": "sha512-qFU+DWMV/hifQ9ZAlTjdFO9wbUIHuUBpNXzv68ZyURAP9pInjZiO4+jCPeAzHVcaBCHER9WL/+YzzTt6ZlN/Nw==", + "dev": true, + "requires": { + "colors": "^1.4.0", + "fs-extra": "^9.1.0", + "handlebars": "^4.7.7", + "lodash": "^4.17.21", + "lunr": "^2.3.9", + "marked": "^2.0.3", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shelljs": "^0.8.4", + "shiki": "^0.9.3", + "typedoc-default-themes": "^0.12.10" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "typedoc-default-themes": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", + "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", + "dev": true + }, "typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", + "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==", "dev": true }, "typestyle": { @@ -24722,6 +24959,12 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" }, + "vscode-textmate": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.4.0.tgz", + "integrity": "sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w==", + "dev": true + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -25683,6 +25926,12 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", @@ -25920,6 +26169,15 @@ "fd-slicer": "~1.1.0" } }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 467ec71a7..153244680 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", "@azure/ms-rest-nodeauth": "3.0.7", + "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", "@fluentui/react": "8.14.3", @@ -21,7 +22,7 @@ "@nteract/data-explorer": "8.0.3", "@nteract/directory-listing": "2.0.6", "@nteract/dropdown-menu": "1.0.1", - "@nteract/editor": "10.1.2", + "@nteract/editor": "10.1.12", "@nteract/fixtures": "2.3.0", "@nteract/iron-icons": "1.0.0", "@nteract/jupyter-widgets": "2.0.0", @@ -41,6 +42,7 @@ "@octokit/rest": "17.9.2", "@phosphor/widgets": "1.9.3", "@testing-library/jest-dom": "5.11.9", + "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "applicationinsights": "1.8.0", @@ -75,7 +77,6 @@ "mkdirp": "1.0.4", "monaco-editor": "0.18.1", "ms": "2.1.3", - "msal": "1.4.4", "p-retry": "4.2.0", "plotly.js-cartesian-dist-min": "1.52.3", "post-robot": "10.0.42", @@ -89,6 +90,7 @@ "react-i18next": "11.8.5", "react-notification-system": "0.2.17", "react-redux": "7.1.3", + "react-splitter-layout": "4.0.0", "redux": "4.0.4", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", @@ -116,15 +118,14 @@ "@types/enzyme-adapter-react-16": "1.0.6", "@types/hasher": "0.0.31", "@types/jest": "26.0.20", - "@types/memoize-one": "4.1.1", "@types/node": "12.11.1", "@types/post-robot": "10.0.1", - "@types/promise.prototype.finally": "2.0.3", "@types/q": "1.5.1", "@types/react": "17.0.3", "@types/react-dom": "17.0.3", "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", + "@types/react-splitter-layout": "3.0.1", "@types/sanitize-html": "1.27.2", "@types/sinon": "2.3.3", "@types/styled-components": "5.1.1", @@ -146,7 +147,6 @@ "eslint-plugin-prefer-arrow": "1.2.2", "eslint-plugin-react-hooks": "4.2.0", "expect-playwright": "0.3.3", - "expose-loader": "0.7.5", "fast-glob": "3.2.5", "file-loader": "2.0.0", "fs-extra": "7.0.0", @@ -164,7 +164,7 @@ "mini-css-extract-plugin": "0.4.3", "monaco-editor-webpack-plugin": "1.7.0", "node-fetch": "2.6.1", - "playwright": "1.10.0", + "playwright": "1.13.0", "prettier": "2.2.1", "raw-loader": "0.5.1", "react-dev-utils": "11.0.4", @@ -174,7 +174,8 @@ "ts-loader": "6.2.2", "tslint": "5.11.0", "tslint-microsoft-contrib": "6.0.0", - "typescript": "4.2.4", + "typedoc": "0.20.36", + "typescript": "4.3.4", "url-loader": "1.1.1", "wait-on": "4.0.2", "webpack": "4.46.0", @@ -196,6 +197,7 @@ "watch": "npm run start", "wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/", "build:ase": "gulp build:ase", + "selfServeDocs": "typedoc", "compile": "tsc", "compile:contracts": "tsc -p ./tsconfig.contracts.json", "compile:strict": "tsc -p ./tsconfig.strict.json", @@ -206,7 +208,7 @@ "strict:find": "node ./strict-null-checks/find.js", "strict:add": "node ./strict-null-checks/auto-add.js", "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks", - "generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts" + "generateARMClients": "npx ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts" }, "repository": { "type": "git", diff --git a/preview/config.json b/preview/config.json index 56bbad187..7e6044988 100644 --- a/preview/config.json +++ b/preview/config.json @@ -1,3 +1,4 @@ { - "PROXY_PATH": "/proxy" + "PROXY_PATH": "/proxy", + "msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/" } diff --git a/preview/index.js b/preview/index.js index c17eec9ee..c205600a9 100644 --- a/preview/index.js +++ b/preview/index.js @@ -62,6 +62,17 @@ app.get("/pull/:pr(\\d+)", (req, res) => { }) .catch(() => res.sendStatus(500)); }); +app.get("/", (req, res) => { + fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master") + .then((response) => response.json()) + .then(({ commit: { sha } }) => { + const explorer = new URL( + "https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/hostedExplorer.html" + ); + return res.redirect(explorer.href); + }) + .catch(() => res.sendStatus(500)); +}); app.listen(port, () => { console.log(`Example app listening on port: ${port}`); diff --git a/src/Common/CollapsedResourceTree.tsx b/src/Common/CollapsedResourceTree.tsx index ea1ee6a32..8a91fd5d3 100644 --- a/src/Common/CollapsedResourceTree.tsx +++ b/src/Common/CollapsedResourceTree.tsx @@ -1,5 +1,6 @@ import React, { FunctionComponent } from "react"; import arrowLeftImg from "../../images/imgarrowlefticon.svg"; +import { userContext } from "../UserContext"; export interface CollapsedResourceTreeProps { toggleLeftPaneExpanded: () => void; @@ -25,7 +26,7 @@ export const CollapsedResourceTree: FunctionComponent - + {userContext.apiType} API diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 31f537d8a..fc97d559d 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -44,7 +44,7 @@ export class ArmResourceTypes { } export class BackendDefaults { - public static partitionKeyKind: string = "Hash"; + public static partitionKeyKind = "Hash"; public static singlePartitionStorageInGb: string = "10"; public static multiPartitionStorageInGb: string = "100"; public static maxChangeFeedRetentionDuration: number = 10; @@ -94,7 +94,7 @@ export class Flights { public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexing = "mongoindexing"; public static readonly AutoscaleTest = "autoscaletest"; - public static readonly SchemaAnalyzer = "schemaanalyzer"; + public static readonly PartitionKeyTest = "partitionkeytest"; } export class AfecFeatures { @@ -158,16 +158,6 @@ export class DocumentsGridMetrics { public static DocumentEditorMaxWidthRatio: number = 0.4; } -export class ExplorerMetrics { - public static SplitterMinWidth: number = 240; - public static SplitterMaxWidth: number = 400; - public static CollapsedResourceTreeWidth: number = 36; -} - -export class SplitterMetrics { - public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth; -} - export class Areas { public static ResourceTree: string = "Resource Tree"; public static ContextualPane: string = "Contextual Pane"; diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 9ba100b3b..14aa882fa 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -10,6 +10,13 @@ const _global = typeof self === "undefined" ? window : self; export const tokenProvider = async (requestInfo: RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; + + if (userContext.features.enableAadDataPlane && userContext.aadToken) { + const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; + const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`; + return authorizationToken; + } + if (configContext.platform === Platform.Emulator) { // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); diff --git a/src/Common/DatabaseAccountUtility.ts b/src/Common/DatabaseAccountUtility.ts new file mode 100644 index 000000000..693f581f8 --- /dev/null +++ b/src/Common/DatabaseAccountUtility.ts @@ -0,0 +1,17 @@ +import { userContext } from "../UserContext"; + +function isVirtualNetworkFilterEnabled() { + return userContext.databaseAccount?.properties?.isVirtualNetworkFilterEnabled; +} + +function isIpRulesEnabled() { + return userContext.databaseAccount?.properties?.ipRules?.length > 0; +} + +function isPrivateEndpointConnectionsEnabled() { + return userContext.databaseAccount?.properties?.privateEndpointConnections?.length > 0; +} + +export function isPublicInternetAccessAllowed(): boolean { + return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled(); +} diff --git a/src/Common/EntityValue.tsx b/src/Common/EntityValue.tsx index edea0fa35..788e08f85 100644 --- a/src/Common/EntityValue.tsx +++ b/src/Common/EntityValue.tsx @@ -32,7 +32,7 @@ export const EntityValue: FunctionComponent = ({ = ({ disabled={isEntityValueDisable} type={entityValueType} placeholder={entityValuePlaceholder} - value={typeof entityValue === "string" && entityValue} + value={typeof entityValue === "string" ? entityValue : ""} onChange={onEntityValueChange} /> ); diff --git a/src/Common/HeadersUtility.test.ts b/src/Common/HeadersUtility.test.ts index 5f46c420f..5432227fa 100644 --- a/src/Common/HeadersUtility.test.ts +++ b/src/Common/HeadersUtility.test.ts @@ -1,6 +1,6 @@ -import * as HeadersUtility from "./HeadersUtility"; -import { ExplorerSettings } from "../Shared/ExplorerSettings"; +import * as ExplorerSettings from "../Shared/ExplorerSettings"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; +import * as HeadersUtility from "./HeadersUtility"; describe("Headers Utility", () => { describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => { diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 8f7e033f9..3a5a02365 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -5,7 +5,6 @@ import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; import { updateUserContext } from "../UserContext"; import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; -jest.mock("../ResourceProvider/ResourceProviderClient.ts"); const databaseId = "testDB"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index f9f5b356d..2945f1288 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -111,7 +111,7 @@ export function queryDocuments( headers: response.headers, }; } - errorHandling(response, "querying documents", params); + await errorHandling(response, "querying documents", params); return undefined; }); } @@ -153,11 +153,11 @@ export function readDocument( ), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "reading document", params); + return await errorHandling(response, "reading document", params); }); } @@ -192,11 +192,11 @@ export function createDocument( ...authHeaders(), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "creating document", params); + return await errorHandling(response, "creating document", params); }); } @@ -238,11 +238,11 @@ export function updateDocument( [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "updating document", params); + return await errorHandling(response, "updating document", params); }); } @@ -278,11 +278,11 @@ export function deleteDocument(databaseId: string, collection: Collection, docum [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return undefined; } - return errorHandling(response, "deleting document", params); + return await errorHandling(response, "deleting document", params); }); } @@ -325,11 +325,11 @@ export function createMongoCollectionWithProxy( }, } ) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "creating collection", mongoParams); + return await errorHandling(response, "creating collection", mongoParams); }); } diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index adc47bdf7..534d12879 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -1,25 +1,23 @@ -import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import * as _ from "underscore"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { useDatabases } from "../Explorer/useDatabases"; import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; -import * as QueryUtils from "../Utils/QueryUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { createCollection } from "./dataAccess/createCollection"; import { createDocument } from "./dataAccess/createDocument"; import { deleteDocument } from "./dataAccess/deleteDocument"; import { queryDocuments } from "./dataAccess/queryDocuments"; -import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { handleError } from "./ErrorHandlingUtils"; export class QueriesClient { private static readonly PartitionKey: DataModels.PartitionKey = { paths: [`/${SavedQueries.PartitionKeyProperty}`], - kind: BackendDefaults.partitionKeyKind, + kind: "Hash", version: BackendDefaults.partitionKeyVersion, }; private static readonly FetchQuery: string = "SELECT * FROM c"; @@ -100,45 +98,35 @@ export class QueriesClient { const options: any = { enableCrossPartitionQuery: true }; const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); - const queryIterator: QueryIterator = queryDocuments( + const results = await 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()); + ).fetchAll(); + + let queries: DataModels.Query[] = _.map(results.resources, (document: DataModels.Query) => { + if (!document) { + return undefined; + } + const { id, resourceId, query, queryName } = document; + const parsedQuery: DataModels.Query = { + resourceId: resourceId, + queryName: queryName, + query: query, + id: id, + }; + try { + this.validateQuery(parsedQuery); + return parsedQuery; + } catch (error) { + return undefined; + } + }); + queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); + NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); + clearMessage(); + return queries; } public async deleteQuery(query: DataModels.Query): Promise { @@ -189,7 +177,7 @@ export class QueriesClient { private findQueriesCollection(): ViewModels.Collection { const queriesDatabase: ViewModels.Database = _.find( - this.container.databases(), + useDatabases.getState().databases, (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName ); if (!queriesDatabase) { diff --git a/src/Common/ResourceTree.tsx b/src/Common/ResourceTreeContainer.tsx similarity index 75% rename from src/Common/ResourceTree.tsx rename to src/Common/ResourceTreeContainer.tsx index 9aae283ea..fe04f9e04 100644 --- a/src/Common/ResourceTree.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -2,17 +2,22 @@ import React, { FunctionComponent } from "react"; import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import refreshImg from "../../images/refresh-cosmos.svg"; import { AuthType } from "../AuthType"; +import Explorer from "../Explorer/Explorer"; +import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree"; +import { ResourceTree } from "../Explorer/Tree/ResourceTree"; import { userContext } from "../UserContext"; -export interface ResourceTreeProps { +export interface ResourceTreeContainerProps { toggleLeftPaneExpanded: () => void; isLeftPaneExpanded: boolean; + container: Explorer; } -export const ResourceTree: FunctionComponent = ({ +export const ResourceTreeContainer: FunctionComponent = ({ toggleLeftPaneExpanded, isLeftPaneExpanded, -}: ResourceTreeProps): JSX.Element => { + container, +}: ResourceTreeContainerProps): JSX.Element => { return (
{/* Collections Window - - Start */} @@ -20,7 +25,7 @@ export const ResourceTree: FunctionComponent = ({ {/* Collections Window Title/Command Bar - Start */}
- + {userContext.apiType} API
= ({ aria-label="Refresh tree" title="Refresh tree" > - Refresh tree + Refresh Tree = ({
{userContext.authType === AuthType.ResourceToken ? ( -
- ) : ( + + ) : userContext.features.enableKOResourceTree ? (
+ ) : ( + )}
{/* Collections Window - End */} diff --git a/src/Common/Splitter.ts b/src/Common/Splitter.ts index cf2518812..1db6d3ba5 100644 --- a/src/Common/Splitter.ts +++ b/src/Common/Splitter.ts @@ -1,7 +1,3 @@ -import * as ko from "knockout"; - -import { SplitterMetrics } from "./Constants"; - export enum SplitterDirection { Horizontal = "horizontal", Vertical = "vertical", @@ -28,14 +24,12 @@ export class Splitter { public lastX!: number; public lastWidth!: number; - private isCollapsed: ko.Observable; 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(); @@ -83,23 +77,4 @@ export class Splitter { }; private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto"); - - public collapseLeft() { - this.lastX = $(this.splitter).position().left; - this.lastWidth = $(this.leftSide).width(); - $(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft); - $(this.leftSide).css("width", ""); - $(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible - $(this.splitter).removeClass("ui-resizable-e"); - this.isCollapsed(true); - } - - public expandLeft() { - $(this.splitter).addClass("ui-resizable-e"); - $(this.leftSide).css("width", this.lastWidth); - $(this.splitter).css("left", this.lastX); - $(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing - $(this.leftSide).resizable("enable"); - this.isCollapsed(false); - } } diff --git a/src/Common/dataAccess/createCollection.test.ts b/src/Common/dataAccess/createCollection.test.ts index ce04404a6..2f6bf63e4 100644 --- a/src/Common/dataAccess/createCollection.test.ts +++ b/src/Common/dataAccess/createCollection.test.ts @@ -1,7 +1,10 @@ jest.mock("../../Utils/arm/request"); jest.mock("../CosmosClient"); +import ko from "knockout"; import { AuthType } from "../../AuthType"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; +import { Database } from "../../Contracts/ViewModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { updateUserContext } from "../../UserContext"; import { armRequest } from "../../Utils/arm/request"; import { client } from "../CosmosClient"; @@ -23,6 +26,15 @@ describe("createCollection", () => { } as DatabaseAccount, apiType: "SQL", }); + useDatabases.setState({ + databases: [ + { + id: ko.observable("testDatabase"), + loadCollections: () => undefined, + collections: ko.observableArray([]), + } as Database, + ], + }); }); it("should call ARM if logged in with AAD", async () => { diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 2aecaeebc..791b29fcc 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -4,24 +4,17 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; -import { - createUpdateCassandraTable, - getCassandraTable, -} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { - createUpdateGremlinGraph, - getGremlinGraph, -} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { - createUpdateMongoDBCollection, - getMongoDBCollection, -} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; -import * as ARMTypes from "../../Utils/arm/generatedClients/2020-04-01/types"; +import { getCollectionName } from "../../Utils/APITypeUtils"; +import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; +import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; @@ -62,6 +55,16 @@ export const createCollection = async (params: DataModels.CreateCollectionParams }; const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise => { + if (!params.createNewDatabase) { + const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId); + if (!isValid) { + const collectionName = getCollectionName().toLocaleLowerCase(); + throw new Error( + `Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists` + ); + } + } + const { apiType } = userContext; switch (apiType) { case "SQL": @@ -80,23 +83,6 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams }; const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getSqlContainer( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create container failed: container with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.SqlContainerResource = { id: params.collectionId, @@ -134,23 +120,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise => { const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }]; - try { - const getResponse = await getMongoDBCollection( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.MongoDBCollectionResource = { id: params.collectionId, @@ -192,23 +161,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams): }; const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getCassandraTable( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create table failed: table with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.CassandraTableResource = { id: params.collectionId, @@ -236,23 +188,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams): }; const createGraph = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getGremlinGraph( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.GremlinGraphResource = { id: params.collectionId, @@ -287,22 +222,6 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getTable( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create table failed: table with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.TableResource = { id: params.collectionId, diff --git a/src/Common/dataAccess/createDatabase.ts b/src/Common/dataAccess/createDatabase.ts index fc4172ce5..2467b7975 100644 --- a/src/Common/dataAccess/createDatabase.ts +++ b/src/Common/dataAccess/createDatabase.ts @@ -2,27 +2,20 @@ import { DatabaseResponse } from "@azure/cosmos"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { userContext } from "../../UserContext"; -import { - createUpdateCassandraKeyspace, - getCassandraKeyspace, -} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { - createUpdateGremlinDatabase, - getGremlinDatabase, -} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { - createUpdateMongoDBDatabase, - getMongoDBDatabase, -} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { getDatabaseName } from "../../Utils/APITypeUtils"; +import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { CassandraKeyspaceCreateUpdateParameters, CreateUpdateOptions, GremlinDatabaseCreateUpdateParameters, MongoDBDatabaseCreateUpdateParameters, SqlDatabaseCreateUpdateParameters, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +} from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; @@ -48,6 +41,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P } async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise { + if (!useDatabases.getState().validateDatabaseId(params.databaseId)) { + const databaseName = getDatabaseName().toLocaleLowerCase(); + throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`); + } + const { apiType } = userContext; switch (apiType) { @@ -65,22 +63,6 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P } async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getSqlDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: SqlDatabaseCreateUpdateParameters = { properties: { @@ -101,22 +83,6 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi } async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getMongoDBDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: MongoDBDatabaseCreateUpdateParameters = { properties: { @@ -137,22 +103,6 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro } async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getCassandraKeyspace( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: CassandraKeyspaceCreateUpdateParameters = { properties: { @@ -173,22 +123,6 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): } async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getGremlinDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: GremlinDatabaseCreateUpdateParameters = { properties: { diff --git a/src/Common/dataAccess/createDocument.ts b/src/Common/dataAccess/createDocument.ts index b64f70ff9..94dde951d 100644 --- a/src/Common/dataAccess/createDocument.ts +++ b/src/Common/dataAccess/createDocument.ts @@ -1,8 +1,8 @@ import { CollectionBase } from "../../Contracts/ViewModels"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { getEntityName } from "../DocumentUtility"; import { handleError } from "../ErrorHandlingUtils"; -import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise => { const entityName = getEntityName(); diff --git a/src/Common/dataAccess/createStoredProcedure.ts b/src/Common/dataAccess/createStoredProcedure.ts index 471ea0f97..579835277 100644 --- a/src/Common/dataAccess/createStoredProcedure.ts +++ b/src/Common/dataAccess/createStoredProcedure.ts @@ -4,11 +4,11 @@ import { userContext } from "../../UserContext"; import { createUpdateSqlStoredProcedure, getSqlStoredProcedure, -} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +} from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { SqlStoredProcedureCreateUpdateParameters, SqlStoredProcedureResource, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +} from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/createTrigger.ts b/src/Common/dataAccess/createTrigger.ts index d9771b095..ad825820f 100644 --- a/src/Common/dataAccess/createTrigger.ts +++ b/src/Common/dataAccess/createTrigger.ts @@ -1,11 +1,8 @@ -import { Resource, TriggerDefinition } from "@azure/cosmos"; +import { TriggerDefinition } from "@azure/cosmos"; import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { - SqlTriggerCreateUpdateParameters, - SqlTriggerResource, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; @@ -13,8 +10,8 @@ import { handleError } from "../ErrorHandlingUtils"; export async function createTrigger( databaseId: string, collectionId: string, - trigger: TriggerDefinition -): Promise { + trigger: SqlTriggerResource +): Promise { const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`); try { if (userContext.authType === AuthType.AAD && !userContext.useSDKOperations && userContext.apiType === "SQL") { @@ -38,7 +35,7 @@ export async function createTrigger( const createTriggerParams: SqlTriggerCreateUpdateParameters = { properties: { - resource: trigger as SqlTriggerResource, + resource: trigger, options: {}, }, }; @@ -51,10 +48,13 @@ export async function createTrigger( trigger.id, createTriggerParams ); - return rpResponse && (rpResponse.properties?.resource as TriggerDefinition & Resource); + return rpResponse && rpResponse.properties?.resource; } - const response = await client().database(databaseId).container(collectionId).scripts.triggers.create(trigger); + const response = await client() + .database(databaseId) + .container(collectionId) + .scripts.triggers.create((trigger as unknown) as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type return response.resource; } catch (error) { handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`); diff --git a/src/Common/dataAccess/createUserDefinedFunction.ts b/src/Common/dataAccess/createUserDefinedFunction.ts index 8608c25da..3b7dd4a12 100644 --- a/src/Common/dataAccess/createUserDefinedFunction.ts +++ b/src/Common/dataAccess/createUserDefinedFunction.ts @@ -4,11 +4,11 @@ import { userContext } from "../../UserContext"; import { createUpdateSqlUserDefinedFunction, getSqlUserDefinedFunction, -} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +} from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { SqlUserDefinedFunctionCreateUpdateParameters, SqlUserDefinedFunctionResource, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +} from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/deleteCollection.ts b/src/Common/dataAccess/deleteCollection.ts index ec3dd7453..63e58b8c8 100644 --- a/src/Common/dataAccess/deleteCollection.ts +++ b/src/Common/dataAccess/deleteCollection.ts @@ -1,10 +1,10 @@ import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { deleteCassandraTable } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; +import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { deleteSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { deleteTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/deleteDatabase.ts b/src/Common/dataAccess/deleteDatabase.ts index 4e7d3523c..c6c744e35 100644 --- a/src/Common/dataAccess/deleteDatabase.ts +++ b/src/Common/dataAccess/deleteDatabase.ts @@ -1,9 +1,9 @@ import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/deleteStoredProcedure.ts b/src/Common/dataAccess/deleteStoredProcedure.ts index d1cd534ac..daaf8315a 100644 --- a/src/Common/dataAccess/deleteStoredProcedure.ts +++ b/src/Common/dataAccess/deleteStoredProcedure.ts @@ -1,6 +1,6 @@ import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { deleteSqlStoredProcedure } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { deleteSqlStoredProcedure } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/deleteTrigger.ts b/src/Common/dataAccess/deleteTrigger.ts index 3309bc2a9..b4a7aa7ad 100644 --- a/src/Common/dataAccess/deleteTrigger.ts +++ b/src/Common/dataAccess/deleteTrigger.ts @@ -1,6 +1,6 @@ import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { deleteSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { deleteSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/deleteUserDefinedFunction.ts b/src/Common/dataAccess/deleteUserDefinedFunction.ts index b502c1a98..c9683c1ab 100644 --- a/src/Common/dataAccess/deleteUserDefinedFunction.ts +++ b/src/Common/dataAccess/deleteUserDefinedFunction.ts @@ -1,6 +1,6 @@ import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { deleteSqlUserDefinedFunction } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { deleteSqlUserDefinedFunction } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 16b2fb39e..c24e22ff6 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -1,6 +1,6 @@ -import { Queries } from "../Constants"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Queries } from "../Constants"; import { client } from "../CosmosClient"; export const queryDocuments = ( diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 064e4126f..e8b5447ef 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -1,8 +1,8 @@ import { QueryResults } from "../../Contracts/ViewModels"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; -import { handleError } from "../ErrorHandlingUtils"; import { getEntityName } from "../DocumentUtility"; +import { handleError } from "../ErrorHandlingUtils"; +import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; export const queryDocumentsPage = async ( resourceName: string, diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index 4b1e0a9e8..c307242d8 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -1,11 +1,11 @@ import { AuthType } from "../../AuthType"; import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; -import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; +import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { getTableThroughput } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { handleError } from "../ErrorHandlingUtils"; import { readOfferWithSDK } from "./readOfferWithSDK"; diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts index 7161f598c..92be4f57e 100644 --- a/src/Common/dataAccess/readCollections.ts +++ b/src/Common/dataAccess/readCollections.ts @@ -1,11 +1,11 @@ import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; -import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; +import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/readDatabaseOffer.ts b/src/Common/dataAccess/readDatabaseOffer.ts index b26976a64..d27d68078 100644 --- a/src/Common/dataAccess/readDatabaseOffer.ts +++ b/src/Common/dataAccess/readDatabaseOffer.ts @@ -1,10 +1,10 @@ import { AuthType } from "../../AuthType"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; -import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { handleError } from "../ErrorHandlingUtils"; import { readOfferWithSDK } from "./readOfferWithSDK"; diff --git a/src/Common/dataAccess/readDatabases.ts b/src/Common/dataAccess/readDatabases.ts index 079c92269..e5136676e 100644 --- a/src/Common/dataAccess/readDatabases.ts +++ b/src/Common/dataAccess/readDatabases.ts @@ -1,10 +1,10 @@ import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; -import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; -import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/readMongoDBCollection.tsx b/src/Common/dataAccess/readMongoDBCollection.tsx index e7ae32c79..cdc4a0845 100644 --- a/src/Common/dataAccess/readMongoDBCollection.tsx +++ b/src/Common/dataAccess/readMongoDBCollection.tsx @@ -1,7 +1,7 @@ import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types"; +import { getMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/readStoredProcedures.ts b/src/Common/dataAccess/readStoredProcedures.ts index b7955ffd7..65edb54c2 100644 --- a/src/Common/dataAccess/readStoredProcedures.ts +++ b/src/Common/dataAccess/readStoredProcedures.ts @@ -1,7 +1,7 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/readTriggers.ts b/src/Common/dataAccess/readTriggers.ts index 7f87d3ae9..764f3ace1 100644 --- a/src/Common/dataAccess/readTriggers.ts +++ b/src/Common/dataAccess/readTriggers.ts @@ -1,7 +1,8 @@ -import { Resource, TriggerDefinition } from "@azure/cosmos"; +import { TriggerDefinition } from "@azure/cosmos"; import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { listSqlTriggers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { listSqlTriggers } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; @@ -9,7 +10,7 @@ import { handleError } from "../ErrorHandlingUtils"; export async function readTriggers( databaseId: string, collectionId: string -): Promise<(TriggerDefinition & Resource)[]> { +): Promise { const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`); try { if (userContext.authType === AuthType.AAD && !userContext.useSDKOperations && userContext.apiType === "SQL") { @@ -20,7 +21,7 @@ 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); } const response = await client().database(databaseId).container(collectionId).scripts.triggers.readAll().fetchAll(); diff --git a/src/Common/dataAccess/readUserDefinedFunctions.ts b/src/Common/dataAccess/readUserDefinedFunctions.ts index 813ff95c5..a55ad0ca6 100644 --- a/src/Common/dataAccess/readUserDefinedFunctions.ts +++ b/src/Common/dataAccess/readUserDefinedFunctions.ts @@ -1,7 +1,7 @@ import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { listSqlUserDefinedFunctions } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { listSqlUserDefinedFunctions } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/updateCollection.ts b/src/Common/dataAccess/updateCollection.ts index 2d022bbe9..cb553b77f 100644 --- a/src/Common/dataAccess/updateCollection.ts +++ b/src/Common/dataAccess/updateCollection.ts @@ -6,23 +6,20 @@ import { userContext } from "../../UserContext"; import { createUpdateCassandraTable, getCassandraTable, -} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; -import { - createUpdateGremlinGraph, - getGremlinGraph, -} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; +} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { createUpdateMongoDBCollection, getMongoDBCollection, -} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; -import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; +} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import { ExtendedResourceProperties, MongoDBCollectionCreateUpdateParameters, SqlContainerCreateUpdateParameters, SqlContainerResource, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +} from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/updateOffer.ts b/src/Common/dataAccess/updateOffer.ts index 7f132d729..380de430d 100644 --- a/src/Common/dataAccess/updateOffer.ts +++ b/src/Common/dataAccess/updateOffer.ts @@ -10,7 +10,7 @@ import { migrateCassandraTableToManualThroughput, updateCassandraKeyspaceThroughput, updateCassandraTableThroughput, -} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; +} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { migrateGremlinDatabaseToAutoscale, migrateGremlinDatabaseToManualThroughput, @@ -18,7 +18,7 @@ import { migrateGremlinGraphToManualThroughput, updateGremlinDatabaseThroughput, updateGremlinGraphThroughput, -} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; +} from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { migrateMongoDBCollectionToAutoscale, migrateMongoDBCollectionToManualThroughput, @@ -26,7 +26,7 @@ import { migrateMongoDBDatabaseToManualThroughput, updateMongoDBCollectionThroughput, updateMongoDBDatabaseThroughput, -} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; +} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { migrateSqlContainerToAutoscale, migrateSqlContainerToManualThroughput, @@ -34,13 +34,13 @@ import { migrateSqlDatabaseToManualThroughput, updateSqlContainerThroughput, updateSqlDatabaseThroughput, -} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +} from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { migrateTableToAutoscale, migrateTableToManualThroughput, updateTableThroughput, -} from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; -import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types"; +} from "../../Utils/arm/generatedClients/cosmos/tableResources"; +import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { HttpHeaders } from "../Constants"; import { client } from "../CosmosClient"; diff --git a/src/Common/dataAccess/updateStoredProcedure.ts b/src/Common/dataAccess/updateStoredProcedure.ts index dd6d86f66..b3cf875a0 100644 --- a/src/Common/dataAccess/updateStoredProcedure.ts +++ b/src/Common/dataAccess/updateStoredProcedure.ts @@ -4,11 +4,11 @@ import { userContext } from "../../UserContext"; import { createUpdateSqlStoredProcedure, getSqlStoredProcedure, -} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +} from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { SqlStoredProcedureCreateUpdateParameters, SqlStoredProcedureResource, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +} from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/Common/dataAccess/updateTrigger.ts b/src/Common/dataAccess/updateTrigger.ts index d43463738..6d5afb4be 100644 --- a/src/Common/dataAccess/updateTrigger.ts +++ b/src/Common/dataAccess/updateTrigger.ts @@ -1,11 +1,8 @@ import { TriggerDefinition } from "@azure/cosmos"; import { AuthType } from "../../AuthType"; import { userContext } from "../../UserContext"; -import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { - SqlTriggerCreateUpdateParameters, - SqlTriggerResource, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; @@ -13,8 +10,8 @@ import { handleError } from "../ErrorHandlingUtils"; export async function updateTrigger( databaseId: string, collectionId: string, - trigger: TriggerDefinition -): Promise { + trigger: SqlTriggerResource +): Promise { const clearMessage = logConsoleProgress(`Updating trigger ${trigger.id}`); const { authType, useSDKOperations, apiType, subscriptionId, resourceGroup, databaseAccount } = userContext; try { @@ -31,7 +28,7 @@ export async function updateTrigger( if (getResponse?.properties?.resource) { const createTriggerParams: SqlTriggerCreateUpdateParameters = { properties: { - resource: trigger as SqlTriggerResource, + resource: trigger, options: {}, }, }; @@ -44,7 +41,7 @@ export async function updateTrigger( trigger.id, createTriggerParams ); - return rpResponse && (rpResponse.properties?.resource as TriggerDefinition); + return rpResponse && rpResponse.properties?.resource; } throw new Error(`Failed to update trigger: ${trigger.id} does not exist.`); @@ -54,7 +51,7 @@ export async function updateTrigger( .database(databaseId) .container(collectionId) .scripts.trigger(trigger.id) - .replace(trigger); + .replace((trigger as unknown) as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type return response?.resource; } catch (error) { handleError(error, "UpdateTrigger", `Error while updating trigger ${trigger.id}`); diff --git a/src/Common/dataAccess/updateUserDefinedFunction.ts b/src/Common/dataAccess/updateUserDefinedFunction.ts index 3b9449915..f3b28bf51 100644 --- a/src/Common/dataAccess/updateUserDefinedFunction.ts +++ b/src/Common/dataAccess/updateUserDefinedFunction.ts @@ -4,11 +4,11 @@ import { userContext } from "../../UserContext"; import { createUpdateSqlUserDefinedFunction, getSqlUserDefinedFunction, -} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +} from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { SqlUserDefinedFunctionCreateUpdateParameters, SqlUserDefinedFunctionResource, -} from "../../Utils/arm/generatedClients/2020-04-01/types"; +} from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index dc7d5f887..289e650cb 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -27,6 +27,7 @@ export interface ConfigContext { hostedExplorerURL: string; armAPIVersion?: string; allowedJunoOrigins: string[]; + msalRedirectURI?: string; } // Default configuration @@ -117,6 +118,14 @@ export async function initializeConfiguration(): Promise { const armAPIVersion = params.get("armAPIVersion") || ""; updateConfigContext({ armAPIVersion }); } + if (params.has("armEndpoint")) { + const ARM_ENDPOINT = params.get("armEndpoint") || ""; + updateConfigContext({ ARM_ENDPOINT }); + } + if (params.has("aadEndpoint")) { + const AAD_ENDPOINT = params.get("aadEndpoint") || ""; + updateConfigContext({ AAD_ENDPOINT }); + } if (params.has("platform")) { const platform = params.get("platform"); switch (platform) { diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index aece6a04a..efd5ffb78 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -9,6 +9,7 @@ export interface DatabaseAccount { export interface DatabaseAccountExtendedProperties { documentEndpoint?: string; + disableLocalAuth?: boolean; tableEndpoint?: string; gremlinEndpoint?: string; cassandraEndpoint?: string; @@ -20,6 +21,9 @@ export interface DatabaseAccountExtendedProperties { writeLocations?: DatabaseAccountResponseLocation[]; enableFreeTier?: boolean; enableAnalyticalStorage?: boolean; + isVirtualNetworkFilterEnabled?: boolean; + ipRules?: IpRule[]; + privateEndpointConnections?: unknown[]; } export interface DatabaseAccountResponseLocation { @@ -31,6 +35,10 @@ export interface DatabaseAccountResponseLocation { provisioningState: string; } +export interface IpRule { + ipAddressOrRange: string; +} + export interface ConfigurationOverrides { EnableBsonSchema: string; } @@ -161,7 +169,7 @@ export interface KeyResource { export interface IndexingPolicy { automatic: boolean; - indexingMode: string; + indexingMode: "consistent" | "lazy" | "none"; includedPaths: any; excludedPaths: any; compositeIndexes?: any; @@ -170,7 +178,7 @@ export interface IndexingPolicy { export interface PartitionKey { paths: string[]; - kind: string; + kind: "Hash" | "Range" | "MultiHash"; version: number; systemKey?: boolean; } @@ -385,16 +393,6 @@ 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; @@ -415,13 +413,6 @@ export interface AccountKeys { secondaryReadonlyMasterKey: string; } -export interface AfecFeature { - id: string; - name: string; - properties: { state: string }; - type: string; -} - export interface OperationStatus { status: string; id?: string; @@ -501,91 +492,6 @@ export interface MongoParameters extends RpParameters { 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/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts index 1689a96b5..d1c3dba58 100644 --- a/src/Contracts/ExplorerContracts.ts +++ b/src/Contracts/ExplorerContracts.ts @@ -1,6 +1,6 @@ -import * as Versions from "./Versions"; import * as ActionContracts from "./ActionContracts"; import * as Diagnostics from "./Diagnostics"; +import * as Versions from "./Versions"; /** * Messaging types used with Data Explorer <-> Portal communication diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 9fb242f79..210d34075 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -5,9 +5,8 @@ import { TriggerDefinition, UserDefinedFunctionDefinition, } from "@azure/cosmos"; -import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent"; import Explorer from "../Explorer/Explorer"; -import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData"; import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient"; import ConflictId from "../Explorer/Tree/ConflictId"; import DocumentId from "../Explorer/Tree/DocumentId"; @@ -15,6 +14,8 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import Trigger from "../Explorer/Tree/Trigger"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import { SelfServeType } from "../SelfServe/SelfServeUtils"; +import { CollectionCreationDefaults } from "../UserContext"; +import { SqlTriggerResource } from "../Utils/arm/generatedClients/cosmos/types"; import * as DataModels from "./DataModels"; import { SubscriptionType } from "./SubscriptionType"; @@ -88,7 +89,6 @@ export interface Database extends TreeNode { selectedSubnodeKind: ko.Observable; - selectDatabase(): void; expandDatabase(): Promise; collapseDatabase(): void; @@ -175,7 +175,7 @@ export interface Collection extends CollectionBase { createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure; createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction; - createTriggerNode(data: TriggerDefinition & Resource): Trigger; + createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger; findStoredProcedureWithId(sprocRid: string): StoredProcedure; findTriggerWithId(triggerRid: string): Trigger; findUserDefinedFunctionWithId(udfRid: string): UserDefinedFunction; @@ -274,8 +274,6 @@ export interface TabOptions { tabKind: CollectionTabKind; title: string; tabPath: string; - hashLocation: string; - onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void; isTabsContentExpanded?: ko.Observable; onLoadStartKey?: number; @@ -286,6 +284,7 @@ export interface TabOptions { rid?: string; node?: TreeNode; theme?: string; + index?: number; } export interface DocumentsTabOptions extends TabOptions { @@ -410,25 +409,6 @@ export interface SelfServeFrameInputs { flights?: readonly string[]; } -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; diff --git a/src/Explorer/ComponentRegisterer.test.ts b/src/Explorer/ComponentRegisterer.test.ts deleted file mode 100644 index ea9164787..000000000 --- a/src/Explorer/ComponentRegisterer.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -jest.mock("monaco-editor"); - -import * as ko from "knockout"; -import "./ComponentRegisterer"; - -describe("Component Registerer", () => { - it("should register json-editor component", () => { - expect(ko.components.isRegistered("json-editor")).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 9a49fd643..a58bd38a5 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -1,16 +1,8 @@ import * as ko from "knockout"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; -import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; -import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; -import * as PaneComponents from "./Panes/PaneComponents"; 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); - -// Panes -ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts deleted file mode 100644 index 1940d025f..000000000 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ /dev/null @@ -1,168 +0,0 @@ -import AddCollectionIcon from "../../images/AddCollection.svg"; -import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg"; -import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg"; -import AddTriggerIcon from "../../images/AddTrigger.svg"; -import AddUdfIcon from "../../images/AddUdf.svg"; -import DeleteCollectionIcon from "../../images/DeleteCollection.svg"; -import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg"; -import DeleteSprocIcon from "../../images/DeleteSproc.svg"; -import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; -import DeleteUDFIcon from "../../images/DeleteUDF.svg"; -import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; -import * as ViewModels from "../Contracts/ViewModels"; -import { userContext } from "../UserContext"; -import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent"; -import Explorer from "./Explorer"; -import StoredProcedure from "./Tree/StoredProcedure"; -import Trigger from "./Tree/Trigger"; -import UserDefinedFunction from "./Tree/UserDefinedFunction"; - -export interface CollectionContextMenuButtonParams { - databaseId: string; - collectionId: string; -} - -export interface DatabaseContextMenuButtonParams { - databaseId: string; -} -/** - * New resource tree (in ReactJS) - */ -export class ResourceTreeContextMenuButtonFactory { - public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] { - const items: TreeNodeMenuItem[] = [ - { - iconSrc: AddCollectionIcon, - onClick: () => container.onNewCollectionClicked(databaseId), - label: container.addCollectionText(), - }, - ]; - - if (userContext.apiType !== "Tables") { - items.push({ - iconSrc: DeleteDatabaseIcon, - onClick: () => container.openDeleteDatabaseConfirmationPane(), - label: container.deleteDatabaseText(), - styleClass: "deleteDatabaseMenuItem", - }); - } - return items; - } - - public static createCollectionContextMenuButton( - container: Explorer, - selectedCollection: ViewModels.Collection - ): TreeNodeMenuItem[] { - const items: TreeNodeMenuItem[] = []; - if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { - items.push({ - iconSrc: AddSqlQueryIcon, - onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null), - label: "New SQL Query", - }); - } - - if (userContext.apiType === "Mongo") { - items.push({ - iconSrc: AddSqlQueryIcon, - onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null), - label: "New Query", - }); - - items.push({ - iconSrc: HostedTerminalIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewMongoShellClick(); - }, - label: "New Shell", - }); - } - - if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { - items.push({ - iconSrc: AddStoredProcedureIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null); - }, - label: "New Stored Procedure", - }); - - items.push({ - iconSrc: AddUdfIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null); - }, - label: "New UDF", - }); - - items.push({ - iconSrc: AddTriggerIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null); - }, - label: "New Trigger", - }); - } - - items.push({ - iconSrc: DeleteCollectionIcon, - onClick: () => container.openDeleteCollectionConfirmationPane(), - label: container.deleteCollectionText(), - styleClass: "deleteCollectionMenuItem", - }); - - return items; - } - - public static createStoreProcedureContextMenuItems( - container: Explorer, - storedProcedure: StoredProcedure - ): TreeNodeMenuItem[] { - if (userContext.apiType === "Cassandra") { - return []; - } - - return [ - { - iconSrc: DeleteSprocIcon, - onClick: () => storedProcedure.delete(), - label: "Delete Store Procedure", - }, - ]; - } - - public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] { - if (userContext.apiType === "Cassandra") { - return []; - } - - return [ - { - iconSrc: DeleteTriggerIcon, - onClick: () => trigger.delete(), - label: "Delete Trigger", - }, - ]; - } - - public static createUserDefinedFunctionContextMenuItems( - container: Explorer, - userDefinedFunction: UserDefinedFunction - ): TreeNodeMenuItem[] { - if (userContext.apiType === "Cassandra") { - return []; - } - - return [ - { - iconSrc: DeleteUDFIcon, - onClick: () => userDefinedFunction.delete(), - label: "Delete User Defined Function", - }, - ]; - } -} diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx new file mode 100644 index 000000000..70d02e5aa --- /dev/null +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -0,0 +1,189 @@ +import React from "react"; +import AddCollectionIcon from "../../images/AddCollection.svg"; +import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg"; +import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg"; +import AddTriggerIcon from "../../images/AddTrigger.svg"; +import AddUdfIcon from "../../images/AddUdf.svg"; +import DeleteCollectionIcon from "../../images/DeleteCollection.svg"; +import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg"; +import DeleteSprocIcon from "../../images/DeleteSproc.svg"; +import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; +import DeleteUDFIcon from "../../images/DeleteUDF.svg"; +import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; +import * as ViewModels from "../Contracts/ViewModels"; +import { useSidePanel } from "../hooks/useSidePanel"; +import { userContext } from "../UserContext"; +import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; +import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent"; +import Explorer from "./Explorer"; +import { useNotebook } from "./Notebook/useNotebook"; +import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; +import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; +import StoredProcedure from "./Tree/StoredProcedure"; +import Trigger from "./Tree/Trigger"; +import UserDefinedFunction from "./Tree/UserDefinedFunction"; +import { useSelectedNode } from "./useSelectedNode"; + +export interface CollectionContextMenuButtonParams { + databaseId: string; + collectionId: string; +} + +export interface DatabaseContextMenuButtonParams { + databaseId: string; +} +/** + * New resource tree (in ReactJS) + */ +export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { + const items: TreeNodeMenuItem[] = [ + { + iconSrc: AddCollectionIcon, + onClick: () => container.onNewCollectionClicked(databaseId), + label: `New ${getCollectionName()}`, + }, + ]; + + if (userContext.apiType !== "Tables") { + items.push({ + iconSrc: DeleteDatabaseIcon, + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Delete " + getDatabaseName(), + container.refreshAllDatabases()} /> + ), + label: `Delete ${getDatabaseName()}`, + styleClass: "deleteDatabaseMenuItem", + }); + } + return items; +}; + +export const createCollectionContextMenuButton = ( + container: Explorer, + selectedCollection: ViewModels.Collection +): TreeNodeMenuItem[] => { + const items: TreeNodeMenuItem[] = []; + if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { + items.push({ + iconSrc: AddSqlQueryIcon, + onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined), + label: "New SQL Query", + }); + } + + if (userContext.apiType === "Mongo") { + items.push({ + iconSrc: AddSqlQueryIcon, + onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined), + label: "New Query", + }); + + items.push({ + iconSrc: HostedTerminalIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + if (useNotebook.getState().isShellEnabled) { + container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); + } else { + selectedCollection && selectedCollection.onNewMongoShellClick(); + } + }, + label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell", + }); + } + + if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { + items.push({ + iconSrc: AddStoredProcedureIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined); + }, + label: "New Stored Procedure", + }); + + items.push({ + iconSrc: AddUdfIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); + }, + label: "New UDF", + }); + + items.push({ + iconSrc: AddTriggerIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined); + }, + label: "New Trigger", + }); + } + + items.push({ + iconSrc: DeleteCollectionIcon, + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Delete " + getCollectionName(), + container.refreshAllDatabases()} /> + ), + label: `Delete ${getCollectionName()}`, + styleClass: "deleteCollectionMenuItem", + }); + + return items; +}; + +export const createStoreProcedureContextMenuItems = ( + container: Explorer, + storedProcedure: StoredProcedure +): TreeNodeMenuItem[] => { + if (userContext.apiType === "Cassandra") { + return []; + } + + return [ + { + iconSrc: DeleteSprocIcon, + onClick: () => storedProcedure.delete(), + label: "Delete Store Procedure", + }, + ]; +}; + +export const createTriggerContextMenuItems = (container: Explorer, trigger: Trigger): TreeNodeMenuItem[] => { + if (userContext.apiType === "Cassandra") { + return []; + } + + return [ + { + iconSrc: DeleteTriggerIcon, + onClick: () => trigger.delete(), + label: "Delete Trigger", + }, + ]; +}; + +export const createUserDefinedFunctionContextMenuItems = ( + container: Explorer, + userDefinedFunction: UserDefinedFunction +): TreeNodeMenuItem[] => { + if (userContext.apiType === "Cassandra") { + return []; + } + + return [ + { + iconSrc: DeleteUDFIcon, + onClick: () => userDefinedFunction.delete(), + label: "Delete User Defined Function", + }, + ]; +}; diff --git a/src/Explorer/Controls/Accordion/AccordionComponent.tsx b/src/Explorer/Controls/Accordion/AccordionComponent.tsx index 90685b6ba..f2b5c1b45 100644 --- a/src/Explorer/Controls/Accordion/AccordionComponent.tsx +++ b/src/Explorer/Controls/Accordion/AccordionComponent.tsx @@ -3,13 +3,14 @@ */ import * as React from "react"; -import * as Constants from "../../../Common/Constants"; import AnimateHeight from "react-animate-height"; - import TriangleDownIcon from "../../../../images/Triangle-down.svg"; import TriangleRightIcon from "../../../../images/Triangle-right.svg"; +import * as Constants from "../../../Common/Constants"; -export interface AccordionComponentProps {} +export interface AccordionComponentProps { + children: React.ReactNode; +} export class AccordionComponent extends React.Component { public render(): JSX.Element { @@ -27,12 +28,12 @@ export interface AccordionItemComponentProps { } interface AccordionItemComponentState { - isExpanded: boolean; + isExpanded?: boolean; } export class AccordionItemComponent extends React.Component { private static readonly durationMS = 500; - private isExpanded: boolean; + private isExpanded?: boolean; constructor(props: AccordionItemComponentProps) { super(props); @@ -79,7 +80,7 @@ export class AccordionItemComponent extends React.Component): void => { + private onHeaderClick = (): void => { this.setState({ isExpanded: !this.state.isExpanded }); }; diff --git a/src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx b/src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx deleted file mode 100644 index 04de9a716..000000000 --- a/src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { DefaultButton, IButtonStyles, IContextualMenuItem, IContextualMenuProps } from "@fluentui/react"; -import * as React from "react"; -import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; -import * as Logger from "../../../Common/Logger"; -import { ArcadiaWorkspace, SparkPool } from "../../../Contracts/DataModels"; - -export interface ArcadiaMenuPickerProps { - selectText?: string; - disableSubmenu?: boolean; - selectedSparkPool: string; - workspaces: ArcadiaWorkspaceItem[]; - onSparkPoolSelect: ( - e: React.MouseEvent | React.KeyboardEvent, - item: IContextualMenuItem - ) => boolean | void; - onCreateNewWorkspaceClicked: () => boolean | void; - onCreateNewSparkPoolClicked: (workspaceResourceId: string) => boolean | void; -} - -interface ArcadiaMenuPickerStates { - selectedSparkPool: string; -} - -export interface ArcadiaWorkspaceItem extends ArcadiaWorkspace { - sparkPools: SparkPool[]; -} - -export class ArcadiaMenuPicker extends React.Component { - constructor(props: ArcadiaMenuPickerProps) { - super(props); - this.state = { - selectedSparkPool: props.selectedSparkPool, - }; - } - - private _onSparkPoolClicked = ( - e: React.MouseEvent | React.KeyboardEvent, - item: IContextualMenuItem - ): boolean | void => { - try { - this.props.onSparkPoolSelect(e, item); - this.setState({ - selectedSparkPool: item.text, - }); - } catch (error) { - Logger.logError(getErrorMessage(error), "ArcadiaMenuPicker/_onSparkPoolClicked"); - throw error; - } - }; - - private _onCreateNewWorkspaceClicked = ( - e: React.MouseEvent | React.KeyboardEvent, - item: IContextualMenuItem - ): boolean | void => { - this.props.onCreateNewWorkspaceClicked(); - }; - - private _onCreateNewSparkPoolClicked = ( - e: React.MouseEvent | React.KeyboardEvent, - item: IContextualMenuItem - ): boolean | void => { - this.props.onCreateNewSparkPoolClicked(item.key); - }; - - public render() { - const { workspaces } = this.props; - let workspaceMenuItems: IContextualMenuItem[] = workspaces.map((workspace) => { - let sparkPoolsMenuProps: IContextualMenuProps = { - items: workspace.sparkPools.map( - (sparkpool): IContextualMenuItem => ({ - key: sparkpool.id, - text: sparkpool.name, - onClick: this._onSparkPoolClicked, - }) - ), - }; - if (!sparkPoolsMenuProps.items.length) { - sparkPoolsMenuProps.items.push({ - key: workspace.id, - text: "Create new spark pool", - onClick: this._onCreateNewSparkPoolClicked, - }); - } - - return { - key: workspace.id, - text: workspace.name, - subMenuProps: this.props.disableSubmenu ? undefined : sparkPoolsMenuProps, - }; - }); - - if (!workspaceMenuItems.length) { - workspaceMenuItems.push({ - key: "create_workspace", - text: "Create new workspace", - onClick: this._onCreateNewWorkspaceClicked, - }); - } - - const dropdownStyle: IButtonStyles = { - root: { - backgroundColor: "transparent", - margin: "auto 5px", - padding: "0", - border: "0", - }, - rootHovered: { - backgroundColor: "transparent", - }, - rootChecked: { - backgroundColor: "transparent", - }, - rootFocused: { - backgroundColor: "transparent", - }, - rootExpanded: { - backgroundColor: "transparent", - }, - flexContainer: { - height: "30px", - border: "1px solid #a6a6a6", - padding: "0 8px", - }, - label: { - fontWeight: "400", - fontSize: "12px", - }, - }; - - return ( - - ); - } -} diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index a514ffffd..2c0f6f283 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -1,15 +1,12 @@ -import * as StringUtils from "../../../Utils/StringUtils"; -import { KeyCodes } from "../../../Common/Constants"; -import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; -import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; - /** * React component for Command button component. */ - import * as React from "react"; -import { ArcadiaMenuPickerProps } from "../Arcadia/ArcadiaMenuPicker"; +import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; +import { KeyCodes } from "../../../Common/Constants"; +import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import * as StringUtils from "../../../Utils/StringUtils"; /** * Options for this component @@ -114,15 +111,6 @@ export interface CommandButtonComponentProps { * Aria-label for the button */ ariaLabel: string; - //TODO: generalize customized command bar - /** - * If set to true, will render arcadia picker - */ - isArcadiaPicker?: boolean; - /** - * props to render arcadia picker - */ - arcadiaProps?: ArcadiaMenuPickerProps; } export class CommandButtonComponent extends React.Component { @@ -133,8 +121,7 @@ export class CommandButtonComponent extends React.Component void; + closeDialog: () => void; +} + +export const useDialog: UseStore = create((set) => ({ + visible: false, + openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), + closeDialog: () => + set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), +})); export interface TextFieldProps extends ITextFieldProps { label: string; @@ -35,7 +50,6 @@ export interface DialogProps { title: string; subText: string; isModal: boolean; - visible: boolean; choiceGroupProps?: IChoiceGroupProps; textFieldProps?: TextFieldProps; linkProps?: LinkProps; @@ -56,24 +70,26 @@ const DIALOG_TITLE_FONT_SIZE = "17px"; const DIALOG_TITLE_FONT_WEIGHT = 400; const DIALOG_SUBTEXT_FONT_SIZE = "15px"; -export const Dialog: FunctionComponent = ({ - title, - subText, - isModal, - visible, - choiceGroupProps, - textFieldProps, - linkProps, - progressIndicatorProps, - primaryButtonText, - secondaryButtonText, - onPrimaryButtonClick, - onSecondaryButtonClick, - primaryButtonDisabled, - type, - showCloseButton, - onDismiss, -}: DialogProps) => { +export const Dialog: FC = () => { + const { visible, dialogProps: props } = useDialog(); + const { + title, + subText, + isModal, + choiceGroupProps, + textFieldProps, + linkProps, + progressIndicatorProps, + primaryButtonText, + secondaryButtonText, + onPrimaryButtonClick, + onSecondaryButtonClick, + primaryButtonDisabled, + type, + showCloseButton, + onDismiss, + } = props || {}; + const dialogProps: IDialogProps = { hidden: !visible, dialogContentProps: { @@ -105,7 +121,7 @@ export const Dialog: FunctionComponent = ({ } : {}; - return ( + return visible ? ( {choiceGroupProps && } {textFieldProps && } @@ -120,5 +136,7 @@ export const Dialog: FunctionComponent = ({ {secondaryButtonProps && } + ) : ( + <> ); }; diff --git a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx b/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx deleted file mode 100644 index 1fca6262d..000000000 --- a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from "react"; -import { shallow, mount } from "enzyme"; -import { DefaultDirectoryDropdownComponent, DefaultDirectoryDropdownProps } from "./DefaultDirectoryDropdownComponent"; -import { Tenant } from "../../../Contracts/DataModels"; - -const createBlankProps = (): DefaultDirectoryDropdownProps => { - return { - defaultDirectoryId: "", - directories: [], - onDefaultDirectoryChange: jest.fn(), - }; -}; - -const createBlankDirectory = (): Tenant => { - return { - countryCode: "", - displayName: "", - domains: [], - id: "", - tenantId: "", - }; -}; - -describe("test render", () => { - it("renders with no directories", () => { - const props = createBlankProps(); - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders with directories but no default", () => { - const props = createBlankProps(); - const tenant1 = createBlankDirectory(); - tenant1.displayName = "Microsoft"; - tenant1.tenantId = "asdfghjklzxcvbnm1234567890"; - const tenant2 = createBlankDirectory(); - tenant1.displayName = "Macrohard"; - tenant1.tenantId = "asdfghjklzxcvbnm9876543210"; - props.directories = [tenant1, tenant2]; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders with directories and default", () => { - const props = createBlankProps(); - const tenant1 = createBlankDirectory(); - tenant1.displayName = "Microsoft"; - tenant1.tenantId = "asdfghjklzxcvbnm1234567890"; - const tenant2 = createBlankDirectory(); - tenant1.displayName = "Macrohard"; - tenant1.tenantId = "asdfghjklzxcvbnm9876543210"; - props.directories = [tenant1, tenant2]; - - props.defaultDirectoryId = "asdfghjklzxcvbnm9876543210"; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders with directories and last visit default", () => { - const props = createBlankProps(); - const tenant1 = createBlankDirectory(); - tenant1.displayName = "Microsoft"; - tenant1.tenantId = "asdfghjklzxcvbnm1234567890"; - const tenant2 = createBlankDirectory(); - tenant1.displayName = "Macrohard"; - tenant1.tenantId = "asdfghjklzxcvbnm9876543210"; - props.directories = [tenant1, tenant2]; - - props.defaultDirectoryId = "lastVisited"; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); - -describe("test function", () => { - it("on default directory change", () => { - const props = createBlankProps(); - const tenant1 = createBlankDirectory(); - tenant1.displayName = "Microsoft"; - tenant1.tenantId = "asdfghjklzxcvbnm1234567890"; - const tenant2 = createBlankDirectory(); - tenant1.displayName = "Macrohard"; - tenant1.tenantId = "asdfghjklzxcvbnm9876543210"; - props.directories = [tenant1, tenant2]; - props.defaultDirectoryId = "lastVisited"; - - const wrapper = mount(); - - 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"); - expect(props.onDefaultDirectoryChange).toBeCalled(); - expect(props.onDefaultDirectoryChange).toHaveBeenCalled(); - - 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"); - expect(props.onDefaultDirectoryChange).toBeCalled(); - expect(props.onDefaultDirectoryChange).toHaveBeenCalled(); - }); -}); diff --git a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx b/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx deleted file mode 100644 index 1763a24b3..000000000 --- a/src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/** - * React component for Switch Directory - */ - -import { Dropdown, IDropdownOption, IDropdownProps } from "@fluentui/react"; -import * as React from "react"; -import _ from "underscore"; -import { Tenant } from "../../../Contracts/DataModels"; - -export interface DefaultDirectoryDropdownProps { - directories: Array; - defaultDirectoryId: string; - onDefaultDirectoryChange: (newDirectory: Tenant) => void; -} - -export class DefaultDirectoryDropdownComponent extends React.Component { - public static readonly lastVisitedKey: string = "lastVisited"; - - public render(): JSX.Element { - const lastVisitedOption: IDropdownOption = { - key: DefaultDirectoryDropdownComponent.lastVisitedKey, - text: "Sign in to your last visited directory", - }; - const directoryOptions: Array = this.props.directories.map( - (dirc): IDropdownOption => { - return { - key: dirc.tenantId, - text: `${dirc.displayName}(${dirc.tenantId})`, - }; - } - ); - const dropDownOptions: Array = [lastVisitedOption, ...directoryOptions]; - const dropDownProps: IDropdownProps = { - label: "Set your default directory", - options: dropDownOptions, - defaultSelectedKey: this.props.defaultDirectoryId ? this.props.defaultDirectoryId : lastVisitedOption.key, - onChange: this._onDropdownChange, - className: "defaultDirectoryDropdown", - }; - - return ; - } - - private _onDropdownChange = (e: React.FormEvent, option?: IDropdownOption, index?: number): void => { - if (!option || !option.key) { - return; - } - - if (option.key === this.props.defaultDirectoryId) { - return; - } - - if (option.key === DefaultDirectoryDropdownComponent.lastVisitedKey) { - this.props.onDefaultDirectoryChange({ - tenantId: option.key, - countryCode: undefined, - displayName: undefined, - domains: [], - id: undefined, - }); - return; - } - - const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === option.key); - if (!selectedDirectory) { - return; - } - - this.props.onDefaultDirectoryChange(selectedDirectory); - }; -} diff --git a/src/Explorer/Controls/Directory/DirectoryComponentAdapter.tsx b/src/Explorer/Controls/Directory/DirectoryComponentAdapter.tsx deleted file mode 100644 index 19df0c8fa..000000000 --- a/src/Explorer/Controls/Directory/DirectoryComponentAdapter.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as ko from "knockout"; -import * as React from "react"; -import { DirectoryListComponent, DirectoryListProps } from "./DirectoryListComponent"; -import { DefaultDirectoryDropdownComponent, DefaultDirectoryDropdownProps } from "./DefaultDirectoryDropdownComponent"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; - -export class DirectoryComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - constructor( - private _dropdownProps: ko.Observable, - private _listProps: ko.Observable - ) { - this._dropdownProps.subscribe(() => this.forceRender()); - this._listProps.subscribe(() => this.forceRender()); - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - return ( -
-
- -
-
-
- -
-
- ); - } - - public forceRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx b/src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx deleted file mode 100644 index 2ca392ff9..000000000 --- a/src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { shallow, mount } from "enzyme"; -import { DirectoryListComponent, DirectoryListProps } from "./DirectoryListComponent"; -import { Tenant } from "../../../Contracts/DataModels"; - -const createBlankProps = (): DirectoryListProps => { - return { - selectedDirectoryId: undefined, - directories: [], - onNewDirectorySelected: jest.fn(), - }; -}; - -const createBlankDirectory = (): Tenant => { - return { - countryCode: undefined, - displayName: undefined, - domains: [], - id: undefined, - tenantId: undefined, - }; -}; - -describe("test render", () => { - it("renders with no directories", () => { - const props = createBlankProps(); - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders with directories and selected", () => { - const props = createBlankProps(); - const tenant1 = createBlankDirectory(); - tenant1.displayName = "Microsoft"; - tenant1.tenantId = "asdfghjklzxcvbnm1234567890"; - const tenant2 = createBlankDirectory(); - tenant1.displayName = "Macrohard"; - tenant1.tenantId = "asdfghjklzxcvbnm9876543210"; - props.directories = [tenant1, tenant2]; - - props.selectedDirectoryId = "asdfghjklzxcvbnm9876543210"; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders with filters", () => { - const props = createBlankProps(); - const tenant1 = createBlankDirectory(); - tenant1.displayName = "Microsoft"; - tenant1.tenantId = "1234567890"; - const tenant2 = createBlankDirectory(); - tenant1.displayName = "Macrohard"; - tenant1.tenantId = "9876543210"; - props.directories = [tenant1, tenant2]; - props.selectedDirectoryId = "9876543210"; - - const wrapper = mount(); - wrapper.find("input.ms-TextField-field").simulate("change", { target: { value: "Macro" } }); - expect(wrapper).toMatchSnapshot(); - }); -}); - -describe("test function", () => { - it("on new directory selected", () => { - const props = createBlankProps(); - const tenant1 = createBlankDirectory(); - tenant1.displayName = "Microsoft"; - tenant1.tenantId = "asdfghjklzxcvbnm1234567890"; - props.directories = [tenant1]; - - const wrapper = mount(); - wrapper.find("button.directoryListButton").simulate("click"); - expect(props.onNewDirectorySelected).toBeCalled(); - expect(props.onNewDirectorySelected).toHaveBeenCalled(); - }); -}); diff --git a/src/Explorer/Controls/Directory/DirectoryListComponent.tsx b/src/Explorer/Controls/Directory/DirectoryListComponent.tsx deleted file mode 100644 index 03c4ae9e2..000000000 --- a/src/Explorer/Controls/Directory/DirectoryListComponent.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - DefaultButton, - IButtonProps, - ITextFieldProps, - List, - ScrollablePane, - Sticky, - StickyPositionType, - TextField, -} from "@fluentui/react"; -import * as React from "react"; -import _ from "underscore"; -import { Tenant } from "../../../Contracts/DataModels"; - -export interface DirectoryListProps { - directories: Array; - selectedDirectoryId: string; - onNewDirectorySelected: (newDirectory: Tenant) => void; -} - -export interface DirectoryListComponentState { - filterText: string; -} - -// onRenderCell is not called when selectedDirectoryId changed, so add a selected state to force render -interface ListTenant extends Tenant { - selected?: boolean; -} - -export class DirectoryListComponent extends React.Component { - constructor(props: DirectoryListProps) { - super(props); - - this.state = { - filterText: "", - }; - } - - public render(): JSX.Element { - const { directories: originalItems, selectedDirectoryId } = this.props; - const { filterText } = this.state; - const filteredItems = - originalItems && originalItems.length && filterText - ? originalItems.filter( - (directory) => - directory.displayName && - directory.displayName.toLowerCase().indexOf(filterText && filterText.toLowerCase()) >= 0 - ) - : originalItems; - const filteredItemsSelected = filteredItems.map((t) => { - let tenant: ListTenant = t; - tenant.selected = t.tenantId === selectedDirectoryId; - return tenant; - }); - - const textFieldProps: ITextFieldProps = { - className: "directoryListFilterTextBox", - placeholder: "Filter by directory name", - onChange: this._onFilterChanged, - ariaLabel: "Directory filter text box", - }; - - // TODO: add magnify glass to search bar with onRenderSuffix - return ( - - - - - - - ); - } - - private _onFilterChanged = (event: React.FormEvent, text?: string): void => { - this.setState({ - filterText: text, - }); - }; - - private _onRenderCell = (directory: ListTenant): JSX.Element => { - const buttonProps: IButtonProps = { - disabled: directory.selected || false, - className: "directoryListButton", - onClick: this._onNewDirectoryClick, - styles: { - root: { - backgroundColor: "transparent", - height: "auto", - borderBottom: "1px solid #ccc", - padding: "1px 0", - width: "100%", - }, - rootDisabled: { - backgroundColor: "#f1f1f8", - }, - rootHovered: { - backgroundColor: "rgba(85,179,255,.1)", - }, - flexContainer: { - height: "auto", - justifyContent: "flex-start", - }, - }, - }; - - return ( - -
-
{directory.displayName}
-
{directory.tenantId}
-
-
- ); - }; - - private _onNewDirectoryClick = (e: React.MouseEvent): void => { - if (!e || !e.currentTarget) { - return; - } - const buttonElement = e.currentTarget; - const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent; - const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === selectedDirectoryId); - - this.props.onNewDirectorySelected(selectedDirectory); - }; -} diff --git a/src/Explorer/Controls/Directory/__snapshots__/DefaultDirectoryDropdownComponent.test.tsx.snap b/src/Explorer/Controls/Directory/__snapshots__/DefaultDirectoryDropdownComponent.test.tsx.snap deleted file mode 100644 index 31ae98d55..000000000 --- a/src/Explorer/Controls/Directory/__snapshots__/DefaultDirectoryDropdownComponent.test.tsx.snap +++ /dev/null @@ -1,93 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`test render renders with directories and default 1`] = ` - -`; - -exports[`test render renders with directories and last visit default 1`] = ` - -`; - -exports[`test render renders with directories but no default 1`] = ` - -`; - -exports[`test render renders with no directories 1`] = ` - -`; diff --git a/src/Explorer/Controls/Directory/__snapshots__/DirectoryListComponent.test.tsx.snap b/src/Explorer/Controls/Directory/__snapshots__/DirectoryListComponent.test.tsx.snap deleted file mode 100644 index 78cf68ad7..000000000 --- a/src/Explorer/Controls/Directory/__snapshots__/DirectoryListComponent.test.tsx.snap +++ /dev/null @@ -1,2016 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`test render renders with directories and selected 1`] = ` - - - - - - -`; - -exports[`test render renders with filters 1`] = ` - - - -
-
-
- -
-
-
-
-
-
- - -
-
-
- -
-
-
-
-
-
-
-
- - -
-
-
-
- - - *": Object { - "left": 0, - "position": "relative", - "top": 0, - }, - }, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - }, - Object { - "height": "32px", - "minWidth": "80px", - }, - Object { - "backgroundColor": "#ffffff", - "color": "#323130", - }, - Object { - "backgroundColor": "transparent", - "borderBottom": "1px solid #ccc", - "height": "auto", - "padding": "1px 0", - "width": "100%", - }, - ], - "rootChecked": Object { - "backgroundColor": "#edebe9", - "color": "#201f1e", - }, - "rootCheckedHovered": Object { - "backgroundColor": "#edebe9", - "color": "#000000", - }, - "rootDisabled": Array [ - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid transparent", - "bottom": 2, - "content": "\\"\\"", - "left": 2, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 2, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "bottom": -2, - "left": -2, - "outlineColor": "ButtonText", - "right": -2, - "top": -2, - }, - }, - "top": 2, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "backgroundColor": "#f3f2f1", - "borderColor": "#f3f2f1", - "color": "#a19f9d", - "cursor": "default", - "selectors": Object { - ":focus": Object { - "outline": 0, - }, - ":hover": Object { - "outline": 0, - }, - }, - }, - Object { - "backgroundColor": "#f3f2f1", - "color": "#a19f9d", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - Object { - "backgroundColor": "#f1f1f8", - }, - ], - "rootExpanded": Object { - "backgroundColor": "#edebe9", - "color": "#201f1e", - }, - "rootHovered": Array [ - Object { - "backgroundColor": "#f3f2f1", - "color": "#201f1e", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "borderColor": "Highlight", - "color": "Highlight", - }, - }, - }, - Object { - "backgroundColor": "rgba(85,179,255,.1)", - }, - ], - "rootPressed": Object { - "backgroundColor": "#edebe9", - "color": "#201f1e", - }, - "screenReaderText": Object { - "border": 0, - "height": 1, - "margin": -1, - "overflow": "hidden", - "padding": 0, - "position": "absolute", - "width": 1, - }, - "splitButtonContainer": Array [ - Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - }, - }, - }, - Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - Object { - "display": "inline-flex", - "selectors": Object { - ".ms-Button--default": Object { - "borderBottomRightRadius": "0", - "borderRight": "none", - "borderTopRightRadius": "0", - }, - ".ms-Button--primary": Object { - "border": "none", - "borderBottomRightRadius": "0", - "borderTopRightRadius": "0", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "border": "1px solid WindowText", - "borderRightWidth": "0", - "color": "WindowText", - "forcedColorAdjust": "none", - }, - }, - }, - ".ms-Button--primary + .ms-Button": Object { - "border": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "1px solid WindowText", - "borderLeftWidth": "0", - }, - }, - }, - }, - }, - ], - "splitButtonContainerChecked": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerCheckedHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "WindowText", - "color": "Window", - "forcedColorAdjust": "none", - }, - }, - }, - }, - }, - "splitButtonContainerDisabled": Object { - "border": "none", - "outline": "none", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "MsHighContrastAdjust": "none", - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - "forcedColorAdjust": "none", - }, - }, - }, - "splitButtonContainerFocused": Object { - "outline": "none!important", - }, - "splitButtonContainerHovered": Object { - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Highlight", - "color": "Window", - }, - }, - }, - ".ms-Button.is-disabled": Object { - "color": "#a19f9d", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - }, - }, - "splitButtonDivider": Array [ - Object { - "backgroundColor": "#c8c6c4", - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "WindowText", - }, - }, - "top": 8, - "width": 1, - }, - Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "WindowText", - }, - }, - "top": 8, - "width": 1, - }, - ], - "splitButtonDividerDisabled": Array [ - Object { - "backgroundColor": "#c8c6c4", - }, - Object { - "bottom": 8, - "position": "absolute", - "right": 31, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "GrayText", - }, - }, - "top": 8, - "width": 1, - }, - ], - "splitButtonFlexContainer": Object { - "alignItems": "center", - "display": "flex", - "flexWrap": "nowrap", - "height": "100%", - "justifyContent": "center", - }, - "splitButtonMenuButton": Array [ - Object { - "backgroundColor": "transparent", - "color": "#ffffff", - "selectors": Object { - ":hover": Object { - "backgroundColor": "#edebe9", - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "color": "Highlight", - }, - }, - }, - }, - }, - Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - ".ms-Button-menuIcon": Object { - "color": "WindowText", - }, - }, - "border": "1px solid #8a8886", - "borderBottomRightRadius": "2px", - "borderLeft": "none", - "borderRadius": 0, - "borderTopRightRadius": "2px", - "boxSizing": "border-box", - "cursor": "pointer", - "display": "inline-block", - "height": "auto", - "marginBottom": 0, - "marginLeft": -1, - "marginRight": 0, - "marginTop": 0, - "outline": "transparent", - "padding": 6, - "textAlign": "center", - "textDecoration": "none", - "userSelect": "none", - "verticalAlign": "top", - "width": 32, - }, - ], - "splitButtonMenuButtonChecked": Object { - "backgroundColor": "#e1dfdd", - "selectors": Object { - ":hover": Object { - "backgroundColor": "#e1dfdd", - }, - }, - }, - "splitButtonMenuButtonDisabled": Array [ - Object { - "backgroundColor": "#f3f2f1", - "selectors": Object { - ":hover": Object { - "backgroundColor": "#f3f2f1", - }, - }, - }, - Object { - "border": "none", - "pointerEvents": "none", - "selectors": Object { - ".ms-Button--primary": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "borderColor": "GrayText", - "color": "GrayText", - }, - }, - }, - ".ms-Button-menuIcon": Object { - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "color": "GrayText", - }, - }, - }, - ":hover": Object { - "cursor": "default", - }, - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "backgroundColor": "Window", - "border": "1px solid GrayText", - "color": "GrayText", - }, - }, - }, - ], - "splitButtonMenuButtonExpanded": Object { - "backgroundColor": "#e1dfdd", - "selectors": Object { - ":hover": Object { - "backgroundColor": "#e1dfdd", - }, - }, - }, - "splitButtonMenuFocused": Object { - "outline": "transparent", - "position": "relative", - "selectors": Object { - ".ms-Fabric--isFocusVisible &:focus:after": Object { - "border": "1px solid #ffffff", - "bottom": 3, - "content": "\\"\\"", - "left": 3, - "outline": "1px solid #605e5c", - "position": "absolute", - "right": 3, - "selectors": Object { - "@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { - "border": "none", - "bottom": -2, - "left": -2, - "right": -2, - "top": -2, - }, - }, - "top": 3, - "zIndex": 1, - }, - "::-moz-focus-inner": Object { - "border": "0", - }, - }, - }, - "splitButtonMenuIcon": Object { - "color": "#323130", - }, - "splitButtonMenuIconDisabled": Object { - "color": "#a19f9d", - }, - "textContainer": Object { - "display": "block", - "flexGrow": 1, - }, - } - } - theme={ - Object { - "disableGlobalClassNames": false, - "effects": Object { - "elevation16": "0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108)", - "elevation4": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)", - "elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "roundedCorner2": "2px", - "roundedCorner4": "4px", - "roundedCorner6": "6px", - }, - "fonts": Object { - "large": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "18px", - "fontWeight": 400, - }, - "medium": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "14px", - "fontWeight": 400, - }, - "mediumPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "16px", - "fontWeight": 400, - }, - "mega": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "68px", - "fontWeight": 600, - }, - "small": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "smallPlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "12px", - "fontWeight": 400, - }, - "superLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "42px", - "fontWeight": 600, - }, - "tiny": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "20px", - "fontWeight": 600, - }, - "xLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "24px", - "fontWeight": 600, - }, - "xSmall": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "10px", - "fontWeight": 400, - }, - "xxLarge": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "28px", - "fontWeight": 600, - }, - "xxLargePlus": Object { - "MozOsxFontSmoothing": "grayscale", - "WebkitFontSmoothing": "antialiased", - "fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif", - "fontSize": "32px", - "fontWeight": 600, - }, - }, - "isInverted": false, - "palette": Object { - "accent": "#0078d4", - "black": "#000000", - "blackTranslucent40": "rgba(0,0,0,.4)", - "blue": "#0078d4", - "blueDark": "#002050", - "blueLight": "#00bcf2", - "blueMid": "#00188f", - "green": "#107c10", - "greenDark": "#004b1c", - "greenLight": "#bad80a", - "magenta": "#b4009e", - "magentaDark": "#5c005c", - "magentaLight": "#e3008c", - "neutralDark": "#201f1e", - "neutralLight": "#edebe9", - "neutralLighter": "#f3f2f1", - "neutralLighterAlt": "#faf9f8", - "neutralPrimary": "#323130", - "neutralPrimaryAlt": "#3b3a39", - "neutralQuaternary": "#d2d0ce", - "neutralQuaternaryAlt": "#e1dfdd", - "neutralSecondary": "#605e5c", - "neutralSecondaryAlt": "#8a8886", - "neutralTertiary": "#a19f9d", - "neutralTertiaryAlt": "#c8c6c4", - "orange": "#d83b01", - "orangeLight": "#ea4300", - "orangeLighter": "#ff8c00", - "purple": "#5c2d91", - "purpleDark": "#32145a", - "purpleLight": "#b4a0ff", - "red": "#e81123", - "redDark": "#a4262c", - "teal": "#008272", - "tealDark": "#004b50", - "tealLight": "#00b294", - "themeDark": "#005a9e", - "themeDarkAlt": "#106ebe", - "themeDarker": "#004578", - "themeLight": "#c7e0f4", - "themeLighter": "#deecf9", - "themeLighterAlt": "#eff6fc", - "themePrimary": "#0078d4", - "themeSecondary": "#2b88d8", - "themeTertiary": "#71afe5", - "white": "#ffffff", - "whiteTranslucent40": "rgba(255,255,255,.4)", - "yellow": "#ffb900", - "yellowDark": "#d29200", - "yellowLight": "#fff100", - }, - "rtl": undefined, - "semanticColors": Object { - "accentButtonBackground": "#0078d4", - "accentButtonText": "#ffffff", - "actionLink": "#323130", - "actionLinkHovered": "#201f1e", - "blockingBackground": "#FDE7E9", - "blockingIcon": "#FDE7E9", - "bodyBackground": "#ffffff", - "bodyBackgroundChecked": "#edebe9", - "bodyBackgroundHovered": "#f3f2f1", - "bodyDivider": "#edebe9", - "bodyFrameBackground": "#ffffff", - "bodyFrameDivider": "#edebe9", - "bodyStandoutBackground": "#faf9f8", - "bodySubtext": "#605e5c", - "bodyText": "#323130", - "bodyTextChecked": "#000000", - "buttonBackground": "#ffffff", - "buttonBackgroundChecked": "#c8c6c4", - "buttonBackgroundCheckedHovered": "#edebe9", - "buttonBackgroundDisabled": "#f3f2f1", - "buttonBackgroundHovered": "#f3f2f1", - "buttonBackgroundPressed": "#edebe9", - "buttonBorder": "#8a8886", - "buttonBorderDisabled": "#f3f2f1", - "buttonText": "#323130", - "buttonTextChecked": "#201f1e", - "buttonTextCheckedHovered": "#000000", - "buttonTextDisabled": "#a19f9d", - "buttonTextHovered": "#201f1e", - "buttonTextPressed": "#201f1e", - "cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)", - "cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)", - "cardStandoutBackground": "#ffffff", - "defaultStateBackground": "#faf9f8", - "disabledBackground": "#f3f2f1", - "disabledBodySubtext": "#c8c6c4", - "disabledBodyText": "#a19f9d", - "disabledBorder": "#c8c6c4", - "disabledSubtext": "#d2d0ce", - "disabledText": "#a19f9d", - "errorBackground": "#FDE7E9", - "errorIcon": "#A80000", - "errorText": "#a4262c", - "focusBorder": "#605e5c", - "infoBackground": "#f3f2f1", - "infoIcon": "#605e5c", - "inputBackground": "#ffffff", - "inputBackgroundChecked": "#0078d4", - "inputBackgroundCheckedHovered": "#005a9e", - "inputBorder": "#605e5c", - "inputBorderHovered": "#323130", - "inputFocusBorderAlt": "#0078d4", - "inputForegroundChecked": "#ffffff", - "inputIcon": "#0078d4", - "inputIconDisabled": "#a19f9d", - "inputIconHovered": "#005a9e", - "inputPlaceholderBackgroundChecked": "#deecf9", - "inputPlaceholderText": "#605e5c", - "inputText": "#323130", - "inputTextHovered": "#201f1e", - "link": "#0078d4", - "linkHovered": "#004578", - "listBackground": "#ffffff", - "listHeaderBackgroundHovered": "#f3f2f1", - "listHeaderBackgroundPressed": "#edebe9", - "listItemBackgroundChecked": "#edebe9", - "listItemBackgroundCheckedHovered": "#e1dfdd", - "listItemBackgroundHovered": "#f3f2f1", - "listText": "#323130", - "listTextColor": "#323130", - "menuBackground": "#ffffff", - "menuDivider": "#c8c6c4", - "menuHeader": "#0078d4", - "menuIcon": "#0078d4", - "menuItemBackgroundChecked": "#edebe9", - "menuItemBackgroundHovered": "#f3f2f1", - "menuItemBackgroundPressed": "#edebe9", - "menuItemText": "#323130", - "menuItemTextHovered": "#201f1e", - "messageLink": "#005A9E", - "messageLinkHovered": "#004578", - "messageText": "#323130", - "primaryButtonBackground": "#0078d4", - "primaryButtonBackgroundDisabled": "#f3f2f1", - "primaryButtonBackgroundHovered": "#106ebe", - "primaryButtonBackgroundPressed": "#005a9e", - "primaryButtonBorder": "transparent", - "primaryButtonText": "#ffffff", - "primaryButtonTextDisabled": "#d2d0ce", - "primaryButtonTextHovered": "#ffffff", - "primaryButtonTextPressed": "#ffffff", - "severeWarningBackground": "#FED9CC", - "severeWarningIcon": "#D83B01", - "smallInputBorder": "#605e5c", - "successBackground": "#DFF6DD", - "successIcon": "#107C10", - "successText": "#107C10", - "variantBorder": "#edebe9", - "variantBorderHovered": "#a19f9d", - "warningBackground": "#FFF4CE", - "warningHighlight": "#ffb900", - "warningIcon": "#797775", - "warningText": "#323130", - }, - "spacing": Object { - "l1": "20px", - "l2": "32px", - "m": "16px", - "s1": "8px", - "s2": "4px", - }, - } - } - variantClassName="ms-Button--default" - > - - - - - -
-
-
-
-
-
-
-
-
-
- - - -`; - -exports[`test render renders with no directories 1`] = ` - - - - - - -`; diff --git a/src/Explorer/Controls/DynamicList/DynamicList.test.ts b/src/Explorer/Controls/DynamicList/DynamicList.test.ts deleted file mode 100644 index 22400de08..000000000 --- a/src/Explorer/Controls/DynamicList/DynamicList.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as ko from "knockout"; -import { DynamicListComponent, DynamicListParams, DynamicListItem } from "./DynamicListComponent"; - -const $ = (selector: string) => document.querySelector(selector) as HTMLElement; - -function buildComponent(buttonOptions: any) { - document.body.innerHTML = DynamicListComponent.template as any; - const vm = new DynamicListComponent.viewModel(buttonOptions); - ko.applyBindings(vm); -} - -describe("Dynamic List Component", () => { - const mockPlaceHolder = "Write here"; - const mockButton = "Add something"; - const mockValue = "/someText"; - const mockAriaLabel = "Add ariaLabel"; - const items: ko.ObservableArray = ko.observableArray(); - - function buildListOptions( - items: ko.ObservableArray, - placeholder?: string, - mockButton?: string - ): DynamicListParams { - return { - placeholder: placeholder, - listItems: items, - buttonText: mockButton, - ariaLabel: mockAriaLabel, - }; - } - - afterEach(() => { - ko.cleanNode(document); - }); - - describe("Rendering", () => { - it("should display button text", () => { - const params = buildListOptions(items, mockPlaceHolder, mockButton); - buildComponent(params); - expect($(".dynamicListItemAdd").textContent).toContain(mockButton); - }); - }); - - describe("Behavior", () => { - it("should add items to the list", () => { - const params = buildListOptions(items, mockPlaceHolder, mockButton); - buildComponent(params); - $(".dynamicListItemAdd").click(); - expect(items().length).toBe(1); - const input = document.getElementsByClassName("dynamicListItem").item(0).children[0]; - input.setAttribute("value", mockValue); - input.dispatchEvent(new Event("change")); - input.dispatchEvent(new Event("blur")); - expect(items()[0].value()).toBe(mockValue); - }); - - it("should remove items from the list", () => { - const params = buildListOptions(items, mockPlaceHolder); - buildComponent(params); - $(".dynamicListItemDelete").click(); - expect(items().length).toBe(0); - }); - }); -}); diff --git a/src/Explorer/Controls/DynamicList/DynamicListComponent.less b/src/Explorer/Controls/DynamicList/DynamicListComponent.less deleted file mode 100644 index 5a9a12d1b..000000000 --- a/src/Explorer/Controls/DynamicList/DynamicListComponent.less +++ /dev/null @@ -1,59 +0,0 @@ -@import "../../../../less/Common/Constants"; - -.dynamicList { - width: 100%; - - .dynamicListContainer { - .dynamicListItem { - justify-content: space-around; - margin-bottom: @MediumSpace; - - input { - width: @newCollectionPaneInputWidth; - margin: auto; - font-size: @mediumFontSize; - padding: @SmallSpace @DefaultSpace; - color: @BaseDark; - } - - .dynamicListItemDelete { - padding: @SmallSpace @SmallSpace @DefaultSpace; - margin-left: @SmallSpace; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - img { - .dataExplorerIcons(); - } - } - } - } - - .dynamicListItemNew { - margin-top: @LargeSpace; - - .dynamicListItemAdd { - padding: @DefaultSpace; - cursor: pointer; - - &:hover { - .hover(); - } - - &:active { - .active(); - } - - img { - .dataExplorerIcons(); - margin: 0px @SmallSpace @SmallSpace 0px; - } - } - } -} \ No newline at end of file diff --git a/src/Explorer/Controls/DynamicList/DynamicListComponent.ts b/src/Explorer/Controls/DynamicList/DynamicListComponent.ts deleted file mode 100644 index 4407700d7..000000000 --- a/src/Explorer/Controls/DynamicList/DynamicListComponent.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Dynamic list: - * - * Creates a list of dynamic inputs that can be populated and deleted. - * - * How to use in your markup: - * - * - * - */ - -import * as ko from "knockout"; -import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel"; -import { KeyCodes } from "../../../Common/Constants"; -import template from "./dynamic-list.html"; - -/** - * Parameters for this component - */ -export interface DynamicListParams { - /** - * Observable list of items to update - */ - listItems: ko.ObservableArray; - - /** - * Placeholder text to use on inputs - */ - placeholder?: string; - - /** - * Text to use as aria-label - */ - ariaLabel: string; - - /** - * Text for the button to add items - */ - buttonText?: string; - - /** - * Callback triggered when the template is bound to the component (for testing purposes) - */ - onTemplateReady?: () => void; -} - -/** - * Item in the dynamic list - */ -export interface DynamicListItem { - value: ko.Observable; -} - -export class DynamicListViewModel extends WaitsForTemplateViewModel { - public placeholder: string; - public ariaLabel: string; - public buttonText: string; - public newItem: ko.Observable; - public isTemplateReady: ko.Observable; - public listItems: ko.ObservableArray; - - public constructor(options: DynamicListParams) { - super(); - super.onTemplateReady((isTemplateReady: boolean) => { - if (isTemplateReady && options.onTemplateReady) { - options.onTemplateReady(); - } - }); - - const params: DynamicListParams = options; - const paramsPlaceholder: string = params.placeholder; - const paramsButtonText: string = params.buttonText; - this.placeholder = paramsPlaceholder || "Write a value"; - this.ariaLabel = "Unique keys"; - this.buttonText = paramsButtonText || "Add item"; - this.listItems = params.listItems || ko.observableArray(); - this.newItem = ko.observable(""); - } - - public removeItem = (data: any, event: MouseEvent | KeyboardEvent): void => { - const context = ko.contextFor(event.target as Node); - this.listItems.splice(context.$index(), 1); - document.getElementById("addUniqueKeyBtn").focus(); - }; - - public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.removeItem(data, event); - (document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus(); - event.stopPropagation(); - return false; - } - return true; - }; - - public addItem(): void { - this.listItems.push({ value: ko.observable("") }); - (document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus(); - } - - public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.addItem(); - event.stopPropagation(); - return false; - } - return true; - }; -} - -/** - * Helper class for ko component registration - */ -export const DynamicListComponent = { - viewModel: DynamicListViewModel, - template, -}; diff --git a/src/Explorer/Controls/DynamicList/dynamic-list.html b/src/Explorer/Controls/DynamicList/dynamic-list.html deleted file mode 100644 index 1ae30adde..000000000 --- a/src/Explorer/Controls/DynamicList/dynamic-list.html +++ /dev/null @@ -1,34 +0,0 @@ -
-
-
- - - Remove item - -
-
-
- - - -
-
diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 71273ed20..53ba3e106 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -1,6 +1,11 @@ +import { Spinner, SpinnerSize } from "@fluentui/react"; import * as React from "react"; import { loadMonaco, monaco } from "../../LazyMonaco"; +// import "./EditorReact.less"; +interface EditorReactStates { + showEditor: boolean; +} export interface EditorReactProps { language: string; content: string; @@ -12,22 +17,26 @@ export interface EditorReactProps { theme?: string; // Monaco editor theme } -export class EditorReact extends React.Component { +export class EditorReact extends React.Component { private rootNode: HTMLElement; private editor: monaco.editor.IStandaloneCodeEditor; private selectionListener: monaco.IDisposable; public constructor(props: EditorReactProps) { super(props); + this.state = { + showEditor: false, + }; } public componentDidMount(): void { this.createEditor(this.configureEditor.bind(this)); } - public shouldComponentUpdate(): boolean { - // Prevents component re-rendering - return false; + public componentDidUpdate(previous: EditorReactProps) { + if (this.props.content !== previous.content) { + this.editor.setValue(this.props.content); + } } public componentWillUnmount(): void { @@ -35,14 +44,19 @@ export class EditorReact extends React.Component { } public render(): JSX.Element { - return
this.setRef(elt)} />; + return ( + + {!this.state.showEditor && } +
this.setRef(elt)} /> + + ); } protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { this.editor = editor; const queryEditorModel = this.editor.getModel(); if (!this.props.isReadOnly && this.props.onContentChanged) { - queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => { + queryEditorModel.onDidChangeContent(() => { const queryEditorModel = this.editor.getModel(); this.props.onContentChanged(queryEditorModel.getValue()); }); @@ -76,6 +90,12 @@ export class EditorReact extends React.Component { this.rootNode.innerHTML = ""; const monaco = await loadMonaco(); createCallback(monaco.editor.create(this.rootNode, options)); + + if (this.rootNode.innerHTML) { + this.setState({ + showEditor: true, + }); + } } private setRef(element: HTMLElement): void { diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx index 29e1a2900..d6b28775d 100644 --- a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx +++ b/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx @@ -56,7 +56,7 @@ export class GitHubReposComponent extends React.Component -
{content}
+
{content}
{!this.props.showAuthorizeAccess && ( <>
diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx deleted file mode 100644 index aa8cabde5..000000000 --- a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent"; - -export class GitHubReposComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - 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/InputTypeahead/InputTypeaheadComponent.tsx b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx index 22aa71388..69f01a745 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx +++ b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx @@ -6,9 +6,10 @@ * typeaheadOverrideOptions: { dynamic:false } * */ +import "jquery-typeahead"; import * as React from "react"; -import "./InputTypeahead.less"; import { KeyCodes } from "../../../Common/Constants"; +import "./InputTypeahead.less"; export interface Item { caption: string; diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx index 627041b38..e991fe05c 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx @@ -1,154 +1,90 @@ +import { shallow } from "enzyme"; +import React from "react"; import * as DataModels from "../../../Contracts/DataModels"; -import { NotebookTerminalComponent } from "./NotebookTerminalComponent"; +import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent"; -const createTestDatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: null, - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - gremlinEndpoint: null, - tableEndpoint: null, - }, - type: "testType", - }; +const testAccount: DataModels.DatabaseAccount = { + id: "id", + kind: "kind", + location: "location", + name: "name", + properties: { + documentEndpoint: "https://testDocumentEndpoint.azure.com/", + }, + type: "type", }; -const createTestMongo32DatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: null, - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - gremlinEndpoint: null, - tableEndpoint: null, - }, - type: "testType", - }; +const testMongo32Account: DataModels.DatabaseAccount = { + ...testAccount, }; -const createTestMongo36DatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: null, - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - gremlinEndpoint: null, - tableEndpoint: null, - mongoEndpoint: "https://testMongoEndpoint.azure.com/", - }, - type: "testType", - }; +const testMongo36Account: DataModels.DatabaseAccount = { + ...testAccount, + properties: { + mongoEndpoint: "https://testMongoEndpoint.azure.com/", + }, }; -const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: "https://testCassandraEndpoint.azure.com/", - documentEndpoint: null, - gremlinEndpoint: null, - tableEndpoint: null, - }, - type: "testType", - }; +const testCassandraAccount: DataModels.DatabaseAccount = { + ...testAccount, + properties: { + cassandraEndpoint: "https://testCassandraEndpoint.azure.com/", + }, }; -const createTerminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/", - }, - databaseAccount: createTestDatabaseAccount(), - }); +const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: "authToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com", }; -const createMongo32Terminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", - }, - databaseAccount: createTestMongo32DatabaseAccount(), - }); +const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: "authToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", }; -const createMongo36Terminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", - }, - databaseAccount: createTestMongo36DatabaseAccount(), - }); -}; - -const createCassandraTerminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra", - }, - databaseAccount: createTestCassandraDatabaseAccount(), - }); +const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: "authToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra", }; describe("NotebookTerminalComponent", () => { - it("getTerminalParams: Test for terminal", () => { - const terminal: NotebookTerminalComponent = createTerminal(); - const params: Map = terminal.getTerminalParams(); + it("renders terminal", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testAccount, + notebookServerInfo: testNotebookServerInfo, + }; - expect(params).toEqual( - new Map([["terminal", "true"]]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); - it("getTerminalParams: Test for Mongo 3.2 terminal", () => { - const terminal: NotebookTerminalComponent = createMongo32Terminal(); - const params: Map = terminal.getTerminalParams(); + it("renders mongo 3.2 shell", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testMongo32Account, + notebookServerInfo: testMongoNotebookServerInfo, + }; - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host], - ]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); - it("getTerminalParams: Test for Mongo 3.6 terminal", () => { - const terminal: NotebookTerminalComponent = createMongo36Terminal(); - const params: Map = terminal.getTerminalParams(); + it("renders mongo 3.6 shell", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testMongo36Account, + notebookServerInfo: testMongoNotebookServerInfo, + }; - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host], - ]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); - it("getTerminalParams: Test for Cassandra terminal", () => { - const terminal: NotebookTerminalComponent = createCassandraTerminal(); - const params: Map = terminal.getTerminalParams(); + it("renders cassandra shell", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testCassandraAccount, + notebookServerInfo: testCassandraNotebookServerInfo, + }; - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host], - ]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx index a7b51e896..637f24192 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx @@ -2,12 +2,12 @@ * Wrapper around Notebook server terminal */ +import postRobot from "post-robot"; import * as React from "react"; import * as DataModels from "../../../Contracts/DataModels"; -import * as StringUtils from "../../../Utils/StringUtils"; +import { TerminalProps } from "../../../Terminal/TerminalProps"; import { userContext } from "../../../UserContext"; -import { TerminalQueryParams } from "../../../Common/Constants"; -import { handleError } from "../../../Common/ErrorHandlingUtils"; +import * as StringUtils from "../../../Utils/StringUtils"; export interface NotebookTerminalComponentProps { notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; @@ -15,79 +15,69 @@ export interface NotebookTerminalComponentProps { } export class NotebookTerminalComponent extends React.Component { + private terminalWindow: Window; + constructor(props: NotebookTerminalComponentProps) { super(props); } + componentDidMount(): void { + this.sendPropsToTerminalFrame(); + } + public render(): JSX.Element { return (
diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTab.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTab.tsx new file mode 100644 index 000000000..d40965849 --- /dev/null +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTab.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import * as DataModels from "../../../Contracts/DataModels"; +import type { TabOptions } from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; +import Explorer from "../../Explorer"; +import TabsBase from "../TabsBase"; +import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent"; + +export interface IMongoShellTabProps { + container: Explorer; +} + +export class NewMongoShellTab extends TabsBase { + public queryText: string; + public currentQuery: string; + public partitionKey: DataModels.PartitionKey; + public iMongoShellTabComponentProps: IMongoShellTabComponentProps; + public iMongoShellTabAccessor: IMongoShellTabAccessor; + + constructor(options: TabOptions, private props: IMongoShellTabProps) { + super(options); + this.iMongoShellTabComponentProps = { + collection: this.collection, + tabsBaseInstance: this, + container: this.props.container, + onMongoShellTabAccessor: (instance: IMongoShellTabAccessor) => { + this.iMongoShellTabAccessor = instance; + }, + }; + } + + public render(): JSX.Element { + return ; + } + + public onTabClick(): void { + useTabs.getState().activateTab(this); + this.iMongoShellTabAccessor.onTabClickEvent(); + } +} diff --git a/src/Explorer/Tabs/MongoShellTab.ts b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx similarity index 57% rename from src/Explorer/Tabs/MongoShellTab.ts rename to src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index 48167b571..7ae8b5982 100644 --- a/src/Explorer/Tabs/MongoShellTab.ts +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -1,67 +1,101 @@ -import * as ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import { configContext, Platform } from "../../ConfigContext"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import { isInvalidParentFrameOrigin, isReadyMessage } from "../../Utils/MessageValidation"; -import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import Explorer from "../Explorer"; -import template from "./MongoShellTab.html"; -import TabsBase from "./TabsBase"; +import React, { Component } from "react"; +import * as Constants from "../../../Common/Constants"; +import { configContext, Platform } from "../../../ConfigContext"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../../UserContext"; +import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/MessageValidation"; +import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; +import Explorer from "../../Explorer"; +import TabsBase from "../TabsBase"; -export default class MongoShellTab extends TabsBase { - public readonly html = template; - public url: ko.Computed; - private _container: Explorer; +//eslint-disable-next-line +class MessageType { + static IframeReady = "iframeready"; + static Notification = "notification"; + static Log = "log"; +} + +//eslint-disable-next-line +class LogType { + static Information = "information"; + static Warning = "warning"; + static Verbose = "verbose"; + static InProgress = "inprogress"; + static StartTrace = "start"; + static SuccessTrace = "success"; + static FailureTrace = "failure"; +} + +export interface IMongoShellTabAccessor { + onTabClickEvent: () => void; +} + +export interface IMongoShellTabComponentStates { + url: string; +} + +export interface IMongoShellTabComponentProps { + collection: ViewModels.CollectionBase; + tabsBaseInstance: TabsBase; + container: Explorer; + onMongoShellTabAccessor: (instance: IMongoShellTabAccessor) => void; +} + +export default class MongoShellTabComponent extends Component< + IMongoShellTabComponentProps, + IMongoShellTabComponentStates +> { private _runtimeEndpoint: string; private _logTraces: Map; - constructor(options: ViewModels.TabOptions) { - super(options); + constructor(props: IMongoShellTabComponentProps) { + super(props); this._logTraces = new Map(); - this._container = options.collection.container; - this.url = ko.computed(() => { - const { databaseAccount: account } = userContext; - const resourceId = account?.id; - const accountName = account?.name; - const mongoEndpoint = 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 (userContext.portalEnv === "localhost") { - baseUrl = "/content/mongoshell/"; - } + this.state = { + url: this.getURL(), + }; - return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; + props.onMongoShellTabAccessor({ + onTabClickEvent: this.onTabClick.bind(this), }); 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 getURL(): string { + const { databaseAccount: account } = userContext; + const resourceId = account?.id; + const accountName = account?.name; + const mongoEndpoint = 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 (userContext.portalEnv === "localhost") { + baseUrl = "/content/mongoshell/"; + } + + return `${extensionEndpoint}${baseUrl}index.html?resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; } + //eslint-disable-next-line + public setContentFocus(event: React.SyntheticEvent): void {} + public onTabClick(): void { - super.onTabClick(); - this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); + this.props.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); } - public handleMessage(event: MessageEvent) { + public handleMessage(event: MessageEvent): void { if (isInvalidParentFrameOrigin(event)) { return; } - const shellIframe: HTMLIFrameElement = document.getElementById(this.tabId); + const shellIframe: HTMLIFrameElement = document.getElementById( + this.props.tabsBaseInstance.tabId + ) as HTMLIFrameElement; if (!shellIframe) { return; @@ -73,9 +107,9 @@ export default class MongoShellTab extends TabsBase { return; } - if (event.data.eventType == MessageType.IframeReady) { + if (event.data.eventType === MessageType.IframeReady) { this.handleReadyMessage(event, shellIframe); - } else if (event.data.eventType == MessageType.Notification) { + } else if (event.data.eventType === MessageType.Notification) { this.handleNotificationMessage(event, shellIframe); } else { this.handleLogMessage(event, shellIframe); @@ -98,8 +132,8 @@ export default class MongoShellTab extends TabsBase { 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 databaseId = this.props.collection.databaseId; + const collectionId = this.props.collection.id(); const apiEndpoint = configContext.BACKEND_ENDPOINT; const encryptedAuthToken: string = userContext.accessToken; @@ -121,6 +155,7 @@ export default class MongoShellTab extends TabsBase { ); } + //eslint-disable-next-line private handleLogMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") { return; @@ -144,6 +179,7 @@ export default class MongoShellTab extends TabsBase { TelemetryProcessor.trace(Action.MongoShell, ActionModifiers.Mark, dataToLog); break; case LogType.StartTrace: + //eslint-disable-next-line const telemetryTraceId: number = TelemetryProcessor.traceStart(Action.MongoShell, dataToLog); this._logTraces.set(shellTraceId, telemetryTraceId); break; @@ -168,6 +204,7 @@ export default class MongoShellTab extends TabsBase { } } + //eslint-disable-next-line private handleNotificationMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { if (!("logType" in event.data.data) || typeof event.data.data["logType"] !== "string") { return; @@ -188,20 +225,19 @@ export default class MongoShellTab extends TabsBase { return logConsoleProgress(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"; + render(): JSX.Element { + return ( + + ); + } } diff --git a/src/Explorer/Tabs/NotebookTabBase.ts b/src/Explorer/Tabs/NotebookTabBase.ts index ed36da3fa..de019eb08 100644 --- a/src/Explorer/Tabs/NotebookTabBase.ts +++ b/src/Explorer/Tabs/NotebookTabBase.ts @@ -6,6 +6,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import Explorer from "../Explorer"; import { NotebookClientV2 } from "../Notebook/NotebookClientV2"; +import { useNotebook } from "../Notebook/useNotebook"; import TabsBase from "./TabsBase"; export interface NotebookTabBaseOptions extends ViewModels.TabOptions { @@ -28,7 +29,7 @@ export default class NotebookTabBase extends TabsBase { if (!NotebookTabBase.clientManager) { NotebookTabBase.clientManager = new NotebookClientV2({ - connectionInfo: this.container.notebookServerInfo(), + connectionInfo: useNotebook.getState().notebookServerInfo, databaseAccountName: userContext?.databaseAccount?.name, defaultExperience: userContext.apiType, contentProvider: this.container.notebookManager?.notebookContentProvider, diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index a43ba3568..012406760 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -1,7 +1,6 @@ import { stringifyNotebook, toJS } from "@nteract/commutable"; import * as ko from "knockout"; import * as Q from "q"; -import * as _ from "underscore"; import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import CutIcon from "../../../images/notebook/Notebook-cut.svg"; @@ -12,14 +11,9 @@ import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg"; import RunIcon from "../../../images/notebook/Notebook-run.svg"; import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; -import { ArmApiVersions } from "../../Common/Constants"; -import { configContext } from "../../ConfigContext"; -import * as DataModels from "../../Contracts/DataModels"; import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore"; -import { trackEvent } from "../../Shared/appInsights"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; @@ -29,6 +23,7 @@ import * as CdbActions from "../Notebook/NotebookComponent/actions"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { NotebookContentItem } from "../Notebook/NotebookContentItem"; +import { useNotebook } from "../Notebook/useNotebook"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; export interface NotebookTabOptions extends NotebookTabBaseOptions { @@ -38,7 +33,6 @@ export interface NotebookTabOptions extends NotebookTabBaseOptions { export default class NotebookTabV2 extends NotebookTabBase { public readonly html = '
'; public notebookPath: ko.Observable; - private selectedSparkPool: ko.Observable; private notebookComponentAdapter: NotebookComponentAdapter; constructor(options: NotebookTabOptions) { @@ -46,23 +40,16 @@ export default class NotebookTabV2 extends NotebookTabBase { this.container = options.container; this.notebookPath = ko.observable(options.notebookContentItem.path); - this.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received.")); + useNotebook.subscribe( + () => logConsoleInfo("New notebook server info received."), + (state) => state.notebookServerInfo + ); this.notebookComponentAdapter = new NotebookComponentAdapter({ contentItem: options.notebookContentItem, - notebooksBasePath: this.container.getNotebookBasePath(), + notebooksBasePath: useNotebook.getState().notebookBasePath, notebookClient: NotebookTabBase.clientManager, onUpdateKernelInfo: this.onKernelUpdate, }); - - this.selectedSparkPool = ko.observable(null); - this.container && - this.container.arcadiaToken.subscribe(async () => { - const currentKernel = this.notebookComponentAdapter.getCurrentKernelName(); - if (!currentKernel) { - return; - } - await this.configureServiceEndpoints(currentKernel); - }); } public onCloseTabButtonClick(): Q.Promise { @@ -361,32 +348,6 @@ export default class NotebookTabV2 extends NotebookTabBase { }, // TODO: Uncomment when undo/redo is reimplemented in nteract ]; - - if (this.container.hasStorageAnalyticsAfecFeature()) { - const arcadiaWorkspaceDropdown: CommandButtonComponentProps = { - iconSrc: null, - iconAlt: workspaceLabel, - ariaLabel: workspaceLabel, - onCommandClick: () => {}, - commandButtonLabel: null, - hasPopup: false, - disabled: this.container.arcadiaWorkspaces.length < 1, - isDropdown: false, - isArcadiaPicker: true, - arcadiaProps: { - selectedSparkPool: this.selectedSparkPool(), - workspaces: this.container.arcadiaWorkspaces(), - onSparkPoolSelect: this.onSparkPoolSelect, - onCreateNewWorkspaceClicked: () => { - this.container.createWorkspace(); - }, - onCreateNewSparkPoolClicked: (workspaceResourceId: string) => { - this.container.createSparkPool(workspaceResourceId); - }, - }, - }; - buttons.splice(1, 0, arcadiaWorkspaceDropdown); - } return buttons; } @@ -394,50 +355,6 @@ export default class NotebookTabV2 extends NotebookTabBase { this.updateNavbarWithTabsButtons(); } - private onSparkPoolSelect = (evt: React.MouseEvent | React.KeyboardEvent, item: any) => { - if (!item || !item.text) { - this.selectedSparkPool(null); - return; - } - - trackEvent( - { name: "SparkPoolSelected" }, - { - subscriptionId: userContext.subscriptionId, - accountName: userContext.databaseAccount?.name, - accountId: userContext.databaseAccount?.id, - } - ); - - this.container && - this.container.arcadiaWorkspaces && - this.container.arcadiaWorkspaces() && - this.container.arcadiaWorkspaces().forEach(async (workspace) => { - if (workspace && workspace.name && workspace.sparkPools) { - const selectedPoolIndex = _.findIndex(workspace.sparkPools, (pool) => pool && pool.name === item.text); - if (selectedPoolIndex >= 0) { - const selectedPool = workspace.sparkPools[selectedPoolIndex]; - if (selectedPool && selectedPool.name) { - this.container.sparkClusterConnectionInfo({ - userName: undefined, - password: undefined, - endpoints: [ - { - endpoint: `https://${workspace.name}.${configContext.ARCADIA_LIVY_ENDPOINT_DNS_ZONE}/livyApi/versions/${ArmApiVersions.arcadiaLivy}/sparkPools/${selectedPool.name}/`, - kind: DataModels.SparkClusterEndpointKind.Livy, - }, - ], - }); - this.selectedSparkPool(item.text); - await this.reconfigureServiceEndpoints(); - this.container.sparkClusterConnectionInfo.valueHasMutated(); - return; - } - } - } - }); - }; - private onKernelUpdate = async () => { await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName()).catch((reason) => { /* Erroring is ok here */ @@ -446,8 +363,8 @@ export default class NotebookTabV2 extends NotebookTabBase { }; private async configureServiceEndpoints(kernelName: string) { - const notebookConnectionInfo = this.container && this.container.notebookServerInfo(); - const sparkClusterConnectionInfo = this.container && this.container.sparkClusterConnectionInfo(); + const notebookConnectionInfo = useNotebook.getState().notebookServerInfo; + const sparkClusterConnectionInfo = useNotebook.getState().sparkClusterConnectionInfo; await NotebookConfigurationUtils.configureServiceEndpoints( this.notebookPath(), notebookConnectionInfo, diff --git a/src/Explorer/Tabs/QueryTab.html b/src/Explorer/Tabs/QueryTab.html deleted file mode 100644 index 9adb59c2d..000000000 --- a/src/Explorer/Tabs/QueryTab.html +++ /dev/null @@ -1,335 +0,0 @@ -
-
-
- 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.less b/src/Explorer/Tabs/QueryTab.less deleted file mode 100644 index f672ef530..000000000 --- a/src/Explorer/Tabs/QueryTab.less +++ /dev/null @@ -1,311 +0,0 @@ -@import "../../../less/Common/Constants"; -@import "../../../less/Common/TabCommon"; - -@MongoQueryEditorHeight: 50px; -@ResultsTextFontWeight: 600; -@ToggleHeight: 30px; -@ToggleWidth: 250px; -@QueryEngineExeInfo: 305px; - -.tab-pane { - .tabContentContainer(); - - .tabPaneContentContainer { - .tabContentContainer(); - - .mongoQueryHelper { - margin:@DefaultSpace 0px 0px 44px; - position: absolute; - top: 115px; //this is to avoid the jump of query editor - } - - .queryEditorWithSplitter { - .flex-display(); - .flex-direction(); - flex-shrink: 0; - height: 100%; - width: 100%; - margin-left: @SmallSpace; - - .queryEditor { - .flex-display(); - height: 100%; - width: 100%; - margin-top: @SmallSpace; - - .jsonEditor { - border: none; - margin-top: @SmallSpace; - } - } - - .queryEditor.mongoQueryEditor { - margin-top: 32px; - overflow: hidden; - } - - .queryEditorHorizontalSplitter { - margin: auto; - display: block; - } - } - - .queryErrorsHeaderContainer { - padding: 24px @LargeSpace 0px @MediumSpace; - - .queryErrors { - font-size: @mediumFontSize; - list-style-type: none; - color: @BaseDark; - font-weight: bold; - margin-left: 24px; - } - } - - .queryResultErrorContentContainer { - .flex-display(); - .flex-direction(); - font-size: @DefaultFontSize; - padding: @DefaultSpace; - height: 100%; - width: 100%; - overflow: hidden; - - .queryEditorWatermark { - text-align: center; - margin: auto; - height: 25vh; // this is to align the water mark in center of the layout. - - p { - margin-bottom: @LargeSpace; - color: @BaseHigh; - } - - .queryEditorWatermarkText { - color: @BaseHigh; - font-size: @DefaultFontSize; - font-family: @DataExplorerFont; - } - } - - .queryResultsErrorsContent { - height: 100%; - margin-left: @MediumSpace; - .flex-display(); - .flex-direction(); - - - .togglesWithMetadata { - margin-top: @MediumSpace; - - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin-left: @MediumSpace; - - &:focus { - .focus(); - } - - .tab { - margin-right: @MediumSpace; - } - - .toggleSwitch { - .toggleSwitch(); - } - - .selectedToggle { - .selectedToggle(); - } - - .unselectedToggle { - .unselectedToggle(); - } - } - } - - .result-metadata { - padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace; - - .queryResultDivider { - margin-left: @SmallSpace; - margin-right: @SmallSpace; - } - - .queryResultNextEnable { - color: @AccentMediumHigh; - font-size: @mediumFontSize; - cursor: pointer; - - img { - height: @ImgHeight; - width: @ImgWidth; - margin-left: @SmallSpace; - } - } - - .queryResultNextDisable { - color: @BaseMediumHigh; - opacity: 0.5; - font-size: @mediumFontSize; - - img { - height: @ImgHeight; - width: @ImgWidth; - margin-left: @SmallSpace; - } - } - } - - .tab-pane.active { - height: 100%; - width: 100%; - } - - .errorContent { - .flex-display(); - width: 60%; - white-space: nowrap; - font-size: @mediumFontSize; - padding: 0px @MediumSpace 0px @MediumSpace; - - .errorMessage { - padding: @SmallSpace; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .errorDetailsLink { - cursor: pointer; - padding: @SmallSpace; - } - - .queryMetricsSummaryContainer { - .flex-display(); - .flex-direction(); - overflow: hidden; - - .queryMetricsSummary { - margin: @LargeSpace @LargeSpace 0px @DefaultSpace; - table-layout: fixed; - display: block; - height: auto; - overflow-y: auto; - overflow-x: hidden; - - caption { - width: 100px; - } - - .queryMetricsSummaryHead { - .flex-display(); - } - - .queryMetricsSummaryHeader.queryMetricsSummaryTuple { - font-size: 10px; - } - - .queryMetricsSummaryBody { - .flex-display(); - .flex-direction(); - } - - .queryMetricsSummaryTuple { - border-bottom: 1px solid @BaseMedium; - height: 32px; - font-size: 12px; - width: 100%; - .flex-display(); - th, td { - padding: @DefaultSpace; - - &:nth-child(1) { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex: 0 0 50%; - } - - &:nth-child(3) { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex: 0 0 50%; - } - - .queryMetricInfoTooltip { - .infoTooltip(); - - &:hover .queryMetricTooltipText { - .tooltipVisible(); - } - - &:focus .queryMetricTooltipText { - .tooltipVisible(); - } - - .queryMetricTooltipText { - top: -50px; - width: auto; - white-space: nowrap; - left: 6px; - visibility: hidden; - background-color: @BaseHigh; - color: @BaseLight; - position: absolute; - z-index: 1; - padding: @MediumSpace; - - &::after { - border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; - bottom: -14px; - .tooltipTextAfter(); - } - } - - .queryEngineExeTimeInfo { - width: @QueryEngineExeInfo; - top: -85px; - white-space: pre-wrap; - } - } - } - } - } - - .downloadMetricsLinkContainer { - margin: 24px 0px 24px @MediumSpace; - - #downloadMetricsLink { - color: @BaseHigh; - padding: @DefaultSpace; - font-size: @mediumFontSize; - border: @ButtonBorderWidth solid @BaseLight; - cursor: pointer; - - &:hover { - .hover(); - } - - &:active { - border: @ButtonBorderWidth dashed @AccentMedium; - .active(); - } - } - } - } - - json-editor { - .flex-display(); - .flex-direction(); - overflow: hidden; - height: 100%; - width: 100%; - padding: @SmallSpace 0px @SmallSpace @MediumSpace; - } - } - } - } -} \ No newline at end of file diff --git a/src/Explorer/Tabs/QueryTab.test.ts b/src/Explorer/Tabs/QueryTab.test.ts deleted file mode 100644 index c146f6d6f..000000000 --- a/src/Explorer/Tabs/QueryTab.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as ko from "knockout"; -import { DatabaseAccount } from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { updateUserContext } from "../../UserContext"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import Explorer from "../Explorer"; -import QueryTab from "./QueryTab"; - -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: "", - 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", () => { - updateUserContext({}); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.isQueryMetricsEnabled()).toBe(true); - }); - - it("should be false for accounts using other APIs", () => { - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableGremlin" }], - }, - } as DatabaseAccount, - }); - 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", () => { - updateUserContext({}); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.saveQueryButton.visible()).toBe(true); - }); - - it("should not be visible when using an unsupported API", () => { - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableMongo" }], - }, - } as DatabaseAccount, - }); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.saveQueryButton.visible()).toBe(false); - }); - }); -}); diff --git a/src/Explorer/Tabs/QueryTab.ts b/src/Explorer/Tabs/QueryTab.ts deleted file mode 100644 index bf85f257c..000000000 --- a/src/Explorer/Tabs/QueryTab.ts +++ /dev/null @@ -1,593 +0,0 @@ -import * as ko from "knockout"; -import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; -import SaveQueryIcon from "../../../images/save-cosmos.svg"; -import * as Constants from "../../Common/Constants"; -import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; -import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; -import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import * as QueryUtils from "../../Utils/QueryUtils"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import template from "./QueryTab.html"; -import TabsBase from "./TabsBase"; - -enum ToggleState { - Result, - QueryMetrics, -} - -export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate { - public readonly html = template; - 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 Map()); - this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))); - this.isQueryMetricsEnabled = ko.computed(() => { - return userContext.apiType === "SQL" || 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 userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; - }); - - 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 onSaveQueryClick = (): void => { - this.collection && this.collection.container && this.collection.container.openSaveQueryPanel(); - }; - - public onSavedQueriesClick = (): void => { - this.collection && this.collection.container && this.collection.container.openBrowseQueriesPanel(); - }; - - 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, { - 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, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - } catch (error) { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - this.error(errorMessage); - TelemetryProcessor.traceFailure( - Action.ExecuteQuery, - { - 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: Map): DataModels.QueryMetrics { - if (!metricsMap) { - return null; - } - - const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics(); - metricsMap.forEach((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 = 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((queryMetric, partitionKeyRangeId) => { - 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/QueryTab/QueryTab.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.tsx new file mode 100644 index 000000000..11d609c87 --- /dev/null +++ b/src/Explorer/Tabs/QueryTab/QueryTab.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import * as DataModels from "../../../Contracts/DataModels"; +import type { QueryTabOptions } from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; +import Explorer from "../../Explorer"; +import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent"; +import TabsBase from "../TabsBase"; +import QueryTabComponent from "./QueryTabComponent"; + +export interface IQueryTabProps { + container: Explorer; +} + +export class NewQueryTab extends TabsBase { + public queryText: string; + public currentQuery: string; + public partitionKey: DataModels.PartitionKey; + public iQueryTabComponentProps: IQueryTabComponentProps; + public iTabAccessor: ITabAccessor; + + constructor(options: QueryTabOptions, private props: IQueryTabProps) { + super(options); + this.partitionKey = options.partitionKey; + this.iQueryTabComponentProps = { + collection: this.collection, + isExecutionError: this.isExecutionError(), + tabId: this.tabId, + tabsBaseInstance: this, + queryText: options.queryText, + partitionKey: this.partitionKey, + container: this.props.container, + onTabAccessor: (instance: ITabAccessor): void => { + this.iTabAccessor = instance; + }, + isPreferredApiMongoDB: false, + }; + } + + public render(): JSX.Element { + return ; + } + + public onTabClick(): void { + useTabs.getState().activateTab(this); + this.iTabAccessor.onTabClickEvent(); + } + + public onCloseTabButtonClick(): void { + useTabs.getState().closeTab(this); + if (this.iTabAccessor) { + this.iTabAccessor.onCloseClickEvent(true); + } + } + + public getContainer(): Explorer { + return this.props.container; + } +} diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.less b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less new file mode 100644 index 000000000..13daf455c --- /dev/null +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less @@ -0,0 +1,285 @@ +@import "../../../../less/Common/Constants.less"; +@import "../../../../less/Common/TabCommon.less"; + +@MongoQueryEditorHeight: 50px; +@ResultsTextFontWeight: 600; +@ToggleHeight: 30px; +@ToggleWidth: 250px; +@QueryEngineExeInfo: 305px; + +.tab-pane { + .tabContentContainer(); + + .tabPaneContentContainer { + position: relative; + .tabContentContainer(); + + .mongoQueryHelper { + margin: @DefaultSpace 0px 0px 44px; + } + + .splitter-layout { + .layout-pane-primary { + overflow: hidden !important; + .queryEditor { + .flex-display(); + height: 100%; + width: 100%; + margin-top: @SmallSpace; + + .jsonEditor { + border: none; + margin-top: @SmallSpace; + } + } + } + + .queryEditor.mongoQueryEditor { + margin-top: 32px; + overflow: hidden; + } + + .queryEditorHorizontalSplitter { + margin: auto; + display: block; + } + } + + .queryErrorsHeaderContainer { + padding: 24px @LargeSpace 0px @MediumSpace; + + .queryErrors { + font-size: @mediumFontSize; + list-style-type: none; + color: @BaseDark; + font-weight: bold; + margin-left: 24px; + } + } + + .queryResultErrorContentContainer { + .flex-display(); + .flex-direction(); + font-size: @DefaultFontSize; + padding: @DefaultSpace; + height: 100%; + width: 100%; + overflow: hidden; + + .queryEditorWatermark { + text-align: center; + margin: auto; + height: 25vh; // this is to align the water mark in center of the layout. + + p { + margin-bottom: @LargeSpace; + color: @BaseHigh; + } + + .queryEditorWatermarkText { + color: @BaseHigh; + font-size: @DefaultFontSize; + font-family: @DataExplorerFont; + } + } + + .queryResultsErrorsContent { + height: 100%; + margin-left: @MediumSpace; + .flex-display(); + .flex-direction(); + + div[role="tabpanel"] { + height: 100%; + div:nth-child(1) { + height: 100%; + } + } + + .result-metadata { + padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace; + height: auto !important; + .queryResultDivider { + margin-left: @SmallSpace; + margin-right: @SmallSpace; + } + + .queryResultNextEnable { + color: @AccentMediumHigh; + font-size: @mediumFontSize; + cursor: pointer; + + img { + height: @ImgHeight; + width: @ImgWidth; + margin-left: @SmallSpace; + } + } + + .queryResultNextDisable { + color: @BaseMediumHigh; + opacity: 0.5; + font-size: @mediumFontSize; + + img { + height: @ImgHeight; + width: @ImgWidth; + margin-left: @SmallSpace; + } + } + } + + .tab-pane.active { + height: 100%; + width: 100%; + } + + .errorContent { + .flex-display(); + width: 60%; + white-space: nowrap; + font-size: @mediumFontSize; + padding: 0px @MediumSpace 0px @MediumSpace; + + .errorMessage { + padding: @SmallSpace; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .errorDetailsLink { + cursor: pointer; + padding: @SmallSpace; + } + + .queryMetricsSummaryContainer { + .flex-display(); + .flex-direction(); + overflow: hidden; + position: relative; + height: 100%; + + .queryMetricsSummary { + margin: @LargeSpace @LargeSpace 0px @DefaultSpace; + table-layout: fixed; + display: block; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + + caption { + width: 100px; + } + + .queryMetricsSummaryHead { + .flex-display(); + } + + .queryMetricsSummaryHeader.queryMetricsSummaryTuple { + font-size: 10px; + } + + .queryMetricsSummaryBody { + .flex-display(); + .flex-direction(); + } + + .queryMetricsSummaryTuple { + border-bottom: 1px solid @BaseMedium; + height: 32px; + font-size: 12px; + width: 100%; + .flex-display(); + th, + td { + padding: @DefaultSpace; + + &:nth-child(1) { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 0 0 50%; + } + + &:nth-child(3) { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 0 0 50%; + } + + .queryMetricInfoTooltip { + .infoTooltip(); + + &:hover .queryMetricTooltipText { + .tooltipVisible(); + } + + &:focus .queryMetricTooltipText { + .tooltipVisible(); + } + + .queryMetricTooltipText { + top: -50px; + width: auto; + white-space: nowrap; + left: 6px; + visibility: hidden; + background-color: @BaseHigh; + color: @BaseLight; + position: absolute; + z-index: 1; + padding: @MediumSpace; + + &::after { + border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; + bottom: -14px; + .tooltipTextAfter(); + } + } + + .queryEngineExeTimeInfo { + width: @QueryEngineExeInfo; + top: -85px; + white-space: pre-wrap; + } + } + } + } + } + + .downloadMetricsLinkContainer { + margin: 24px 0px 50px @MediumSpace; + position: sticky; + #downloadMetricsLink { + color: @BaseHigh; + padding: @DefaultSpace; + font-size: @mediumFontSize; + border: @ButtonBorderWidth solid @BaseLight; + cursor: pointer; + + &:hover { + .hover(); + } + + &:active { + border: @ButtonBorderWidth dashed @AccentMedium; + .active(); + } + } + } + } + + json-editor { + .flex-display(); + .flex-direction(); + overflow: hidden; + height: 100%; + width: 100%; + padding: @SmallSpace 0px @SmallSpace @MediumSpace; + } + } + } + } +} diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx new file mode 100644 index 000000000..4f9a63dcf --- /dev/null +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -0,0 +1,1076 @@ +import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react"; +import React, { Fragment } from "react"; +import SplitterLayout from "react-splitter-layout"; +import "react-splitter-layout/lib/index.css"; +import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg"; +import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; +import InfoColor from "../../../../images/info_color.svg"; +import QueryEditorNext from "../../../../images/Query-Editor-Next.svg"; +import RunQuery from "../../../../images/RunQuery.png"; +import SaveQueryIcon from "../../../../images/save-cosmos.svg"; +import * as Constants from "../../../Common/Constants"; +import { NormalizedEventKey } from "../../../Common/Constants"; +import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; +import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; +import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; +import * as HeadersUtility from "../../../Common/HeadersUtility"; +import { MinimalQueryIterator } from "../../../Common/IteratorUtilities"; +import { queryIterator } from "../../../Common/MongoProxyClient"; +import MongoUtility from "../../../Common/MongoUtility"; +import { Splitter } from "../../../Common/Splitter"; +import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; +import * as DataModels from "../../../Contracts/DataModels"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; +import { useSidePanel } from "../../../hooks/useSidePanel"; +import { userContext } from "../../../UserContext"; +import * as QueryUtils from "../../../Utils/QueryUtils"; +import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; +import { EditorReact } from "../../Controls/Editor/EditorReact"; +import Explorer from "../../Explorer"; +import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; +import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; +import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane"; +import TabsBase from "../TabsBase"; +import "./QueryTabComponent.less"; + +enum ToggleState { + Result, + QueryMetrics, +} + +export interface IDocument { + metric: string; + value: string; + toolTip: string; + isQueryMetricsEnabled: boolean; +} + +export interface ITabAccessor { + onTabClickEvent: () => void; + onSaveClickEvent: () => string; + onCloseClickEvent: (isClicked: boolean) => void; +} + +export interface Button { + visible: boolean; + enabled: boolean; + isSelected?: boolean; +} + +export interface IQueryTabComponentProps { + collection: ViewModels.CollectionBase; + isExecutionError: boolean; + tabId: string; + tabsBaseInstance: TabsBase; + queryText: string; + partitionKey: DataModels.PartitionKey; + container: Explorer; + activeTab?: TabsBase; + onTabAccessor: (instance: ITabAccessor) => void; + isPreferredApiMongoDB?: boolean; + monacoEditorSetting?: string; + viewModelcollection?: ViewModels.Collection; +} + +interface IQueryTabStates { + queryMetrics: Map; + aggregatedQueryMetrics: DataModels.QueryMetrics; + activityId: string; + roundTrips: number; + toggleState: ToggleState; + isQueryMetricsEnabled: boolean; + showingDocumentsDisplayText: string; + requestChargeDisplayText: string; + initialEditorContent: string; + sqlQueryEditorContent: string; + selectedContent: string; + _executeQueryButtonTitle: string; + sqlStatementToExecute: string; + queryResults: string; + statusMessge: string; + statusIcon: string; + allResultsMetadata: ViewModels.QueryResultsMetadata[]; + error: string; + isTemplateReady: boolean; + _isSaveQueriesEnabled: boolean; + isExecutionError: boolean; + isExecuting: boolean; + columns: IColumn[]; + items: IDocument[]; +} + +export default class QueryTabComponent extends React.Component { + public queryEditorId: string; + public executeQueryButton: Button; + public saveQueryButton: Button; + public splitterId: string; + public splitter: Splitter; + public isPreferredApiMongoDB: boolean; + public resultsDisplay: string; + protected monacoSettings: ViewModels.MonacoEditorSettings; + protected _iterator: MinimalQueryIterator; + private _resourceTokenPartitionKey: string; + _partitionKey: DataModels.PartitionKey; + public maybeSubQuery: boolean; + public isCloseClicked: boolean; + public allItems: IDocument[]; + public defaultQueryText: string; + + constructor(props: IQueryTabComponentProps) { + super(props); + const columns: IColumn[] = [ + { + key: "column1", + name: "", + minWidth: 16, + maxWidth: 16, + data: String, + fieldName: "toolTip", + onRender: this.onRenderColumnItem, + }, + { + key: "column2", + name: "METRIC", + minWidth: 200, + data: String, + fieldName: "metric", + }, + { + key: "column3", + name: "VALUE", + minWidth: 200, + data: String, + fieldName: "value", + }, + ]; + + if (this.props.isPreferredApiMongoDB) { + this.defaultQueryText = props.queryText; + } else { + this.defaultQueryText = props.queryText !== void 0 ? props.queryText : "SELECT * FROM c"; + } + + this.state = { + queryMetrics: new Map(), + aggregatedQueryMetrics: undefined, + activityId: "", + roundTrips: undefined, + toggleState: ToggleState.Result, + isQueryMetricsEnabled: userContext.apiType === "SQL" || false, + showingDocumentsDisplayText: this.resultsDisplay, + requestChargeDisplayText: "", + initialEditorContent: this.defaultQueryText, + sqlQueryEditorContent: this.defaultQueryText, + selectedContent: "", + _executeQueryButtonTitle: "Execute Query", + sqlStatementToExecute: this.defaultQueryText, + queryResults: "", + statusMessge: "", + statusIcon: "", + allResultsMetadata: [], + error: "", + isTemplateReady: false, + _isSaveQueriesEnabled: userContext.apiType === "SQL" || userContext.apiType === "Gremlin", + isExecutionError: this.props.isExecutionError, + isExecuting: false, + columns: columns, + items: [], + }; + this.isCloseClicked = false; + this.splitterId = this.props.tabId + "_splitter"; + this.queryEditorId = `queryeditor${this.props.tabId}`; + this._partitionKey = props.partitionKey; + this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; + this.monacoSettings = new ViewModels.MonacoEditorSettings(this.props.monacoEditorSetting, false); + + this.executeQueryButton = { + enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, + visible: true, + }; + const sql = this.state.sqlQueryEditorContent; + this.maybeSubQuery = sql && /.*\(.*SELECT.*\)/i.test(sql); + + this.saveQueryButton = { + enabled: this.state._isSaveQueriesEnabled, + visible: this.state._isSaveQueriesEnabled, + }; + + this._buildCommandBarOptions(); + props.onTabAccessor({ + onTabClickEvent: this.onTabClick.bind(this), + onSaveClickEvent: this.getCurrentEditorQuery.bind(this), + onCloseClickEvent: this.onCloseClick.bind(this), + }); + } + + public onRenderColumnItem(item: IDocument): JSX.Element { + if (item.toolTip !== "") { + return {`${item.toolTip}`}; + } else { + return undefined; + } + } + + public generateDetailsList(): IDocument[] { + const items: IDocument[] = []; + const allItems: IDocument[] = [ + { + metric: "Request Charge", + value: this.state.requestChargeDisplayText, + toolTip: "", + isQueryMetricsEnabled: true, + }, + { + metric: "Showing Results", + value: this.state.showingDocumentsDisplayText, + toolTip: "", + isQueryMetricsEnabled: true, + }, + { + metric: "Retrieved document count", + value: + this.state.aggregatedQueryMetrics.retrievedDocumentCount !== undefined + ? this.state.aggregatedQueryMetrics.retrievedDocumentCount.toString() + : "", + toolTip: "Total number of retrieved documents", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Retrieved document size", + value: + this.state.aggregatedQueryMetrics.retrievedDocumentSize !== undefined + ? this.state.aggregatedQueryMetrics.retrievedDocumentSize.toString() + " bytes" + : "", + toolTip: "Total size of retrieved documents in bytes", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Output document count", + value: + this.state.aggregatedQueryMetrics.outputDocumentCount !== undefined + ? this.state.aggregatedQueryMetrics.outputDocumentCount.toString() + : "", + toolTip: "Number of output documents", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Output document size", + value: + this.state.aggregatedQueryMetrics.outputDocumentSize !== undefined + ? this.state.aggregatedQueryMetrics.outputDocumentSize.toString() + " bytes" + : "", + toolTip: "Total size of output documents in bytes", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Index hit document count", + value: + this.state.aggregatedQueryMetrics.indexHitDocumentCount !== undefined + ? this.state.aggregatedQueryMetrics.indexHitDocumentCount.toString() + : "", + toolTip: "Total number of documents matched by the filter", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Index lookup time", + value: + this.state.aggregatedQueryMetrics.indexLookupTime !== undefined + ? this.state.aggregatedQueryMetrics.indexLookupTime.toString() + " ms" + : "", + toolTip: "Time spent in physical index layer", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Document load time", + value: + this.state.aggregatedQueryMetrics.documentLoadTime !== undefined + ? this.state.aggregatedQueryMetrics.documentLoadTime.toString() + " ms" + : "", + toolTip: "Time spent in loading documents", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Query engine execution time", + value: + this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime !== undefined + ? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.toString() + " ms" + : "", + toolTip: + "Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "System function execution time", + value: + this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime !== undefined + ? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.toString() + " ms" + : "", + toolTip: "Total time spent executing system (built-in) functions", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "User defined function execution time", + value: + this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime !== undefined + ? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.toString() + + " ms" + : "", + toolTip: "Total time spent executing user-defined functions", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Document write time", + value: + this.state.aggregatedQueryMetrics.documentWriteTime !== undefined + ? this.state.aggregatedQueryMetrics.documentWriteTime.toString() + " ms" + : "", + toolTip: "Time spent to write query result set to response buffer", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Round Trips", + value: this.state.roundTrips ? this.state.roundTrips.toString() : "", + toolTip: "", + isQueryMetricsEnabled: true, + }, + { + metric: "Activity id", + value: this.state.activityId ? this.state.activityId : "", + toolTip: "", + isQueryMetricsEnabled: true, + }, + ]; + + allItems.forEach((item) => { + if (item.metric === "Round Trips" || item.metric === "Activity id") { + if (item.metric === "Round Trips" && this.state.roundTrips !== undefined) { + items.push(item); + } else if (item.metric === "Activity id" && this.state.activityId !== undefined) { + items.push(item); + } + } else { + if (item.isQueryMetricsEnabled) { + items.push(item); + } + } + }); + return items; + } + + public onCloseClick(isClicked: boolean): void { + this.isCloseClicked = isClicked; + } + + public getCurrentEditorQuery(): string { + return this.state.sqlQueryEditorContent; + } + + public onTabClick(): void { + setTimeout(() => { + if (!this.isCloseClicked) { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } else { + this.isCloseClicked = false; + } + }, 0); + } + + public onExecuteQueryClick = async (): Promise => { + const sqlStatement = this.state.selectedContent || this.state.sqlQueryEditorContent; + + this.setState({ + sqlStatementToExecute: sqlStatement, + allResultsMetadata: [], + queryResults: "", + }); + + this._iterator = undefined; + setTimeout(async () => { + await this._executeQueryDocumentsPage(0); + }, 100); + }; + + public onSaveQueryClick = (): void => { + useSidePanel.getState().openSidePanel("Save Query", ); + }; + + public onSavedQueriesClick = (): void => { + useSidePanel + .getState() + .openSidePanel("Open Saved Queries", ); + }; + + public async onFetchNextPageClick(): Promise { + const allResultsMetadata = (this.state.allResultsMetadata && this.state.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); + } + + //eslint-disable-next-line + public onErrorDetailsClick = (): boolean => { + useNotificationConsole.getState().expandConsole(); + + return false; + }; + + public onErrorDetailsKeyPress = (event: React.KeyboardEvent): boolean => { + if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { + this.onErrorDetailsClick(); + return false; + } + + return true; + }; + + public toggleResult(): void { + this.setState({ + toggleState: ToggleState.Result, + }); + } + + public toggleMetrics(): void { + this.setState({ + toggleState: ToggleState.QueryMetrics, + }); + } + + public onToggleKeyDown = (event: React.KeyboardEvent): boolean => { + if (event.key === NormalizedEventKey.LeftArrow) { + this.toggleResult(); + event.stopPropagation(); + return false; + } else if (event.key === NormalizedEventKey.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.state.toggleState === ToggleState.Result; + } + + public isMetricsToggled(): boolean { + return this.state.toggleState === ToggleState.QueryMetrics; + } + + public onDownloadQueryMetricsCsvClick = (): boolean => { + this._downloadQueryMetricsCsvData(); + return false; + }; + + public onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent): boolean => { + if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) { + this._downloadQueryMetricsCsvData(); + return false; + } + + return true; + }; + + //eslint-disable-next-line + private async _executeQueryDocumentsPage(firstItemIndex: number): Promise { + this.setState({ + error: "", + roundTrips: undefined, + }); + + if (this._iterator === undefined) { + if (this.isPreferredApiMongoDB) { + this._initIteratorMongo(); + } else { + this._initIterator(); + } + } + + await this._queryDocumentsPage(firstItemIndex); + } + + private async _queryDocumentsPage(firstItemIndex: number): Promise { + let results: string; + + this.props.tabsBaseInstance.isExecutionError(false); + this.setState({ + isExecutionError: false, + }); + this._resetAggregateQueryMetrics(); + + const queryDocuments = async (firstItemIndex: number) => + await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex); + this.props.tabsBaseInstance.isExecuting(true); + this.setState({ + isExecuting: true, + }); + + try { + const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent( + firstItemIndex, + queryDocuments + ); + const allResultsMetadata = (this.state.allResultsMetadata && this.state.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.state.allResultsMetadata.push(resultsMetadata); + + this.setState({ + activityId: queryResults.activityId, + roundTrips: queryResults.roundTrips, + }); + + const documents = queryResults.documents; + if (this.isPreferredApiMongoDB) { + results = MongoUtility.tojson(documents, undefined, false); + } else { + results = this.props.tabsBaseInstance.renderObjectForEditor(documents, undefined, 4); + } + + const resultsDisplay: string = + queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; + + this.setState({ + showingDocumentsDisplayText: resultsDisplay, + requestChargeDisplayText: `${queryResults.requestCharge} RUs`, + queryResults: results, + }); + + this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); + + if (queryResults.itemCount === 0 && metadata !== undefined && 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; + } + } catch (error) { + this.props.tabsBaseInstance.isExecutionError(true); + this.setState({ + isExecutionError: true, + }); + const errorMessage = getErrorMessage(error); + this.setState({ + error: errorMessage, + }); + + document.getElementById("error-display").focus(); + } finally { + this.props.tabsBaseInstance.isExecuting(false); + this.setState({ + isExecuting: false, + }); + this.togglesOnFocus(); + } + } + + private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { + if (!metricsMap) { + this.allItems = this.generateDetailsList(); + this.setState({ + items: this.allItems, + }); + return; + } + + Object.keys(metricsMap).forEach((key: string) => { + this.state.queryMetrics.set(key, metricsMap[key]); + }); + + this._aggregateQueryMetrics(this.state.queryMetrics); + this.allItems = this.generateDetailsList(); + this.setState({ + items: this.allItems, + }); + } + + private _aggregateQueryMetrics(metricsMap: Map): DataModels.QueryMetrics { + if (!metricsMap) { + return undefined; + } + + const aggregatedMetrics: DataModels.QueryMetrics = this.state.aggregatedQueryMetrics; + metricsMap.forEach((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 = QueryTabComponent.getIteratorOptions(); + if (this._resourceTokenPartitionKey) { + options.partitionKey = this._resourceTokenPartitionKey; + } + + this._iterator = queryDocuments( + this.props.collection.databaseId, + this.props.collection.id(), + this.state.sqlStatementToExecute, + options + ); + } + + protected _initIteratorMongo(): Promise { + //eslint-disable-next-line + const options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + this._iterator = queryIterator( + this.props.collection.databaseId, + this.props.viewModelcollection, + this.state.sqlStatementToExecute + ); + const mongoPromise: Promise = new Promise((resolve) => { + resolve(this._iterator); + }); + return mongoPromise; + } + + //eslint-disable-next-line + public static getIteratorOptions(collection?: ViewModels.Collection): any { + //eslint-disable-next-line + const options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + return options; + } + + private _normalize(value: number): number { + if (!value) { + return 0; + } + + return value; + } + + private _resetAggregateQueryMetrics(): void { + this.setState({ + 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.state.queryMetrics) { + return undefined; + } + + const queryMetrics = this.state.queryMetrics; + let csvData = ""; + 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((queryMetric, partitionKeyRangeId) => { + 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.state._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 { + this.props.tabsBaseInstance.updateNavbarWithTabsButtons(); + } + + public onChangeContent(newContent: string): void { + this.setState({ + sqlQueryEditorContent: newContent, + }); + if (this.isPreferredApiMongoDB) { + if (newContent.length > 0) { + this.executeQueryButton = { + enabled: true, + visible: true, + }; + } else { + this.executeQueryButton = { + enabled: false, + visible: true, + }; + } + } + + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } + + public onSelectedContent(selectedContent: string): void { + if (selectedContent.trim().length > 0) { + this.setState({ + selectedContent: selectedContent, + _executeQueryButtonTitle: "Execute Selection", + }); + } else { + this.setState({ + selectedContent: "", + _executeQueryButtonTitle: "Execute Query", + }); + } + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } + + render(): JSX.Element { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + + return ( + +
+
+ + +
+ this.onChangeContent(newContent)} + onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)} + /> +
+
+ + {this.isPreferredApiMongoDB && this.state.sqlQueryEditorContent.length === 0 && ( +
+ Start by writing a Mongo query, for example: {"{'id':'foo'}"} or{" "} + + {"{ "} + {" }"} + {" "} + to get all the documents. +
+ )} + {this.maybeSubQuery && ( +
+
+ + 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 + + +
+
+ )} + {/* */} + {!!this.state.error && ( +
+ + Errors + +
+ )} + {/* */} + {/* */} +
+ {this.state.allResultsMetadata.length === 0 && + !this.state.error && + !this.state.queryResults && + !this.props.tabsBaseInstance.isExecuting() && ( +
+

+ Execute Query Watermark +

+

Execute a query to see the results

+
+ )} + {(this.state.allResultsMetadata.length > 0 || !!this.state.error || this.state.queryResults) && ( +
+ {!this.state.error && ( + + +
+ + {this.state.showingDocumentsDisplayText} + + {this.state.allResultsMetadata[this.state.allResultsMetadata.length - 1] + .hasMoreResults && ( + <> + | + + + Load more + Fetch next page + + + + )} +
+ {this.state.queryResults && + this.state.queryResults.length > 0 && + this.state.allResultsMetadata.length > 0 && + !this.state.error && ( +
+ +
+ )} +
+ + {this.state.allResultsMetadata.length > 0 && !this.state.error && ( + + )} + +
+ )} + {/* */} + {!!this.state.error && ( + + )} + {/* */} +
+ )} +
+
+
+
+
+
+ ); + } +} diff --git a/src/Explorer/Tabs/QueryTablesTab.ts b/src/Explorer/Tabs/QueryTablesTab.tsx similarity index 85% rename from src/Explorer/Tabs/QueryTablesTab.ts rename to src/Explorer/Tabs/QueryTablesTab.tsx index 59bd01cc7..fb096e1a9 100644 --- a/src/Explorer/Tabs/QueryTablesTab.ts +++ b/src/Explorer/Tabs/QueryTablesTab.tsx @@ -1,5 +1,5 @@ import * as ko from "knockout"; -import Q from "q"; +import React from "react"; import AddEntityIcon from "../../../images/AddEntity.svg"; import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg"; import EditEntityIcon from "../../../images/Edit-entity.svg"; @@ -7,13 +7,16 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; import QueryBuilderIcon from "../../../images/Query-Builder.svg"; import QueryTextIcon from "../../../images/Query-Text.svg"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { userContext } from "../../UserContext"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../Explorer"; +import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel"; +import { EditTableEntityPanel } from "../Panes/Tables/EditTableEntityPanel"; import TableCommands from "../Tables/DataTable/TableCommands"; import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel"; import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel"; -import { TableDataClient } from "../Tables/TableDataClient"; +import { CassandraAPIDataClient, TableDataClient } from "../Tables/TableDataClient"; import template from "./QueryTablesTab.html"; import TabsBase from "./TabsBase"; @@ -130,34 +133,38 @@ export default class QueryTablesTab extends TabsBase { this.buildCommandBarOptions(); } - public onExecuteQueryClick = (): Q.Promise => { - this.queryViewModel().runQuery(); - return null; + public onAddEntityClick = (): void => { + useSidePanel + .getState() + .openSidePanel( + "Add Table Row", + , + "700px" + ); }; - public onQueryBuilderClick = (): Q.Promise => { - this.queryViewModel().selectHelper(); - return null; + public onEditEntityClick = (): void => { + useSidePanel + .getState() + .openSidePanel( + "Edit Table Entity", + , + "700px" + ); }; - public onQueryTextClick = (): Q.Promise => { - this.queryViewModel().selectEditor(); - return null; - }; - - public onAddEntityClick = (): Q.Promise => { - this.container.openAddTableEntityPanel(this, this.tableEntityListViewModel()); - return null; - }; - - public onEditEntityClick = (): Q.Promise => { - this.container.openEditTableEntityPanel(this, this.tableEntityListViewModel()); - return null; - }; - - public onDeleteEntityClick = (): Q.Promise => { + public onDeleteEntityClick = (): void => { this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel()); - return null; }; public onActivate(): void { @@ -166,7 +173,7 @@ export default class QueryTablesTab extends TabsBase { !!this.tableEntityListViewModel() && !!this.tableEntityListViewModel().table && this.tableEntityListViewModel().table.columns; - if (!!columns) { + if (columns) { columns.adjust(); $(window).resize(); } @@ -179,7 +186,7 @@ export default class QueryTablesTab extends TabsBase { buttons.push({ iconSrc: QueryBuilderIcon, iconAlt: label, - onCommandClick: this.onQueryBuilderClick, + onCommandClick: () => this.queryViewModel().selectHelper(), commandButtonLabel: label, ariaLabel: label, hasPopup: false, @@ -193,7 +200,7 @@ export default class QueryTablesTab extends TabsBase { buttons.push({ iconSrc: QueryTextIcon, iconAlt: label, - onCommandClick: this.onQueryTextClick, + onCommandClick: () => this.queryViewModel().selectEditor(), commandButtonLabel: label, ariaLabel: label, hasPopup: false, @@ -207,7 +214,7 @@ export default class QueryTablesTab extends TabsBase { buttons.push({ iconSrc: ExecuteQueryIcon, iconAlt: label, - onCommandClick: this.onExecuteQueryClick, + onCommandClick: () => this.queryViewModel().runQuery(), commandButtonLabel: label, ariaLabel: label, hasPopup: false, diff --git a/src/Explorer/Tabs/ScriptTabBase.ts b/src/Explorer/Tabs/ScriptTabBase.ts index 4f270cb60..02116a07b 100644 --- a/src/Explorer/Tabs/ScriptTabBase.ts +++ b/src/Explorer/Tabs/ScriptTabBase.ts @@ -193,7 +193,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode } } - public abstract onSaveClick: () => Promise; + public abstract onSaveClick: () => void; public abstract onUpdateClick: () => Promise; public onDiscard = (): Q.Promise => { @@ -205,16 +205,6 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode return Q(); }; - public onSaveOrUpdateClick(): Promise { - if (this.saveButton.visible()) { - return this.onSaveClick(); - } else if (this.updateButton.visible()) { - return this.onUpdateClick(); - } - - return undefined; - } - protected getTabsButtons(): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; const label = "Save"; diff --git a/src/Explorer/Tabs/SettingsTabV2.tsx b/src/Explorer/Tabs/SettingsTabV2.tsx index 1221a53f7..e0f5358af 100644 --- a/src/Explorer/Tabs/SettingsTabV2.tsx +++ b/src/Explorer/Tabs/SettingsTabV2.tsx @@ -1,139 +1,23 @@ -import ko from "knockout"; -import * as Constants from "../../Common/Constants"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as DataModels from "../../Contracts/DataModels"; +import React from "react"; import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor"; -import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; -import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent"; -import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter"; +import { SettingsComponent } from "../Controls/Settings/SettingsComponent"; import TabsBase from "./TabsBase"; export class SettingsTabV2 extends TabsBase { - public readonly html = '
'; - public settingsComponentAdapter: SettingsComponentAdapter; - - constructor(options: ViewModels.TabOptions) { - super(options); - const props: SettingsComponentProps = { - settingsTab: this, - }; - this.settingsComponentAdapter = new SettingsComponentAdapter(props); + public render(): JSX.Element { + return ; } } export class CollectionSettingsTabV2 extends SettingsTabV2 { - private notificationRead: ko.Observable; - private notification: DataModels.Notification; - private offerRead: ko.Observable; - - constructor(options: ViewModels.TabOptions) { - super(options); - - this.tabId = "SettingsV2-" + this.tabId; - this.notificationRead = ko.observable(false); - this.offerRead = ko.observable(false); - this.settingsComponentAdapter.parameters = ko.computed(() => { - if (this.notificationRead() && this.offerRead()) { - this.pendingNotification(this.notification); - this.notification = undefined; - this.offerRead(false); - this.notificationRead(false); - return true; - } - return false; - }); - } - - public async onActivate(): Promise { - try { - this.isExecuting(true); - - const collection: ViewModels.Collection = this.collection as ViewModels.Collection; - await collection.loadOffer(); - // passed in options and set by parent as "Settings" by default - this.tabTitle(collection.offer() ? "Settings" : "Scale & Settings"); - - const data: DataModels.Notification = await collection.getPendingThroughputSplitNotification(); - this.notification = data; - this.notificationRead(true); - } catch (error) { - const errorMessage = getErrorMessage(error); - this.notification = undefined; - this.notificationRead(true); - traceFailure( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle, - error: errorMessage, - errorStack: getErrorStack(error), - }, - this.onLoadStartKey - ); - logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`); - throw error; - } finally { - this.offerRead(true); - this.isExecuting(false); - } - + public onActivate(): void { super.onActivate(); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2); } } export class DatabaseSettingsTabV2 extends SettingsTabV2 { - private notificationRead: ko.Observable; - private notification: DataModels.Notification; - - constructor(options: ViewModels.TabOptions) { - super(options); - this.tabId = "DatabaseSettingsV2-" + this.tabId; - this.notificationRead = ko.observable(false); - this.settingsComponentAdapter.parameters = ko.computed(() => { - if (this.notificationRead()) { - this.pendingNotification(this.notification); - this.notification = undefined; - this.notificationRead(false); - return true; - } - return false; - }); - } - - public async onActivate(): Promise { - try { - this.isExecuting(true); - - const data: DataModels.Notification = await this.database.getPendingThroughputSplitNotification(); - this.notification = data; - this.notificationRead(true); - } catch (error) { - const errorMessage = getErrorMessage(error); - this.notification = undefined; - this.notificationRead(true); - traceFailure( - Action.Tab, - { - databaseName: this.database.id(), - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle, - error: errorMessage, - errorStack: getErrorStack(error), - }, - this.onLoadStartKey - ); - logConsoleError(`Error while fetching database settings for database ${this.database.id()}: ${errorMessage}`); - throw error; - } finally { - this.isExecuting(false); - } - + public onActivate(): void { super.onActivate(); this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettingsV2); } diff --git a/src/Explorer/Tabs/StoredProcedureTab.html b/src/Explorer/Tabs/StoredProcedureTab.html deleted file mode 100644 index dc7db1bf7..000000000 --- a/src/Explorer/Tabs/StoredProcedureTab.html +++ /dev/null @@ -1,89 +0,0 @@ -
- -
-
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 deleted file mode 100644 index 01ff59c11..000000000 --- a/src/Explorer/Tabs/StoredProcedureTab.ts +++ /dev/null @@ -1,286 +0,0 @@ -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 { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -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 template from "./StoredProcedureTab.html"; - -enum ToggleState { - Result = "result", - Logs = "logs", -} - -export default class StoredProcedureTab extends ScriptTabBase { - public readonly html = template; - 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, { - 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, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - }, - (error: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.UpdateStoredProcedure, - { - 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.openExecuteSprocParamsPanel(this.node); - }, - 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, { - 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, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); - return createdResource; - }, - (createError) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateStoredProcedure, - { - 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/StoredProcedureTab/StoredProcedureTab.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTab.tsx new file mode 100644 index 000000000..b7b0f1673 --- /dev/null +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTab.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure"; +import * as DataModels from "../../../Contracts/DataModels"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { useTabs } from "../../../hooks/useTabs"; +import Explorer from "../../Explorer"; +import StoredProcedure from "../../Tree/StoredProcedure"; +import ScriptTabBase from "../ScriptTabBase"; +import StoredProcedureTabComponent, { + IStoredProcTabComponentProps, + IStorProcTabComponentAccessor, +} from "./StoredProcedureTabComponent"; + +export interface IStoredProcTabProps { + container: Explorer; + collection: ViewModels.Collection; +} + +export class NewStoredProcedureTab extends ScriptTabBase { + public queryText: string; + public currentQuery: string; + public partitionKey: DataModels.PartitionKey; + public iStoredProcTabComponentProps: IStoredProcTabComponentProps; + public iStoreProcAccessor: IStorProcTabComponentAccessor; + public node: StoredProcedure; + public onSaveClick: () => void; + public onUpdateClick: () => Promise; + + constructor(options: ViewModels.ScriptTabOption, private props: IStoredProcTabProps) { + super(options); + this.partitionKey = options.partitionKey; + + this.iStoredProcTabComponentProps = { + resource: options.resource, + isNew: options.isNew, + tabKind: options.tabKind, + title: options.title, + tabPath: options.tabPath, + collectionBase: options.collection, + node: options.node, + scriptTabBaseInstance: this, + collection: props.collection, + iStorProcTabComponentAccessor: (instance: IStorProcTabComponentAccessor) => { + this.iStoreProcAccessor = instance; + }, + container: props.container, + }; + } + + public render(): JSX.Element { + return ; + } + + public onTabClick(): void { + useTabs.getState().activateTab(this); + this.iStoreProcAccessor.onTabClickEvent(); + } + + public onCloseTabButtonClick(): void { + useTabs.getState().closeTab(this); + } + + public onExecuteSprocsResult(result: ExecuteSprocResult): void { + this.iStoreProcAccessor.onExecuteSprocsResultEvent(result); + } + + public onExecuteSprocsError(error: string): void { + this.iStoreProcAccessor.onExecuteSprocsErrorEvent(error); + } +} diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx new file mode 100644 index 000000000..b0d156b44 --- /dev/null +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -0,0 +1,597 @@ +import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; +import { Pivot, PivotItem } from "@fluentui/react"; +import React from "react"; +import DiscardIcon from "../../../../images/discard.svg"; +import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; +import SaveIcon from "../../../../images/save-cosmos.svg"; +import { NormalizedEventKey } from "../../../Common/Constants"; +import { createStoredProcedure } from "../../../Common/dataAccess/createStoredProcedure"; +import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure"; +import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; +import { useTabs } from "../../../hooks/useTabs"; +import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; +import { EditorReact } from "../../Controls/Editor/EditorReact"; +import Explorer from "../../Explorer"; +import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; +import StoredProcedure from "../../Tree/StoredProcedure"; +import { useSelectedNode } from "../../useSelectedNode"; +import ScriptTabBase from "../ScriptTabBase"; + +export interface IStorProcTabComponentAccessor { + onExecuteSprocsResultEvent: (result: ExecuteSprocResult) => void; + onExecuteSprocsErrorEvent: (error: string) => void; + onTabClickEvent: () => void; +} + +export interface Button { + visible: boolean; + enabled: boolean; + isSelected?: boolean; +} + +interface IStoredProcTabComponentStates { + hasResults: boolean; + hasErrors: boolean; + error: string; + resultData: string; + logsData: string; + originalSprocBody: string; + initialEditorContent: string; + sProcEditorContent: string; + id: string; + executeButton: Button; + saveButton: Button; + updateButton: Button; + discardButton: Button; +} + +export interface IStoredProcTabComponentProps { + resource: StoredProcedureDefinition; + isNew: boolean; + tabKind: ViewModels.CollectionTabKind; + title: string; + tabPath: string; + collectionBase: ViewModels.CollectionBase; + //eslint-disable-next-line + node?: any; + scriptTabBaseInstance: ScriptTabBase; + collection: ViewModels.Collection; + iStorProcTabComponentAccessor: (instance: IStorProcTabComponentAccessor) => void; + container: Explorer; +} + +export default class StoredProcedureTabComponent extends React.Component< + IStoredProcTabComponentProps, + IStoredProcTabComponentStates +> { + public node: StoredProcedure; + public executeResultsEditorId: string; + public executeLogsEditorId: string; + public collection: ViewModels.Collection; + + constructor( + public storedProcTabCompProps: IStoredProcTabComponentProps, + private storedProcTabCompStates: IStoredProcTabComponentStates + ) { + super(storedProcTabCompProps); + this.state = { + error: "", + hasErrors: false, + hasResults: false, + resultData: "", + logsData: "", + originalSprocBody: this.props.resource.body.toString(), + initialEditorContent: this.props.resource.body.toString(), + sProcEditorContent: this.props.resource.body.toString(), + id: this.props.resource.id, + executeButton: { + enabled: !this.props.scriptTabBaseInstance.isNew(), + visible: true, + }, + saveButton: { + enabled: (() => { + if (!this.props.scriptTabBaseInstance.formIsValid()) { + return false; + } + if (!this.props.scriptTabBaseInstance.formIsDirty()) { + return false; + } + return true; + })(), + visible: this.props.scriptTabBaseInstance.isNew(), + }, + updateButton: { + enabled: (() => { + if (!this.props.scriptTabBaseInstance.formIsValid()) { + return false; + } + if (!this.props.scriptTabBaseInstance.formIsDirty()) { + return false; + } + return true; + })(), + visible: !this.props.scriptTabBaseInstance.isNew(), + }, + discardButton: { + enabled: (() => { + if (!this.props.scriptTabBaseInstance.formIsValid()) { + return false; + } + if (!this.props.scriptTabBaseInstance.formIsDirty()) { + return false; + } + return true; + })(), + visible: true, + }, + }; + + this.collection = this.props.collection; + this.executeResultsEditorId = `executestoredprocedureresults${this.props.scriptTabBaseInstance.tabId}`; + this.executeLogsEditorId = `executestoredprocedurelogs${this.props.scriptTabBaseInstance.tabId}`; + this.props.scriptTabBaseInstance.ariaLabel("Stored Procedure Body"); + + this.props.iStorProcTabComponentAccessor({ + onExecuteSprocsResultEvent: this.onExecuteSprocsResult.bind(this), + onExecuteSprocsErrorEvent: this.onExecuteSprocsError.bind(this), + onTabClickEvent: this.onTabClick.bind(this), + }); + + this.node = this.props.node; + + this.buildCommandBarOptions(); + } + + public onTabClick(): void { + if (useTabs.getState().openedTabs.length > 0) { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } + } + + public onSaveClick = (): Promise => { + return this._createStoredProcedure({ + id: this.state.id, + body: this.state.sProcEditorContent, + }); + }; + + public onDiscard = (): Promise => { + const onDiscardPromise = new Promise(() => { + this.props.scriptTabBaseInstance.setBaselines(); + const original = this.props.scriptTabBaseInstance.editorContent.getEditableOriginalValue(); + if (this.state.updateButton.visible) { + this.setState({ + updateButton: { + enabled: false, + visible: true, + }, + sProcEditorContent: original, + discardButton: { + enabled: false, + visible: true, + }, + executeButton: { + enabled: true, + visible: true, + }, + }); + } else { + this.setState({ + saveButton: { + enabled: false, + visible: true, + }, + sProcEditorContent: original, + discardButton: { + enabled: false, + visible: true, + }, + executeButton: { + enabled: false, + visible: true, + }, + id: "", + }); + } + }); + + setTimeout(() => { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }, 100); + + return onDiscardPromise; + }; + + public onUpdateClick = (): Promise => { + const data = this._getResource(); + + this.props.scriptTabBaseInstance.isExecutionError(false); + this.props.scriptTabBaseInstance.isExecuting(true); + + return updateStoredProcedure( + this.props.scriptTabBaseInstance.collection.databaseId, + this.props.scriptTabBaseInstance.collection.id(), + data + ) + .then( + (updatedResource) => { + this.props.scriptTabBaseInstance.resource(updatedResource); + this.props.scriptTabBaseInstance.tabTitle(updatedResource.id); + this.node.id(updatedResource.id); + this.node.body(updatedResource.body as string); + this.props.scriptTabBaseInstance.setBaselines(); + + const editorModel = + this.props.scriptTabBaseInstance.editor() && this.props.scriptTabBaseInstance.editor().getModel(); + editorModel && editorModel.setValue(updatedResource.body as string); + this.props.scriptTabBaseInstance.editorContent.setBaseline(updatedResource.body as string); + this.setState({ + discardButton: { + enabled: false, + visible: true, + }, + updateButton: { + enabled: false, + visible: true, + }, + executeButton: { + enabled: true, + visible: true, + }, + }); + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }, + () => { + this.props.scriptTabBaseInstance.isExecutionError(true); + } + ) + .finally(() => this.props.scriptTabBaseInstance.isExecuting(false)); + }; + + public onExecuteSprocsResult(result: ExecuteSprocResult): void { + const resultData: string = this.props.scriptTabBaseInstance.renderObjectForEditor(result.result, undefined, 4); + const scriptLogs: string = (result.scriptLogs && decodeURIComponent(result.scriptLogs)) || ""; + const logs: string = this.props.scriptTabBaseInstance.renderObjectForEditor(scriptLogs, undefined, 4); + + this.setState({ + hasResults: false, + }); + setTimeout(() => { + this.setState({ + error: undefined, + resultData: resultData, + logsData: logs, + hasResults: resultData ? true : false, + hasErrors: false, + }); + }, 100); + } + + public onExecuteSprocsError(error: string): void { + this.props.scriptTabBaseInstance.isExecutionError(true); + console.error(error); + this.setState({ + error: error, + hasErrors: true, + hasResults: false, + }); + } + + public onErrorDetailsClick = (): boolean => { + useNotificationConsole.getState().expandConsole(); + + return false; + }; + + public onErrorDetailsKeyPress = (event: React.KeyboardEvent): boolean => { + if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { + this.onErrorDetailsClick(); + return false; + } + + return true; + }; + + protected updateSelectedNode(): void { + if (this.props.collectionBase === undefined) { + return; + } + + const database: ViewModels.Database = this.props.collectionBase.getDatabase(); + const setSelectedNode = useSelectedNode.getState().setSelectedNode; + if (!database.isDatabaseExpanded()) { + setSelectedNode(database); + } else if (!this.props.collectionBase.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) { + setSelectedNode(this.props.collectionBase); + } else { + setSelectedNode(this.node); + } + } + + protected buildCommandBarOptions(): void { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + const label = "Save"; + if (this.state.saveButton.visible) { + buttons.push({ + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.state.saveButton.enabled, + }); + } + + if (this.state.updateButton.visible) { + const label = "Update"; + buttons.push({ + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onUpdateClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.state.updateButton.enabled, + }); + } + + if (this.state.discardButton.visible) { + const label = "Discard"; + buttons.push({ + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onDiscard, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.state.discardButton.enabled, + }); + } + + if (this.state.executeButton.visible) { + const label = "Execute"; + buttons.push({ + iconSrc: ExecuteQueryIcon, + iconAlt: label, + onCommandClick: () => { + this.collection.container.openExecuteSprocParamsPanel(this.node); + }, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.state.executeButton.enabled, + }); + } + + return buttons; + } + + private _getResource() { + return { + id: this.state.id, + body: this.state.sProcEditorContent, + }; + } + + private _createStoredProcedure(resource: StoredProcedureDefinition): Promise { + this.props.scriptTabBaseInstance.isExecutionError(false); + this.props.scriptTabBaseInstance.isExecuting(true); + + return createStoredProcedure(this.props.collectionBase.databaseId, this.props.collectionBase.id(), resource) + .then( + (createdResource) => { + this.props.scriptTabBaseInstance.tabTitle(createdResource.id); + this.props.scriptTabBaseInstance.isNew(false); + this.props.scriptTabBaseInstance.resource(createdResource); + this.props.scriptTabBaseInstance.setBaselines(); + + const editorModel = + this.props.scriptTabBaseInstance.editor() && this.props.scriptTabBaseInstance.editor().getModel(); + editorModel && editorModel.setValue(createdResource.body as string); + this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string); + this.node = this.collection.createStoredProcedureNode(createdResource); + this.props.scriptTabBaseInstance.node = this.node; + useTabs.getState().updateTab(this.props.scriptTabBaseInstance); + this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); + + this.setState({ + executeButton: { + enabled: false, + visible: true, + }, + }); + setTimeout(() => { + this.setState({ + executeButton: { + enabled: true, + visible: true, + }, + updateButton: { + enabled: false, + visible: true, + }, + saveButton: { + enabled: false, + visible: false, + }, + discardButton: { + enabled: false, + visible: true, + }, + sProcEditorContent: this.state.sProcEditorContent, + }); + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }, 100); + + return createdResource; + }, + (createError) => { + this.props.scriptTabBaseInstance.isExecutionError(true); + + return Promise.reject(createError); + } + ) + .finally(() => this.props.scriptTabBaseInstance.isExecuting(false)); + } + + public onDelete(): Promise { + const isDeleted = false; + const onDeletePromise = new Promise((resolve) => { + resolve(isDeleted); + }); + return onDeletePromise; + } + + public handleIdOnChange(event: React.ChangeEvent): void { + if (this.state.saveButton.visible) { + this.setState({ + id: event.target.value, + saveButton: { + enabled: true, + visible: this.props.scriptTabBaseInstance.isNew(), + }, + discardButton: { + enabled: true, + visible: true, + }, + }); + } + setTimeout(() => { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }, 1000); + } + + public onChangeContent(newConent: string): void { + if (this.state.updateButton.visible) { + this.setState({ + updateButton: { + enabled: true, + visible: true, + }, + discardButton: { + enabled: true, + visible: true, + }, + executeButton: { + enabled: false, + visible: true, + }, + sProcEditorContent: newConent, + }); + } else { + this.setState({ + saveButton: { + enabled: false, + visible: this.props.scriptTabBaseInstance.isNew(), + }, + executeButton: { + enabled: false, + visible: true, + }, + discardButton: { + enabled: true, + visible: true, + }, + sProcEditorContent: newConent, + }); + } + + setTimeout(() => { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }, 100); + } + + render(): JSX.Element { + return ( +
+
+
Stored Procedure Id
+ + ) => this.handleIdOnChange(event)} + /> + +
Stored Procedure Body
+ this.onChangeContent(newContent)} + /> + {this.state.hasResults && ( +
+ + + + + + + + +
+ )} + {this.state.hasErrors && ( + + )} +
+
+ ); + } +} diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 9c4de6477..caf13ef7d 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import errorIcon from "../../../images/close-black.svg"; import { useObservable } from "../../hooks/useObservable"; +import { useTabs } from "../../hooks/useTabs"; import TabsBase from "./TabsBase"; type Tab = TabsBase | (TabsBase & { render: () => JSX.Element }); -export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => ( -
-
-
- -
-
- {tabs.map((tab) => ( - - ))} +
-
-); + ); +}; function TabNav({ tab, active }: { tab: Tab; active: boolean }) { const [hovering, setHovering] = useState(false); diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index dfd92a651..308bb451f 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -3,17 +3,19 @@ import * as Constants from "../../Common/Constants"; import * as ThemeUtility from "../../Common/ThemeUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { RouteHandler } from "../../RouteHandlers/RouteHandler"; +import { useNotificationConsole } from "../../hooks/useNotificationConsole"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../Explorer"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import { useSelectedNode } from "../useSelectedNode"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; -import { TabsManager } from "./TabsManager"; - // TODO: Use specific actions for logging telemetry data export default class TabsBase extends WaitsForTemplateViewModel { private static id = 0; + public readonly index: number; public closeTabButton: ViewModels.Button; public node: ViewModels.TreeNode; public collection: ViewModels.CollectionBase; @@ -23,16 +25,15 @@ export default class TabsBase extends WaitsForTemplateViewModel { public tabKind: ViewModels.CollectionTabKind; public tabTitle: ko.Observable; public tabPath: ko.Observable; - public hashLocation: ko.Observable; public isExecutionError = ko.observable(false); public isExecuting = ko.observable(false); public pendingNotification?: ko.Observable; - public manager?: TabsManager; protected _theme: string; public onLoadStartKey: number; constructor(options: ViewModels.TabOptions) { super(); + this.index = options.index; this._theme = ThemeUtility.getMonacoTheme(options.theme); this.node = options.node; this.collection = options.collection; @@ -46,8 +47,6 @@ export default class TabsBase extends WaitsForTemplateViewModel { ko.observable(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`)); this.pendingNotification = ko.observable(undefined); this.onLoadStartKey = options.onLoadStartKey; - this.hashLocation = ko.observable(options.hashLocation || ""); - this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation)); this.closeTabButton = { enabled: ko.computed(() => { return true; @@ -60,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onCloseTabButtonClick(): void { - this.manager?.closeTab(this); + useTabs.getState().closeTab(this); TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { tabName: this.constructor.name, dataExplorerArea: Constants.Areas.Tab, @@ -70,17 +69,18 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onTabClick(): void { - this.manager?.activateTab(this); + useTabs.getState().activateTab(this); } protected updateSelectedNode(): void { const relatedDatabase = (this.collection && this.collection.getDatabase()) || this.database; + const setSelectedNode = useSelectedNode.getState().setSelectedNode; if (relatedDatabase && !relatedDatabase.isDatabaseExpanded()) { - this.getContainer().selectedNode(relatedDatabase); + setSelectedNode(relatedDatabase); } else if (this.collection && !this.collection.isCollectionExpanded()) { - this.getContainer().selectedNode(this.collection); + setSelectedNode(this.collection); } else { - this.getContainer().selectedNode(this.node); + setSelectedNode(this.node); } } @@ -104,14 +104,13 @@ export default class TabsBase extends WaitsForTemplateViewModel { /** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */ public isActive() { - return this === this.manager?.activeTab(); + return this === useTabs.getState().activeTab; } public onActivate(): void { this.updateSelectedNode(); this.collection?.selectedSubnodeKind(this.tabKind); this.database?.selectedSubnodeKind(this.tabKind); - this.updateGlobalHash(this.hashLocation()); this.updateNavbarWithTabsButtons(); TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, { tabName: this.constructor.name, @@ -122,8 +121,8 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { - this.collection?.container?.expandConsole(); - this.database?.container?.expandConsole(); + useNotificationConsole.getState().expandConsole(); + useNotificationConsole.getState().expandConsole(); return false; }; @@ -145,14 +144,10 @@ export default class TabsBase extends WaitsForTemplateViewModel { } /** Renders a Javascript object to be displayed inside Monaco Editor */ - protected renderObjectForEditor(value: any, replacer: any, space: string | number): string { + public 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 */ @@ -160,9 +155,9 @@ export default class TabsBase extends WaitsForTemplateViewModel { return []; } - protected updateNavbarWithTabsButtons = (): void => { + public updateNavbarWithTabsButtons = (): void => { if (this.isActive()) { - this.getContainer().onUpdateTabsButtons(this.getTabsButtons()); + useCommandBar.getState().setContextButtons(this.getTabsButtons()); } }; } diff --git a/src/Explorer/Tabs/TabsManager.test.ts b/src/Explorer/Tabs/TabsManager.test.ts deleted file mode 100644 index e8bf95f22..000000000 --- a/src/Explorer/Tabs/TabsManager.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { updateUserContext } from "../../UserContext"; -import Explorer from "../Explorer"; -import DocumentId from "../Tree/DocumentId"; -import DocumentsTab from "./DocumentsTab"; -import QueryTab from "./QueryTab"; -import { TabsManager } from "./TabsManager"; - -describe("Tabs manager tests", () => { - let tabsManager: TabsManager; - let explorer: Explorer; - let database: ViewModels.Database; - let collection: ViewModels.Collection; - let queryTab: QueryTab; - let documentsTab: DocumentsTab; - - beforeAll(() => { - explorer = new Explorer(); - updateUserContext({ - databaseAccount: { - id: "test", - name: "test", - location: "", - type: "", - kind: "", - properties: undefined, - }, - }); - - database = { - container: explorer, - id: ko.observable("test"), - isDatabaseShared: () => false, - } as ViewModels.Database; - database.isDatabaseExpanded = ko.observable(true); - database.selectedSubnodeKind = ko.observable(); - - collection = { - container: explorer, - databaseId: "test", - id: ko.observable("test"), - } as ViewModels.Collection; - collection.getDatabase = (): ViewModels.Database => database; - collection.isCollectionExpanded = ko.observable(true); - collection.selectedSubnodeKind = ko.observable(); - - queryTab = new QueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - collection, - database, - title: "", - tabPath: "", - hashLocation: "", - onUpdateTabsButtons: undefined, - }); - - documentsTab = new DocumentsTab({ - partitionKey: undefined, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - collection, - title: "", - tabPath: "", - hashLocation: "", - onUpdateTabsButtons: undefined, - }); - - // make sure tabs have different tabId - queryTab.tabId = "1"; - documentsTab.tabId = "2"; - }); - - beforeEach(() => (tabsManager = new TabsManager())); - - it("open new tabs", () => { - tabsManager.activateNewTab(queryTab); - expect(tabsManager.openedTabs().length).toBe(1); - expect(tabsManager.openedTabs()[0]).toEqual(queryTab); - expect(tabsManager.activeTab()).toEqual(queryTab); - expect(queryTab.isActive()).toBe(true); - - tabsManager.activateNewTab(documentsTab); - expect(tabsManager.openedTabs().length).toBe(2); - expect(tabsManager.openedTabs()[1]).toEqual(documentsTab); - expect(tabsManager.activeTab()).toEqual(documentsTab); - expect(queryTab.isActive()).toBe(false); - expect(documentsTab.isActive()).toBe(true); - }); - - it("open existing tabs", () => { - tabsManager.activateNewTab(queryTab); - tabsManager.activateNewTab(documentsTab); - tabsManager.activateTab(queryTab); - expect(tabsManager.openedTabs().length).toBe(2); - expect(tabsManager.activeTab()).toEqual(queryTab); - expect(queryTab.isActive()).toBe(true); - expect(documentsTab.isActive()).toBe(false); - }); - - it("get tabs", () => { - tabsManager.activateNewTab(queryTab); - tabsManager.activateNewTab(documentsTab); - - const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query); - expect(queryTabs.length).toBe(1); - expect(queryTabs[0]).toEqual(queryTab); - - const documentsTabs = tabsManager.getTabs( - ViewModels.CollectionTabKind.Documents, - (tab) => tab.tabId === documentsTab.tabId - ); - expect(documentsTabs.length).toBe(1); - expect(documentsTabs[0]).toEqual(documentsTab); - }); - - it("close tabs", () => { - tabsManager.activateNewTab(queryTab); - tabsManager.activateNewTab(documentsTab); - - tabsManager.closeTab(documentsTab); - expect(tabsManager.openedTabs().length).toBe(1); - expect(tabsManager.openedTabs()[0]).toEqual(queryTab); - expect(tabsManager.activeTab()).toEqual(queryTab); - expect(queryTab.isActive()).toBe(true); - expect(documentsTab.isActive()).toBe(false); - - 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 deleted file mode 100644 index 95221a685..000000000 --- a/src/Explorer/Tabs/TabsManager.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; -import TabsBase from "./TabsBase"; - -export class TabsManager { - public openedTabs = ko.observableArray([]); - public activeTab = ko.observable(); - - public activateNewTab(tab: TabsBase): void { - this.openedTabs.push(tab); - this.activateTab(tab); - } - - public activateTab(tab: TabsBase): void { - if (this.openedTabs().includes(tab)) { - tab.manager = this; - this.activeTab(tab); - tab.onActivate(); - } - } - - public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] { - return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab))); - } - - public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void { - // ensures that the tab selects/highlights the right node based on resource tree expand/collapse state - this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate(); - } - - public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void { - this.openedTabs() - .filter(comparator) - .forEach((tab) => tab.onCloseTabButtonClick()); - } - - public closeTab(tab: TabsBase): void { - const tabIndex = this.openedTabs().indexOf(tab); - if (tabIndex !== -1) { - this.openedTabs.remove(tab); - tab.manager = undefined; - - if (this.openedTabs().length === 0) { - this.activeTab(undefined); - } - - if (tab === this.activeTab()) { - const tabToTheRight = this.openedTabs()[tabIndex]; - const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1]; - this.activateTab(tabToTheRight ?? lastOpenTab); - } - } - } -} diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index b3126d790..7eee433d3 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -1,3 +1,4 @@ +import { Spinner, SpinnerSize } from "@fluentui/react"; import * as ko from "knockout"; import * as React from "react"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; @@ -7,6 +8,7 @@ import { userContext } from "../../UserContext"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent"; import Explorer from "../Explorer"; +import { useNotebook } from "../Notebook/useNotebook"; import TabsBase from "./TabsBase"; export interface TerminalTabOptions extends ViewModels.TabOptions { @@ -33,7 +35,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { databaseAccount={this.getDatabaseAccount()} /> ) : ( - <> + ); } } @@ -51,7 +53,11 @@ export default class TerminalTab extends TabsBase { () => userContext?.databaseAccount ); this.notebookTerminalComponentAdapter.parameters = ko.computed(() => { - if (this.isTemplateReady() && this.container.isNotebookEnabled()) { + if ( + this.isTemplateReady() && + useNotebook.getState().isNotebookEnabled && + useNotebook.getState().notebookServerInfo?.notebookServerEndpoint + ) { return true; } return false; @@ -90,7 +96,7 @@ export default class TerminalTab extends TabsBase { throw new Error(`Terminal kind: ${options.kind} not supported`); } - const info: DataModels.NotebookWorkspaceConnectionInfo = options.container.notebookServerInfo(); + const info: DataModels.NotebookWorkspaceConnectionInfo = useNotebook.getState().notebookServerInfo; return { authToken: info.authToken, notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`, diff --git a/src/Explorer/Tabs/TriggerTab.html b/src/Explorer/Tabs/TriggerTab.html deleted file mode 100644 index 62aab2dad..000000000 --- a/src/Explorer/Tabs/TriggerTab.html +++ /dev/null @@ -1,39 +0,0 @@ -
- -
-
Trigger Id
- - - - -
Trigger Type
- - - -
Trigger Operation
- - - -
Trigger Body
-
-
- -
diff --git a/src/Explorer/Tabs/TriggerTab.ts b/src/Explorer/Tabs/TriggerTab.ts deleted file mode 100644 index 54cd47c3f..000000000 --- a/src/Explorer/Tabs/TriggerTab.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Resource, TriggerDefinition, TriggerOperation, TriggerType } from "@azure/cosmos"; -import * as Constants from "../../Common/Constants"; -import { createTrigger } from "../../Common/dataAccess/createTrigger"; -import { updateTrigger } from "../../Common/dataAccess/updateTrigger"; -import editable from "../../Common/EditableUtility"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import Trigger from "../Tree/Trigger"; -import ScriptTabBase from "./ScriptTabBase"; -import template from "./TriggerTab.html"; - -export default class TriggerTab extends ScriptTabBase { - public readonly html = template; - public collection: ViewModels.Collection; - public node: Trigger; - public triggerType: ViewModels.Editable; - public triggerOperation: ViewModels.Editable; - - constructor(options: ViewModels.ScriptTabOption) { - super(options); - super.onActivate.bind(this); - this.ariaLabel("Trigger Body"); - this.triggerType = editable.observable(options.resource.triggerType); - this.triggerOperation = editable.observable(options.resource.triggerOperation); - - this.formFields([this.id, this.triggerType, this.triggerOperation, this.editorContent]); - super.buildCommandBarOptions.bind(this); - super.buildCommandBarOptions(); - } - - public onSaveClick = (): Promise => { - return this._createTrigger({ - id: this.id(), - body: this.editorContent(), - triggerOperation: this.triggerOperation() as TriggerOperation, - triggerType: this.triggerType() as TriggerType, - }); - }; - - public onUpdateClick = (): Promise => { - const data = this._getResource(); - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, { - 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, - }) - .then( - (createdResource) => { - this.resource(createdResource); - this.tabTitle(createdResource.id); - - this.node.id(createdResource.id); - this.node.body(createdResource.body as string); - this.node.triggerType(createdResource.triggerOperation); - this.node.triggerOperation(createdResource.triggerOperation); - TelemetryProcessor.traceSuccess( - Action.UpdateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.UpdateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - }; - - public setBaselines() { - super.setBaselines(); - - const resource = this.resource(); - this.triggerOperation.setBaseline(resource.triggerOperation); - this.triggerType.setBaseline(resource.triggerType); - } - - 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.isTriggersExpanded()) { - this.collection.container.selectedNode(this.collection); - } else { - this.collection.container.selectedNode(this.node); - } - } - - private _createTrigger(resource: TriggerDefinition): Promise { - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - return createTrigger(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() - )}/triggers/${createdResource.id}` - ); - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - - this.node = this.collection.createTriggerNode(createdResource); - TelemetryProcessor.traceSuccess( - Action.CreateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); - return createdResource; - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - return Promise.reject(createError); - } - ) - .finally(() => this.isExecuting(false)); - } - - private _getResource() { - return { - id: this.id(), - body: this.editorContent(), - triggerOperation: this.triggerOperation(), - triggerType: this.triggerType(), - }; - } -} diff --git a/src/Explorer/Tabs/TriggerTab.tsx b/src/Explorer/Tabs/TriggerTab.tsx new file mode 100644 index 000000000..e22634e1f --- /dev/null +++ b/src/Explorer/Tabs/TriggerTab.tsx @@ -0,0 +1,36 @@ +import { TriggerDefinition } from "@azure/cosmos"; +import React from "react"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; +import Trigger from "../Tree/Trigger"; +import ScriptTabBase from "./ScriptTabBase"; +import { TriggerTabContent } from "./TriggerTabContent"; + +export default class TriggerTab extends ScriptTabBase { + public onSaveClick: () => void; + public onUpdateClick: () => Promise; + public collection: ViewModels.Collection; + public node: Trigger; + public triggerType: ViewModels.Editable; + public triggerOperation: ViewModels.Editable; + public triggerOptions: ViewModels.ScriptTabOption; + + constructor(options: ViewModels.ScriptTabOption) { + super(options); + super.onActivate.bind(this); + this.triggerOptions = options; + } + + addNodeInCollection(createdResource: TriggerDefinition | SqlTriggerResource): void { + this.node = this.collection.createTriggerNode(createdResource); + } + + public render(): JSX.Element { + return ( + this.addNodeInCollection(createdResource)} + /> + ); + } +} diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx new file mode 100644 index 000000000..2add748b1 --- /dev/null +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -0,0 +1,337 @@ +import { TriggerDefinition } from "@azure/cosmos"; +import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react"; +import React, { Component } from "react"; +import DiscardIcon from "../../../images/discard.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import * as Constants from "../../Common/Constants"; +import { createTrigger } from "../../Common/dataAccess/createTrigger"; +import { updateTrigger } from "../../Common/dataAccess/updateTrigger"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { EditorReact } from "../Controls/Editor/EditorReact"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import TriggerTab from "./TriggerTab"; + +const triggerTypeOptions: IDropdownOption[] = [ + { key: "Pre", text: "Pre" }, + { key: "Post", text: "Post" }, +]; + +const triggerOperationOptions: IDropdownOption[] = [ + { key: "All", text: "All" }, + { key: "Create", text: "Create" }, + { key: "Delete", text: "Delete" }, + { key: "Replace", text: "Replace" }, +]; + +interface Ibutton { + visible: boolean; + enabled: boolean; +} + +interface ITriggerTabContentState { + [key: string]: string | boolean; + triggerId: string; + triggerBody: string; + triggerType?: "Pre" | "Post"; + triggerOperation?: "All" | "Create" | "Update" | "Delete" | "Replace"; + isIdEditable: boolean; +} + +export class TriggerTabContent extends Component { + public saveButton: Ibutton; + public updateButton: Ibutton; + public discardButton: Ibutton; + + constructor(props: TriggerTab) { + super(props); + this.saveButton = { + visible: props.isNew(), + enabled: false, + }; + this.updateButton = { + visible: !props.isNew(), + enabled: true, + }; + + this.discardButton = { + visible: true, + enabled: true, + }; + + const { id, body, triggerType, triggerOperation } = props.triggerOptions.resource; + this.state = { + triggerId: id, + triggerType: triggerType, + triggerOperation: triggerOperation, + triggerBody: body, + isIdEditable: props.isNew() ? true : false, + }; + } + + private async onSaveClick(): Promise { + const { triggerId, triggerType, triggerBody, triggerOperation } = this.state; + const resource = { + id: triggerId, + body: triggerBody, + triggerOperation: triggerOperation, + triggerType: triggerType, + }; + + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }); + + try { + resource.body = String(resource.body); // Ensure trigger body is converted to string + const createdResource: TriggerDefinition | SqlTriggerResource = await createTrigger( + this.props.collection.databaseId, + this.props.collection.id(), + resource + ); + if (createdResource) { + this.props.tabTitle(createdResource.id); + this.props.isNew(false); + this.props.resource(createdResource); + this.props.editorContent.setBaseline(createdResource.body as string); + this.props.addNodeInCollection(createdResource); + this.saveButton.visible = false; + this.updateButton.visible = true; + this.setState({ isIdEditable: false }); + TelemetryProcessor.traceSuccess( + Action.CreateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }, + startKey + ); + this.props.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); + this.props.isExecuting(false); + } + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.CreateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + } + } + + private async onUpdateClick(): Promise { + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, { + tabTitle: this.props.tabTitle(), + }); + + try { + const { triggerId, triggerBody, triggerOperation, triggerType } = this.state; + const createdResource = await updateTrigger(this.props.collection.databaseId, this.props.collection.id(), { + id: triggerId, + body: triggerBody, + triggerOperation: triggerOperation as SqlTriggerResource["triggerOperation"], + triggerType: triggerType as SqlTriggerResource["triggerType"], + }); + if (createdResource) { + this.props.resource(createdResource); + this.props.tabTitle(createdResource.id); + + this.props.node.id(createdResource.id); + this.props.node.body(createdResource.body as string); + this.props.node.triggerType(createdResource.triggerType); + this.props.node.triggerOperation(createdResource.triggerOperation); + TelemetryProcessor.traceSuccess( + Action.UpdateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }, + startKey + ); + this.props.isExecuting(false); + } + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.UpdateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + } + } + + private onDiscard(): void { + const { id, body, triggerType, triggerOperation } = this.props.triggerOptions.resource; + this.setState({ + triggerId: id, + triggerType: triggerType, + triggerOperation: triggerOperation, + triggerBody: body, + }); + } + + private isValidId(id: string): boolean { + if (!id) { + return false; + } + + const invalidStartCharacters = /^[/?#\\]/; + if (invalidStartCharacters.test(id)) { + return false; + } + + const invalidMiddleCharacters = /^.+[/?#\\]/; + if (invalidMiddleCharacters.test(id)) { + return false; + } + + const invalidEndCharacters = /.*[/?#\\ ]$/; + if (invalidEndCharacters.test(id)) { + return false; + } + + return true; + } + + private isNotEmpty(value: string): boolean { + return !!value; + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + const label = "Save"; + if (this.saveButton.visible) { + buttons.push({ + setState: this.setState, + ...this, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveButton.enabled, + }); + } + + if (this.updateButton.visible) { + const label = "Update"; + buttons.push({ + ...this, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onUpdateClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.updateButton.enabled, + }); + } + + if (this.discardButton.visible) { + const label = "Discard"; + buttons.push({ + setState: this.setState, + ...this, + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onDiscard, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardButton.enabled, + }); + } + return buttons; + } + + private handleTriggerIdChange = ( + _event: React.FormEvent, + newValue?: string + ): void => { + this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue); + this.setState({ triggerId: newValue }); + }; + + private handleTriggerTypeOprationChange = ( + _event: React.FormEvent, + selectedParam: IDropdownOption, + key: string + ): void => { + this.setState({ [key]: String(selectedParam.key) }); + }; + + private handleTriggerBodyChange = (newContent: string) => { + this.setState({ triggerBody: newContent }); + }; + + render(): JSX.Element { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state; + return ( +
+ + this.handleTriggerTypeOprationChange(event, selectedKey, "triggerType")} + /> + + this.handleTriggerTypeOprationChange(event, selectedKey, "triggerOperation") + } + /> + + +
+ ); + } +} diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.html b/src/Explorer/Tabs/UserDefinedFunctionTab.html deleted file mode 100644 index 259604f29..000000000 --- a/src/Explorer/Tabs/UserDefinedFunctionTab.html +++ /dev/null @@ -1,30 +0,0 @@ -
- -
-
User Defined Function Id
- - - -
User Defined Function Body
-
-
- -
diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.ts b/src/Explorer/Tabs/UserDefinedFunctionTab.ts deleted file mode 100644 index ff5a7753d..000000000 --- a/src/Explorer/Tabs/UserDefinedFunctionTab.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; -import * as Constants from "../../Common/Constants"; -import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction"; -import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import UserDefinedFunction from "../Tree/UserDefinedFunction"; -import ScriptTabBase from "./ScriptTabBase"; -import template from "./UserDefinedFunctionTab.html"; - -export default class UserDefinedFunctionTab extends ScriptTabBase { - public readonly html = template; - public collection: ViewModels.Collection; - public node: UserDefinedFunction; - constructor(options: ViewModels.ScriptTabOption) { - super(options); - this.ariaLabel("User Defined Function Body"); - super.onActivate.bind(this); - super.buildCommandBarOptions.bind(this); - super.buildCommandBarOptions(); - } - - public onSaveClick = (): Promise => { - const data = this._getResource(); - return this._createUserDefinedFunction(data); - }; - - public onUpdateClick = (): Promise => { - const data = this._getResource(); - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - return updateUserDefinedFunction(this.collection.databaseId, this.collection.id(), data) - .then( - (createdResource) => { - this.resource(createdResource); - this.tabTitle(createdResource.id); - - this.node.id(createdResource.id); - this.node.body(createdResource.body as string); - TelemetryProcessor.traceSuccess( - Action.UpdateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.UpdateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - }; - - 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.isUserDefinedFunctionsExpanded()) { - this.collection.container.selectedNode(this.collection); - } else { - this.collection.container.selectedNode(this.node); - } - } - - private _createUserDefinedFunction( - resource: UserDefinedFunctionDefinition - ): Promise { - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - return createUserDefinedFunction(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())}/udfs/${ - createdResource.id - }` - ); - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - - this.node = this.collection.createUserDefinedFunctionNode(createdResource); - TelemetryProcessor.traceSuccess( - Action.CreateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - - tabTitle: this.tabTitle(), - }, - startKey - ); - this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); - return createdResource; - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - return Promise.reject(createError); - } - ) - .finally(() => this.isExecuting(false)); - } - - private _getResource() { - const resource = { - _rid: this.resource()._rid, - _self: this.resource()._self, - id: this.id(), - body: this.editorContent(), - }; - - return resource; - } -} diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.tsx b/src/Explorer/Tabs/UserDefinedFunctionTab.tsx new file mode 100644 index 000000000..ba3744422 --- /dev/null +++ b/src/Explorer/Tabs/UserDefinedFunctionTab.tsx @@ -0,0 +1,41 @@ +import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; +import React from "react"; +import * as ViewModels from "../../Contracts/ViewModels"; +import UserDefinedFunction from "../Tree/UserDefinedFunction"; +import ScriptTabBase from "./ScriptTabBase"; +import UserDefinedFunctionTabContent from "./UserDefinedFunctionTabContent"; + +export default class UserDefinedFunctionTab extends ScriptTabBase { + public onSaveClick: () => Promise; + public onUpdateClick: () => Promise; + public collection: ViewModels.Collection; + public node: UserDefinedFunction; + constructor(options: ViewModels.ScriptTabOption) { + super(options); + this.ariaLabel("User Defined Function Body"); + super.onActivate.bind(this); + super.buildCommandBarOptions.bind(this); + super.buildCommandBarOptions(); + } + + addNodeInCollection(createdResource: Resource & UserDefinedFunctionDefinition): void { + this.node = this.collection.createUserDefinedFunctionNode(createdResource); + } + + updateNodeInCollection(updateResource: Resource & UserDefinedFunctionDefinition): void { + this.node.id(updateResource.id); + this.node.body(updateResource.body as string); + } + + render(): JSX.Element { + return ( + this.addNodeInCollection(createdResource)} + updateNodeInCollection={(updateResource: Resource & UserDefinedFunctionDefinition) => + this.updateNodeInCollection(updateResource) + } + /> + ); + } +} diff --git a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx new file mode 100644 index 000000000..2ff327a5f --- /dev/null +++ b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx @@ -0,0 +1,300 @@ +import { UserDefinedFunctionDefinition } from "@azure/cosmos"; +import { Label, TextField } from "@fluentui/react"; +import React, { Component } from "react"; +import DiscardIcon from "../../../images/discard.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import * as Constants from "../../Common/Constants"; +import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction"; +import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +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 { EditorReact } from "../Controls/Editor/EditorReact"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import UserDefinedFunctionTab from "./UserDefinedFunctionTab"; + +interface IUserDefinedFunctionTabContentState { + udfId: string; + udfBody: string; + isUdfIdEditable: boolean; +} + +interface Ibutton { + visible: boolean; + enabled: boolean; +} + +export default class UserDefinedFunctionTabContent extends Component< + UserDefinedFunctionTab, + IUserDefinedFunctionTabContentState +> { + public saveButton: Ibutton; + public updateButton: Ibutton; + public discardButton: Ibutton; + + constructor(props: UserDefinedFunctionTab) { + super(props); + + this.saveButton = { + visible: props.isNew(), + enabled: false, + }; + this.updateButton = { + visible: !props.isNew(), + enabled: true, + }; + + this.discardButton = { + visible: true, + enabled: true, + }; + + const { id, body } = props.resource(); + this.state = { + udfId: id, + udfBody: body, + isUdfIdEditable: props.isNew() ? true : false, + }; + } + + private handleUdfIdChange = ( + _event: React.FormEvent, + newValue?: string + ): void => { + this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue); + this.setState({ udfId: newValue }); + }; + + private handleUdfBodyChange = (newContent: string) => { + this.setState({ udfBody: newContent }); + }; + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + const label = "Save"; + if (this.saveButton.visible) { + buttons.push({ + ...this, + setState: this.setState, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveButton.enabled, + }); + } + + if (this.updateButton.visible) { + const label = "Update"; + buttons.push({ + ...this, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onUpdateClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.updateButton.enabled, + }); + } + + if (this.discardButton.visible) { + const label = "Discard"; + buttons.push({ + setState: this.setState, + ...this, + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onDiscard, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardButton.enabled, + }); + } + return buttons; + } + + private async onSaveClick(): Promise { + const { udfId, udfBody } = this.state; + const resource: UserDefinedFunctionDefinition = { + id: udfId, + body: udfBody, + }; + + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }); + + try { + const createdResource = await createUserDefinedFunction( + this.props.collection.databaseId, + this.props.collection.id(), + resource + ); + if (createdResource) { + this.props.tabTitle(createdResource.id); + this.props.isNew(false); + this.updateButton.visible = true; + this.saveButton.visible = false; + this.props.resource(createdResource); + this.props.addNodeInCollection(createdResource); + this.setState({ isUdfIdEditable: false }); + this.props.isExecuting(false); + TelemetryProcessor.traceSuccess( + Action.CreateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + + tabTitle: this.props.tabTitle(), + }, + startKey + ); + this.props.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); + } + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.CreateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + return Promise.reject(createError); + } + } + + private async onUpdateClick(): Promise { + const { udfId, udfBody } = this.state; + const resource: UserDefinedFunctionDefinition = { + id: udfId, + body: udfBody, + }; + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }); + + try { + const createdResource = await updateUserDefinedFunction( + this.props.collection.databaseId, + this.props.collection.id(), + resource + ); + + this.props.resource(createdResource); + this.props.tabTitle(createdResource.id); + this.props.updateNodeInCollection(createdResource); + this.props.isExecuting(false); + TelemetryProcessor.traceSuccess( + Action.UpdateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }, + startKey + ); + + this.props.editorContent.setBaseline(createdResource.body as string); + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.UpdateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + } + } + + private onDiscard(): void { + const { id, body } = this.props.resource(); + this.setState({ + udfId: id, + udfBody: body, + }); + } + + private isValidId(id: string): boolean { + if (!id) { + return false; + } + + const invalidStartCharacters = /^[/?#\\]/; + if (invalidStartCharacters.test(id)) { + return false; + } + + const invalidMiddleCharacters = /^.+[/?#\\]/; + if (invalidMiddleCharacters.test(id)) { + return false; + } + + const invalidEndCharacters = /.*[/?#\\ ]$/; + if (invalidEndCharacters.test(id)) { + return false; + } + + return true; + } + + private isNotEmpty(value: string): boolean { + return !!value; + } + + componentDidUpdate(_prevProps: UserDefinedFunctionTab, prevState: IUserDefinedFunctionTabContentState): void { + const { udfBody, udfId } = this.state; + if (udfId !== prevState.udfId || udfBody !== prevState.udfBody) { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } + } + + render(): JSX.Element { + const { udfId, udfBody, isUdfIdEditable } = this.state; + return ( +
+ + + +
+ ); + } +} diff --git a/src/Explorer/Tabs/useTabs.test.ts b/src/Explorer/Tabs/useTabs.test.ts new file mode 100644 index 000000000..8d8148c69 --- /dev/null +++ b/src/Explorer/Tabs/useTabs.test.ts @@ -0,0 +1,139 @@ +import * as ko from "knockout"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; +import { updateUserContext } from "../../UserContext"; +import { container } from "../Controls/Settings/TestUtils"; +import DocumentId from "../Tree/DocumentId"; +import DocumentsTab from "./DocumentsTab"; +import { NewQueryTab } from "./QueryTab/QueryTab"; + +describe("useTabs tests", () => { + let database: ViewModels.Database; + let collection: ViewModels.Collection; + let queryTab: NewQueryTab; + let documentsTab: DocumentsTab; + + beforeEach(() => { + updateUserContext({ + databaseAccount: { + id: "test", + name: "test", + location: "", + type: "", + kind: "", + properties: undefined, + }, + }); + + database = { + id: ko.observable("test"), + isDatabaseShared: () => false, + } as ViewModels.Database; + database.isDatabaseExpanded = ko.observable(true); + database.selectedSubnodeKind = ko.observable(); + + collection = { + databaseId: "test", + id: ko.observable("test"), + } as ViewModels.Collection; + collection.getDatabase = (): ViewModels.Database => database; + collection.isCollectionExpanded = ko.observable(true); + collection.selectedSubnodeKind = ko.observable(); + + queryTab = new NewQueryTab( + { + tabKind: ViewModels.CollectionTabKind.Query, + collection, + database, + title: "", + tabPath: "", + queryText: "", + partitionKey: collection.partitionKey, + onLoadStartKey: 1, + }, + { + container: container, + } + ); + + documentsTab = new DocumentsTab({ + partitionKey: undefined, + documentIds: ko.observableArray(), + tabKind: ViewModels.CollectionTabKind.Documents, + collection, + title: "", + tabPath: "", + }); + + // make sure tabs have different tabId + queryTab.tabId = "1"; + documentsTab.tabId = "2"; + }); + + beforeEach(() => useTabs.setState({ openedTabs: [], activeTab: undefined })); + + it("open new tabs", () => { + const { activateNewTab } = useTabs.getState(); + activateNewTab(queryTab); + let tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(1); + expect(tabsState.openedTabs[0]).toEqual(queryTab); + expect(tabsState.activeTab).toEqual(queryTab); + expect(queryTab.isActive()).toBe(true); + + activateNewTab(documentsTab); + tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(2); + expect(tabsState.openedTabs[1]).toEqual(documentsTab); + expect(tabsState.activeTab).toEqual(documentsTab); + expect(queryTab.isActive()).toBe(false); + expect(documentsTab.isActive()).toBe(true); + }); + + it("open existing tabs", () => { + const { activateNewTab, activateTab } = useTabs.getState(); + activateNewTab(queryTab); + activateNewTab(documentsTab); + activateTab(queryTab); + + const { openedTabs, activeTab } = useTabs.getState(); + expect(openedTabs.length).toBe(2); + expect(activeTab).toEqual(queryTab); + expect(queryTab.isActive()).toBe(true); + expect(documentsTab.isActive()).toBe(false); + }); + + it("get tabs", () => { + const { activateNewTab, getTabs } = useTabs.getState(); + activateNewTab(queryTab); + activateNewTab(documentsTab); + + const queryTabs = getTabs(ViewModels.CollectionTabKind.Query); + expect(queryTabs.length).toBe(1); + expect(queryTabs[0]).toEqual(queryTab); + + const documentsTabs = getTabs(ViewModels.CollectionTabKind.Documents, (tab) => tab.tabId === documentsTab.tabId); + expect(documentsTabs.length).toBe(1); + expect(documentsTabs[0]).toEqual(documentsTab); + }); + + it("close tabs", () => { + const { activateNewTab, closeTab, closeTabsByComparator } = useTabs.getState(); + activateNewTab(queryTab); + activateNewTab(documentsTab); + closeTab(documentsTab); + + let tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(1); + expect(tabsState.openedTabs[0]).toEqual(queryTab); + expect(tabsState.activeTab).toEqual(queryTab); + expect(queryTab.isActive()).toBe(true); + expect(documentsTab.isActive()).toBe(false); + + closeTabsByComparator((tab) => tab.tabId === queryTab.tabId); + tabsState = useTabs.getState(); + expect(tabsState.openedTabs.length).toBe(0); + expect(tabsState.activeTab).toEqual(undefined); + expect(queryTab.isActive()).toBe(false); + }); +}); diff --git a/src/Explorer/Tree/Collection.test.ts b/src/Explorer/Tree/Collection.test.ts index 7e185ef4a..1c701798e 100644 --- a/src/Explorer/Tree/Collection.test.ts +++ b/src/Explorer/Tree/Collection.test.ts @@ -1,22 +1,15 @@ -import * as ko from "knockout"; import * as DataModels from "../../Contracts/DataModels"; import Explorer from "../Explorer"; import Collection from "./Collection"; 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); - } + const generateCollection = (container: Explorer, databaseId: string, data: DataModels.Collection): Collection => + new Collection(container, databaseId, data); - function generateMockCollectionsDataModelWithPartitionKey( + const generateMockCollectionsDataModelWithPartitionKey = ( partitionKey: DataModels.PartitionKey - ): DataModels.Collection { + ): DataModels.Collection => { return { defaultTtl: 1, indexingPolicy: {} as DataModels.IndexingPolicy, @@ -27,19 +20,12 @@ describe("Collection", () => { _ts: 1, id: "", }; - } + }; - function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection { + const generateMockCollectionWithDataModel = (data: DataModels.Collection): Collection => { const mockContainer = {} as Explorer; - - mockContainer.isDatabaseNodeOrNoneSelected = () => { - return false; - }; - - mockContainer.deleteCollectionText = ko.observable("delete collection"); - - return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer); - } + return generateCollection(mockContainer, "abc", data); + }; describe("Partition key path parsing", () => { let collection: Collection; @@ -95,7 +81,7 @@ describe("Collection", () => { kind: "Hash", }); collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyPropertyHeader).toBeNull; + expect(collection.partitionKeyPropertyHeader).toBeNull(); }); }); }); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 1239b1257..cefa6bbeb 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -15,21 +15,27 @@ import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { UploadDetailsRecord } from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; +import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; +import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import ConflictsTab from "../Tabs/ConflictsTab"; import DocumentsTab from "../Tabs/DocumentsTab"; import GraphTab from "../Tabs/GraphTab"; import MongoDocumentsTab from "../Tabs/MongoDocumentsTab"; -import MongoQueryTab from "../Tabs/MongoQueryTab"; -import MongoShellTab from "../Tabs/MongoShellTab"; -import QueryTab from "../Tabs/QueryTab"; +import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab"; +import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab"; +import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import QueryTablesTab from "../Tabs/QueryTablesTab"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; +import { useDatabases } from "../useDatabases"; +import { useSelectedNode } from "../useSelectedNode"; import ConflictId from "./ConflictId"; import DocumentId from "./DocumentId"; import StoredProcedure from "./StoredProcedure"; @@ -171,6 +177,19 @@ export default class Collection implements ViewModels.Collection { }); this.children = ko.observableArray([]); + this.children.subscribe(() => { + // update the database in zustand store + const database = this.getDatabase(); + database.collections( + database.collections()?.map((collection) => { + if (collection.id() === this.id()) { + return this; + } + return collection; + }) + ); + useDatabases.getState().updateDatabase(database); + }); this.storedProcedures = ko.computed(() => { return this.children() @@ -206,7 +225,7 @@ export default class Collection implements ViewModels.Collection { } public expandCollapseCollection() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Collection node", @@ -220,10 +239,12 @@ export default class Collection implements ViewModels.Collection { } else { this.expandCollection(); } - this.container.onUpdateTabsButtons([]); - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useCommandBar.getState().setContextButtons([]); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public collapseCollection() { @@ -259,7 +280,7 @@ export default class Collection implements ViewModels.Collection { } public onDocumentDBDocumentsClick() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Documents node", @@ -270,14 +291,16 @@ export default class Collection implements ViewModels.Collection { 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() - ) as DocumentsTab[]; + const documentsTabs: DocumentsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Documents, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as DocumentsTab[]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; if (documentsTab) { - this.container.tabsManager.activateTab(documentsTab); + useTabs.getState().activateTab(documentsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -296,17 +319,15 @@ export default class Collection implements ViewModels.Collection { collection: this, node: this, tabPath: `${this.databaseId}>${this.id()}>Documents`, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/documents`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(documentsTab); + useTabs.getState().activateNewTab(documentsTab); } } public onConflictsClick() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Conflicts node", @@ -317,14 +338,16 @@ export default class Collection implements ViewModels.Collection { 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() - ) as ConflictsTab[]; + const conflictsTabs: ConflictsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Conflicts, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as ConflictsTab[]; let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0]; if (conflictsTab) { - this.container.tabsManager.activateTab(conflictsTab); + useTabs.getState().activateTab(conflictsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -343,17 +366,15 @@ export default class Collection implements ViewModels.Collection { collection: this, node: this, tabPath: `${this.databaseId}>${this.id()}>Conflicts`, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/conflicts`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(conflictsTab); + useTabs.getState().activateNewTab(conflictsTab); } } public onTableEntitiesClick() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.QueryTables); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Entities node", @@ -370,14 +391,16 @@ 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() - ) as QueryTablesTab[]; + const queryTablesTabs: QueryTablesTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.QueryTables, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as QueryTablesTab[]; let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0]; if (queryTablesTab) { - this.container.tabsManager.activateTab(queryTablesTab); + useTabs.getState().activateTab(queryTablesTab); } else { this.documentIds([]); let title = `Entities`; @@ -398,17 +421,15 @@ export default class Collection implements ViewModels.Collection { tabPath: "", collection: this, node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(queryTablesTab); + useTabs.getState().activateNewTab(queryTablesTab); } } public onGraphDocumentsClick() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Documents node", @@ -419,14 +440,16 @@ export default class Collection implements ViewModels.Collection { 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() - ) as GraphTab[]; + const graphTabs: GraphTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Graph, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as GraphTab[]; let graphTab: GraphTab = graphTabs && graphTabs[0]; if (graphTab) { - this.container.tabsManager.activateTab(graphTab); + useTabs.getState().activateTab(graphTab); } else { this.documentIds([]); const title = "Graph"; @@ -448,20 +471,18 @@ export default class Collection implements ViewModels.Collection { collection: this, masterKey: userContext.masterKey || "", collectionPartitionKeyProperty: this.partitionKeyProperty, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`, collectionId: this.id(), databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(graphTab); + useTabs.getState().activateNewTab(graphTab); } } public onMongoDBDocumentsClick = () => { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Documents node", @@ -472,14 +493,16 @@ export default class Collection implements ViewModels.Collection { 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() - ) as MongoDocumentsTab[]; + const mongoDocumentsTabs: MongoDocumentsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Documents, + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ) as MongoDocumentsTab[]; let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0]; if (mongoDocumentsTab) { - this.container.tabsManager.activateTab(mongoDocumentsTab); + useTabs.getState().activateTab(mongoDocumentsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -498,16 +521,14 @@ export default class Collection implements ViewModels.Collection { tabPath: "", collection: this, node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(mongoDocumentsTab); + useTabs.getState().activateNewTab(mongoDocumentsTab); } }; public onSchemaAnalyzerClick = async () => { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer); const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default; TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { @@ -517,13 +538,13 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - for (const tab of this.container.tabsManager.openedTabs()) { + for (const tab of useTabs.getState().openedTabs) { if ( tab instanceof SchemaAnalyzerTab && tab.collection?.databaseId === this.databaseId && tab.collection?.id() === this.id() ) { - return this.container.tabsManager.activateTab(tab); + return useTabs.getState().activateTab(tab); } } @@ -534,7 +555,7 @@ export default class Collection implements ViewModels.Collection { tabTitle: "Schema", }); this.documentIds([]); - this.container.tabsManager.activateNewTab( + useTabs.getState().activateNewTab( new SchemaAnalyzerTab({ account: userContext.databaseAccount, masterKey: userContext.masterKey || "", @@ -544,15 +565,14 @@ export default class Collection implements ViewModels.Collection { tabPath: "", collection: this, node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/schemaAnalyzer`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }) ); }; public onSettingsClick = async (): Promise => { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); + await this.loadOffer(); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Settings node", @@ -564,12 +584,9 @@ export default class Collection implements ViewModels.Collection { }); const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; - const matchingTabs = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.CollectionSettingsV2, - (tab) => { - return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); - } - ); + const matchingTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.CollectionSettingsV2, (tab) => { + return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); + }); const traceStartData = { databaseName: this.databaseId, @@ -585,8 +602,6 @@ export default class Collection implements ViewModels.Collection { tabPath: "", collection: this, node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }; let settingsTabV2 = matchingTabs && (matchingTabs[0] as CollectionSettingsTabV2); @@ -603,15 +618,15 @@ export default class Collection implements ViewModels.Collection { settingsTabOptions.onLoadStartKey = startKey; settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2; settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions); - this.container.tabsManager.activateNewTab(settingsTabV2); + useTabs.getState().activateNewTab(settingsTabV2); } else { - this.container.tabsManager.activateTab(settingsTabV2); + useTabs.getState().activateTab(settingsTabV2); } }; public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; - const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const title = "Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -621,25 +636,26 @@ export default class Collection implements ViewModels.Collection { tabTitle: title, }); - const queryTab: QueryTab = new QueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - title: title, - tabPath: "", - collection: this, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, - queryText: queryText, - partitionKey: collection.partitionKey, - onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, - }); - - this.container.tabsManager.activateNewTab(queryTab); + useTabs.getState().activateNewTab( + new NewQueryTab( + { + tabKind: ViewModels.CollectionTabKind.Query, + title: title, + tabPath: "", + collection: this, + node: this, + queryText: queryText, + partitionKey: collection.partitionKey, + onLoadStartKey: startKey, + }, + { container: this.container } + ) + ); } public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; - const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const title = "Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { @@ -650,23 +666,27 @@ export default class Collection implements ViewModels.Collection { tabTitle: title, }); - const mongoQueryTab: MongoQueryTab = new MongoQueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - title: title, - tabPath: "", - collection: this, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`, - partitionKey: collection.partitionKey, - onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, - }); + const newMongoQueryTab: NewMongoQueryTab = new NewMongoQueryTab( + { + tabKind: ViewModels.CollectionTabKind.Query, + title: title, + tabPath: "", + collection: this, + node: this, + partitionKey: collection.partitionKey, + onLoadStartKey: startKey, + }, + { + container: this.container, + viewModelcollection: this, + } + ); - this.container.tabsManager.activateNewTab(mongoQueryTab); + useTabs.getState().activateNewTab(newMongoQueryTab); } public onNewGraphClick() { - const id: number = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Graph).length + 1; + const id: number = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Graph).length + 1; const title: string = "Graph Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { @@ -686,38 +706,46 @@ export default class Collection implements ViewModels.Collection { collection: this, masterKey: userContext.masterKey || "", collectionPartitionKeyProperty: this.partitionKeyProperty, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`, collectionId: this.id(), databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(graphTab); + useTabs.getState().activateNewTab(graphTab); } public onNewMongoShellClick() { - const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.MongoShell).length + 1; - const mongoShellTab: MongoShellTab = new MongoShellTab({ - tabKind: ViewModels.CollectionTabKind.MongoShell, - title: "Shell " + id, - tabPath: "", - collection: this, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, - }); + const mongoShellTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.MongoShell) as NewMongoShellTab[]; - this.container.tabsManager.activateNewTab(mongoShellTab); + let index = 1; + if (mongoShellTabs.length > 0) { + index = mongoShellTabs[mongoShellTabs.length - 1].index + 1; + } + + const mongoShellTab: NewMongoShellTab = new NewMongoShellTab( + { + tabKind: ViewModels.CollectionTabKind.MongoShell, + title: "Shell " + index, + tabPath: "", + collection: this, + node: this, + index: index, + }, + { + container: this.container, + } + ); + + useTabs.getState().activateNewTab(mongoShellTab); } public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) { StoredProcedure.create(source, event); } - public onNewUserDefinedFunctionClick(source: ViewModels.Collection, event: MouseEvent) { - UserDefinedFunction.create(source, event); + public onNewUserDefinedFunctionClick(source: ViewModels.Collection) { + UserDefinedFunction.create(source); } public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) { @@ -726,21 +754,21 @@ export default class Collection implements ViewModels.Collection { public createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure { const node = new StoredProcedure(this.container, this, data); - this.container.selectedNode(node); + useSelectedNode.getState().setSelectedNode(node); this.children.push(node); return node; } public createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction { const node = new UserDefinedFunction(this.container, this, data); - this.container.selectedNode(node); + useSelectedNode.getState().setSelectedNode(node); this.children.push(node); return node; } public createTriggerNode(data: TriggerDefinition & Resource): Trigger { const node = new Trigger(this.container, this, data); - this.container.selectedNode(node); + useSelectedNode.getState().setSelectedNode(node); this.children.push(node); return node; } @@ -767,9 +795,11 @@ export default class Collection implements ViewModels.Collection { } else { this.expandStoredProcedures(); } - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public expandStoredProcedures() { @@ -826,9 +856,11 @@ export default class Collection implements ViewModels.Collection { } else { this.expandUserDefinedFunctions(); } - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public expandUserDefinedFunctions() { @@ -885,9 +917,11 @@ export default class Collection implements ViewModels.Collection { } else { this.expandTriggers(); } - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() - ); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() + ); } public expandTriggers() { @@ -962,7 +996,9 @@ export default class Collection implements ViewModels.Collection { 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 triggerNodes: ViewModels.TreeNode[] = triggers.map( + (trigger: SqlTriggerResource | TriggerDefinition) => new Trigger(this.container, this, trigger) + ); const otherNodes = this.children().filter((node) => node.nodeKind !== "Trigger"); const allNodes = otherNodes.concat(triggerNodes); this.children(allNodes); @@ -1141,11 +1177,11 @@ export default class Collection implements ViewModels.Collection { } public getDatabase(): ViewModels.Database { - return this.container.findDatabaseWithId(this.databaseId); + return useDatabases.getState().findDatabaseWithId(this.databaseId); } public async loadOffer(): Promise { - if (!this.isOfferRead && !this.container.isServerlessEnabled() && !this.offer()) { + if (!this.isOfferRead && !isServerlessAccount() && !this.offer()) { const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, { databaseName: this.databaseId, collectionName: this.id(), diff --git a/src/Explorer/Tree/Database.test.ts b/src/Explorer/Tree/Database.test.tsx similarity index 89% rename from src/Explorer/Tree/Database.test.ts rename to src/Explorer/Tree/Database.test.tsx index d364adca5..47e4b665a 100644 --- a/src/Explorer/Tree/Database.test.ts +++ b/src/Explorer/Tree/Database.test.tsx @@ -1,7 +1,7 @@ -import * as ko from "knockout"; import { HttpStatusCodes } from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; import { JunoClient } from "../../Juno/JunoClient"; +import { Features } from "../../Platform/Hosted/extractFeatures"; import { updateUserContext, userContext } from "../../UserContext"; import Explorer from "../Explorer"; import Database from "./Database"; @@ -31,11 +31,10 @@ updateUserContext({ describe("Add Schema", () => { it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => { - const collection: DataModels.Collection = {} as DataModels.Collection; + const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection; collection.analyticalStorageTtl = undefined; - const database = new Database(createMockContainer(), { id: "fakeId" }); + const database = new Database(createMockContainer(), collection); database.container = createMockContainer(); - database.container.isSchemaEnabled = ko.computed(() => false); database.junoClient = new JunoClient(); database.junoClient.requestSchema = jest.fn(); @@ -47,12 +46,16 @@ describe("Add Schema", () => { }); it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => { - const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection; + const collection: DataModels.Collection = {} as DataModels.Collection; collection.analyticalStorageTtl = 0; - const database = new Database(createMockContainer(), {}); + const database = new Database(createMockContainer(), collection); database.container = createMockContainer(); - database.container.isSchemaEnabled = ko.computed(() => true); + updateUserContext({ + features: { + enableSchema: true, + } as Features, + }); database.junoClient = new JunoClient(); database.junoClient.requestSchema = jest.fn(); diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.tsx similarity index 85% rename from src/Explorer/Tree/Database.ts rename to src/Explorer/Tree/Database.tsx index f9cc2e605..5dc01343f 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.tsx @@ -1,4 +1,5 @@ import * as ko from "knockout"; +import React from "react"; import * as _ from "underscore"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; @@ -9,15 +10,21 @@ import * as Logger from "../../Common/Logger"; import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useSidePanel } from "../../hooks/useSidePanel"; +import { useTabs } from "../../hooks/useTabs"; import { IJunoResponse, JunoClient } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; +import { getCollectionName } from "../../Utils/APITypeUtils"; +import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; +import { AddCollectionPanel } from "../Panes/AddCollectionPanel"; import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2"; +import { useDatabases } from "../useDatabases"; +import { useSelectedNode } from "../useSelectedNode"; import Collection from "./Collection"; - export default class Database implements ViewModels.Database { public nodeKind: string; public container: Explorer; @@ -32,7 +39,7 @@ export default class Database implements ViewModels.Database { public junoClient: JunoClient; private isOfferRead: boolean; - constructor(container: Explorer, data: any) { + constructor(container: Explorer, data: DataModels.Database) { this.nodeKind = "Database"; this.container = container; this.self = data._self; @@ -40,6 +47,7 @@ export default class Database implements ViewModels.Database { this.id = ko.observable(data.id); this.offer = ko.observable(); this.collections = ko.observableArray(); + this.collections.subscribe(() => useDatabases.getState().updateDatabase(this)); this.isDatabaseExpanded = ko.observable(false); this.selectedSubnodeKind = ko.observable(); this.isDatabaseShared = ko.pureComputed(() => { @@ -49,8 +57,8 @@ export default class Database implements ViewModels.Database { this.isOfferRead = false; } - public onSettingsClick = () => { - this.container.selectedNode(this); + public onSettingsClick = (): void => { + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Settings node", @@ -60,7 +68,7 @@ export default class Database implements ViewModels.Database { const pendingNotificationsPromise: Promise = this.getPendingThroughputSplitNotification(); const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2; - const matchingTabs = this.container.tabsManager.getTabs(tabKind, (tab) => tab.node?.id() === this.id()); + const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id()); let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2; if (!settingsTab) { @@ -70,6 +78,7 @@ export default class Database implements ViewModels.Database { tabTitle: "Scale", }); pendingNotificationsPromise.then( + // eslint-disable-next-line @typescript-eslint/no-explicit-any (data: any) => { const pendingNotification: DataModels.Notification = data?.[0]; const tabOptions: ViewModels.TabOptions = { @@ -79,15 +88,13 @@ export default class Database implements ViewModels.Database { node: this, rid: this.rid, database: this, - hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }; settingsTab = new DatabaseSettingsTabV2(tabOptions); settingsTab.pendingNotification(pendingNotification); - this.container.tabsManager.activateNewTab(settingsTab); + useTabs.getState().activateNewTab(settingsTab); }, - (error: any) => { + (error) => { const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( Action.Tab, @@ -110,35 +117,16 @@ export default class Database implements ViewModels.Database { pendingNotificationsPromise.then( (pendingNotification: DataModels.Notification) => { settingsTab.pendingNotification(pendingNotification); - this.container.tabsManager.activateTab(settingsTab); + useTabs.getState().activateTab(settingsTab); }, - (error: any) => { + () => { settingsTab.pendingNotification(undefined); - this.container.tabsManager.activateTab(settingsTab); + useTabs.getState().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 selectDatabase() { - this.container.selectedNode(this); - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Database node", - - dataExplorerArea: Constants.Areas.ResourceTree, - }); - } - public async expandDatabase() { if (this.isDatabaseExpanded()) { return; @@ -205,10 +193,18 @@ export default class Database implements ViewModels.Database { //merge collections this.addCollectionsToList(collectionVMs); this.deleteCollectionsFromList(deltaCollections.toDelete); + + useDatabases.getState().updateDatabase(this); } - public openAddCollection(database: Database) { - database.container.openAddCollectionPanel(database.id()); + public async openAddCollection(database: Database): Promise { + await useDatabases.getState().loadDatabaseOffers(); + useSidePanel + .getState() + .openSidePanel( + "New " + getCollectionName(), + + ); } public findCollectionWithId(collectionId: string): ViewModels.Collection { @@ -216,7 +212,7 @@ export default class Database implements ViewModels.Database { } public async loadOffer(): Promise { - if (!this.isOfferRead && !this.container.isServerlessEnabled() && !this.offer()) { + if (!this.isOfferRead && !isServerlessAccount() && !this.offer()) { const params: DataModels.ReadDatabaseOfferParams = { databaseId: this.id(), databaseResourceId: this.self, @@ -238,7 +234,7 @@ export default class Database implements ViewModels.Database { } return _.find(notifications, (notification: DataModels.Notification) => { - const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress"); + const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress"); return ( notification.kind === "message" && !notification.collectionName && @@ -276,7 +272,7 @@ export default class Database implements ViewModels.Database { } ); - let collectionsToDelete: Collection[] = []; + const collectionsToDelete: Collection[] = []; ko.utils.arrayForEach(this.collections(), (collection: Collection) => { const collectionPresentInUpdatedList = _.some( updatedCollectionsList, @@ -316,10 +312,10 @@ export default class Database implements ViewModels.Database { } public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout { - let checkForSchema: NodeJS.Timeout = null; + let checkForSchema: NodeJS.Timeout; interval = interval || 5000; - if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) { + if (collection.analyticalStorageTtl !== undefined && userContext.features.enableSchema) { collection.requestSchema = () => { this.junoClient.requestSchema({ id: undefined, @@ -342,7 +338,7 @@ export default class Database implements ViewModels.Database { clearInterval(checkForSchema); } - if (response.data !== null) { + if (response.data !== undefined) { clearInterval(checkForSchema); collection.schema = response.data; } diff --git a/src/Explorer/Tree/ResourceTokenCollection.ts b/src/Explorer/Tree/ResourceTokenCollection.ts index dbc5ac060..04daa74a3 100644 --- a/src/Explorer/Tree/ResourceTokenCollection.ts +++ b/src/Explorer/Tree/ResourceTokenCollection.ts @@ -2,14 +2,17 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import DocumentId from "./DocumentId"; -import DocumentsTab from "../Tabs/DocumentsTab"; -import Q from "q"; -import QueryTab from "../Tabs/QueryTab"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; import Explorer from "../Explorer"; +import DocumentsTab from "../Tabs/DocumentsTab"; +import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import TabsBase from "../Tabs/TabsBase"; +import { useDatabases } from "../useDatabases"; +import { useSelectedNode } from "../useSelectedNode"; +import DocumentId from "./DocumentId"; export default class ResourceTokenCollection implements ViewModels.CollectionBase { public nodeKind: string; @@ -75,7 +78,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; - const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const title = "Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -85,25 +88,25 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas tabTitle: title, }); - const queryTab: QueryTab = new QueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - title: title, - tabPath: "", - collection: this, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, - queryText: queryText, - partitionKey: collection.partitionKey, - resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(), - onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, - }); - - this.container.tabsManager.activateNewTab(queryTab); + useTabs.getState().activateNewTab( + new NewQueryTab( + { + tabKind: ViewModels.CollectionTabKind.Query, + title: title, + tabPath: "", + collection: this, + node: this, + queryText: queryText, + partitionKey: collection.partitionKey, + onLoadStartKey: startKey, + }, + { container: this.container } + ) + ); } public onDocumentDBDocumentsClick() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Documents node", @@ -113,16 +116,18 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas dataExplorerArea: Constants.Areas.ResourceTree, }); - const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.Documents, - (tab: TabsBase) => - tab.collection?.id() === this.id() && - (tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId - ) as DocumentsTab[]; + const documentsTabs: DocumentsTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.Documents, + (tab: TabsBase) => + tab.collection?.id() === this.id() && + (tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId + ) as DocumentsTab[]; let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; if (documentsTab) { - this.container.tabsManager.activateTab(documentsTab); + useTabs.getState().activateTab(documentsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, @@ -134,23 +139,21 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas documentsTab = new DocumentsTab({ partitionKey: this.partitionKey, - resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(), + resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey, documentIds: ko.observableArray([]), tabKind: ViewModels.CollectionTabKind.Documents, title: "Items", collection: this, node: this, tabPath: `${this.databaseId}>${this.id()}>Documents`, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/documents`, onLoadStartKey: startKey, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(documentsTab); + useTabs.getState().activateNewTab(documentsTab); } } public getDatabase(): ViewModels.Database { - return this.container.findDatabaseWithId(this.databaseId); + return useDatabases.getState().findDatabaseWithId(this.databaseId); } } diff --git a/src/Explorer/Tree/ResourceTokenTree.tsx b/src/Explorer/Tree/ResourceTokenTree.tsx new file mode 100644 index 000000000..b405e2559 --- /dev/null +++ b/src/Explorer/Tree/ResourceTokenTree.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import CollectionIcon from "../../../images/tree-collection.svg"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; +import { userContext } from "../../UserContext"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; +import { useDatabases } from "../useDatabases"; +import { useSelectedNode } from "../useSelectedNode"; + +export const ResourceTokenTree: React.FC = (): JSX.Element => { + const collection = useDatabases((state) => state.resourceTokenCollection); + + const buildCollectionNode = (): TreeNode => { + if (!collection) { + return { + label: undefined, + isExpanded: true, + children: [], + }; + } + + const children: TreeNode[] = []; + children.push({ + label: "Items", + onClick: () => { + collection.onDocumentDBDocumentsClick(); + // push to most recent + mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); + }, + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Documents]), + }); + + const collectionNode: TreeNode = { + label: collection.id(), + iconSrc: CollectionIcon, + isExpanded: true, + children, + className: "collectionHeader", + onClick: () => { + // Rewritten version of expandCollapseCollection + useSelectedNode.getState().setSelectedNode(collection); + useCommandBar.getState().setContextButtons([]); + useTabs + .getState() + .refreshActiveTab( + (tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), + }; + + return { + label: undefined, + isExpanded: true, + children: [collectionNode], + }; + }; + + return ; +}; diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx new file mode 100644 index 000000000..b5b6dfe92 --- /dev/null +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -0,0 +1,722 @@ +import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; +import * as React from "react"; +import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; +import DeleteIcon from "../../../images/delete.svg"; +import GalleryIcon from "../../../images/GalleryIcon.svg"; +import FileIcon from "../../../images/notebook/file-cosmos.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; +import PublishIcon from "../../../images/notebook/publish_content.svg"; +import RefreshIcon from "../../../images/refresh-cosmos.svg"; +import CollectionIcon from "../../../images/tree-collection.svg"; +import { Areas } from "../../Common/Constants"; +import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { useSidePanel } from "../../hooks/useSidePanel"; +import { useTabs } from "../../hooks/useTabs"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; +import { isServerlessAccount } from "../../Utils/CapabilityUtils"; +import * as GitHubUtils from "../../Utils/GitHubUtils"; +import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; +import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import Explorer from "../Explorer"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; +import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; +import { useNotebook } from "../Notebook/useNotebook"; +import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; +import TabsBase from "../Tabs/TabsBase"; +import { useDatabases } from "../useDatabases"; +import { useSelectedNode } from "../useSelectedNode"; +import StoredProcedure from "./StoredProcedure"; +import Trigger from "./Trigger"; +import UserDefinedFunction from "./UserDefinedFunction"; + +export const MyNotebooksTitle = "My Notebooks"; +export const GitHubReposTitle = "GitHub repos"; + +interface ResourceTreeProps { + container: Explorer; +} + +export const ResourceTree: React.FC = ({ container }: ResourceTreeProps): JSX.Element => { + const databases = useDatabases((state) => state.databases); + const { + isNotebookEnabled, + myNotebooksContentRoot, + galleryContentRoot, + gitHubNotebooksContentRoot, + updateNotebookItem, + } = useNotebook(); + const { activeTab, refreshActiveTab } = useTabs(); + const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; + const pseudoDirPath = "PsuedoDir"; + + const buildGalleryCallout = (): JSX.Element => { + if ( + LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && + LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) + ) { + return undefined; + } + + const calloutProps: ICalloutProps = { + calloutMaxWidth: 350, + ariaLabel: "New gallery", + role: "alertdialog", + gapSpace: 0, + target: ".galleryHeader", + directionalHint: DirectionalHint.leftTopEdge, + onDismiss: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + }, + setInitialFocus: true, + }; + + const openGalleryProps: ILinkProps = { + onClick: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + container.openGallery(); + }, + }; + + return ( + + + + New gallery + + + Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other + contributors. + + Open gallery + + + ); + }; + + const buildNotebooksTree = (): TreeNode => { + const notebooksTree: TreeNode = { + label: undefined, + isExpanded: true, + children: [], + }; + + if (galleryContentRoot) { + notebooksTree.children.push(buildGalleryNotebooksTree()); + } + + if (myNotebooksContentRoot) { + notebooksTree.children.push(buildMyNotebooksTree()); + } + + if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + // collapse all other notebook nodes + notebooksTree.children.forEach((node) => (node.isExpanded = false)); + notebooksTree.children.push(buildGitHubNotebooksTree()); + } + + return notebooksTree; + }; + + const buildGalleryNotebooksTree = (): TreeNode => { + return { + label: "Gallery", + iconSrc: GalleryIcon, + className: "notebookHeader galleryHeader", + onClick: () => container.openGallery(), + isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, + }; + }; + + const buildMyNotebooksTree = (): TreeNode => { + const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( + myNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + } + ); + + myNotebooksTree.isExpanded = true; + myNotebooksTree.isAlphaSorted = true; + // Remove "Delete" menu item from context menu + myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); + return myNotebooksTree; + }; + + const buildGitHubNotebooksTree = (): TreeNode => { + const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( + gitHubNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + } + ); + + gitHubNotebooksTree.contextMenu = [ + { + label: "Manage GitHub settings", + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Manage GitHub settings", + + ), + }, + { + label: "Disconnect from GitHub", + onClick: () => { + TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { + dataExplorerArea: Areas.Notebook, + }); + container.notebookManager?.gitHubOAuthService.logout(); + }, + }, + ]; + + gitHubNotebooksTree.isExpanded = true; + gitHubNotebooksTree.isAlphaSorted = true; + + return gitHubNotebooksTree; + }; + + const buildChildNodes = ( + container: Explorer, + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void + ): TreeNode[] => { + if (!item || !item.children) { + return []; + } else { + return item.children.map((item) => { + const result = + item.type === NotebookContentItemType.Directory + ? buildNotebookDirectoryNode(item, onFileClick) + : buildNotebookFileNode(item, onFileClick); + result.timestamp = item.timestamp; + return result; + }); + } + }; + + const buildNotebookFileNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void + ): TreeNode => { + return { + label: item.name, + iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, + className: "notebookHeader", + onClick: () => onFileClick(item), + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: createFileContextMenu(container, item), + data: item, + }; + }; + + const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + container.showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); + }, + }, + { + label: "Copy to ...", + iconSrc: CopyIcon, + onClick: () => copyNotebook(container, item), + }, + { + label: "Download", + iconSrc: NotebookIcon, + onClick: () => container.downloadFile(item), + }, + ]; + + if (item.type === NotebookContentItemType.Notebook) { + items.push({ + label: "Publish to gallery", + iconSrc: PublishIcon, + onClick: async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.ResourceTreeMenu, + }); + + const content = await container.readFile(item); + if (content) { + await container.publishNotebook(item.name, content); + } + }, + }); + } + + // "Copy to ..." isn't needed if github locations are not available + if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + items = items.filter((item) => item.label !== "Copy to ..."); + } + + return items; + }; + + const copyNotebook = async (container: Explorer, item: NotebookContentItem) => { + const content = await container.readFile(item); + if (content) { + container.copyNotebook(item.name, content); + } + }; + + const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Refresh", + iconSrc: RefreshIcon, + onClick: () => loadSubitems(item), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + container.showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); + }, + }, + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item), + }, + { + label: "New Directory", + iconSrc: NewNotebookIcon, + onClick: () => container.onCreateDirectory(item), + }, + { + label: "New Notebook", + iconSrc: NewNotebookIcon, + onClick: () => container.onNewNotebookClicked(item), + }, + { + label: "Upload File", + iconSrc: NewNotebookIcon, + onClick: () => container.openUploadFilePanel(item), + }, + ]; + + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" + if (GitHubUtils.fromContentUri(item.path)) { + items = items.filter( + (item) => + item.label !== "Delete" && + item.label !== "Rename" && + item.label !== "New Directory" && + item.label !== "Upload File" + ); + } + + return items; + }; + + const buildNotebookDirectoryNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void + ): TreeNode => { + return { + label: item.name, + iconSrc: undefined, + className: "notebookHeader", + isAlphaSorted: true, + isLeavesParentsSeparate: true, + onClick: () => { + if (!item.children) { + loadSubitems(item); + } + }, + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined, + data: item, + children: buildChildNodes(container, item, onFileClick), + }; + }; + + const buildDataTree = (): TreeNode => { + const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { + const databaseNode: TreeNode = { + label: database.id(), + iconSrc: CosmosDBIcon, + isExpanded: false, + className: "databaseHeader", + children: [], + isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), + contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()), + onClick: async (isExpanded) => { + useSelectedNode.getState().setSelectedNode(database); + // Rewritten version of expandCollapseDatabase(): + if (isExpanded) { + database.collapseDatabase(); + } else { + if (databaseNode.children?.length === 0) { + databaseNode.isLoading = true; + } + await database.expandDatabase(); + } + databaseNode.isLoading = false; + useCommandBar.getState().setContextButtons([]); + refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); + }, + onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database), + }; + + if (database.isDatabaseShared()) { + databaseNode.children.push({ + label: "Scale", + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettingsV2]), + onClick: database.onSettingsClick.bind(database), + }); + } + + // Find collections + database + .collections() + .forEach((collection: ViewModels.Collection) => + databaseNode.children.push(buildCollectionNode(database, collection)) + ); + + database.collections.subscribe((collections: ViewModels.Collection[]) => { + collections.forEach((collection: ViewModels.Collection) => + databaseNode.children.push(buildCollectionNode(database, collection)) + ); + }); + + return databaseNode; + }); + + return { + label: undefined, + isExpanded: true, + children: databaseTreeNodes, + }; + }; + + const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => { + const children: TreeNode[] = []; + children.push({ + label: collection.getLabel(), + onClick: () => { + collection.openTab(); + // push to most recent + mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); + }, + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.Documents, + ViewModels.CollectionTabKind.Graph, + ]), + contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), + }); + + if (isNotebookEnabled && userContext.apiType === "Mongo" && isPublicInternetAccessAllowed()) { + children.push({ + label: "Schema (Preview)", + onClick: collection.onSchemaAnalyzerClick.bind(collection), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), + }); + } + + if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { + children.push({ + label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings", + onClick: collection.onSettingsClick.bind(collection), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.CollectionSettingsV2, + ]), + }); + } + + const schemaNode: TreeNode = buildSchemaNode(collection); + if (schemaNode) { + children.push(schemaNode); + } + + if (showScriptNodes) { + children.push(buildStoredProcedureNode(collection)); + children.push(buildUserDefinedFunctionsNode(collection)); + children.push(buildTriggerNode(collection)); + } + + // This is a rewrite of showConflicts + const showConflicts = + userContext?.databaseAccount?.properties.enableMultipleWriteLocations && + collection.rawDataModel && + !!collection.rawDataModel.conflictResolutionPolicy; + + if (showConflicts) { + children.push({ + label: "Conflicts", + onClick: collection.onConflictsClick.bind(collection), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]), + }); + } + + return { + label: collection.id(), + iconSrc: CollectionIcon, + isExpanded: false, + children: children, + className: "collectionHeader", + contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), + onClick: () => { + // Rewritten version of expandCollapseCollection + useSelectedNode.getState().setSelectedNode(collection); + useCommandBar.getState().setContextButtons([]); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + onExpanded: () => { + if (showScriptNodes) { + collection.loadStoredProcedures(); + collection.loadUserDefinedFunctions(); + collection.loadTriggers(); + } + }, + isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), + onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), + }; + }; + + const buildStoredProcedureNode = (collection: ViewModels.Collection): TreeNode => { + return { + label: "Stored Procedures", + children: collection.storedProcedures().map((sp: StoredProcedure) => ({ + label: sp.id(), + onClick: sp.open.bind(sp), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.StoredProcedures, + ]), + contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp), + })), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + }; + }; + + const buildUserDefinedFunctionsNode = (collection: ViewModels.Collection): TreeNode => { + return { + label: "User Defined Functions", + children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({ + label: udf.id(), + onClick: udf.open.bind(udf), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.UserDefinedFunctions, + ]), + contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf), + })), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + }; + }; + + const buildTriggerNode = (collection: ViewModels.Collection): TreeNode => { + return { + label: "Triggers", + children: collection.triggers().map((trigger: Trigger) => ({ + label: trigger.id(), + onClick: trigger.open.bind(trigger), + isSelected: () => + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]), + contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger), + })), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers); + refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); + }, + }; + }; + + const buildSchemaNode = (collection: ViewModels.Collection): TreeNode => { + if (collection.analyticalStorageTtl() === undefined) { + return undefined; + } + + if (!collection.schema || !collection.schema.fields) { + return undefined; + } + + return { + label: "Schema", + children: getSchemaNodes(collection.schema.fields), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema); + refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid); + }, + }; + }; + + const getSchemaNodes = (fields: DataModels.IDataField[]): TreeNode[] => { + const schema: any = {}; + + //unflatten + fields.forEach((field: DataModels.IDataField) => { + const path: string[] = field.path.split("."); + const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; + let current: any = {}; + path.forEach((name: string, pathIndex: number) => { + if (pathIndex === 0) { + if (schema[name] === undefined) { + if (pathIndex === path.length - 1) { + schema[name] = fieldProperties; + } else { + schema[name] = {}; + } + } + current = schema[name]; + } else { + if (current[name] === undefined) { + if (pathIndex === path.length - 1) { + current[name] = fieldProperties; + } else { + current[name] = {}; + } + } + current = current[name]; + } + }); + }); + + const traverse = (obj: any): TreeNode[] => { + const children: TreeNode[] = []; + + if (obj !== undefined && !Array.isArray(obj) && typeof obj === "object") { + Object.entries(obj).forEach(([key, value]) => { + children.push({ label: key, children: traverse(value) }); + }); + } else if (Array.isArray(obj)) { + return [{ label: obj[0] }, { label: obj[1] }]; + } + + return children; + }; + + return traverse(schema); + }; + + const loadSubitems = async (item: NotebookContentItem): Promise => { + const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); + updateNotebookItem(updatedItem); + }; + + const dataRootNode = buildDataTree(); + + if (isNotebookEnabled) { + return ( + <> + + + + + + + + + + {buildGalleryCallout()} + + ); + } + + return ; +}; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.ts b/src/Explorer/Tree/ResourceTreeAdapter.test.ts index e65ee1011..05bebd913 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.ts +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.ts @@ -1,114 +1,109 @@ import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; -import Explorer from "../Explorer"; +import { useTabs } from "../../hooks/useTabs"; import TabsBase from "../Tabs/TabsBase"; -import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; +import { useSelectedNode } from "../useSelectedNode"; -describe("ResourceTreeAdapter", () => { - const mockContainer = (): Explorer => - (({ - selectedNode: ko.observable({ - nodeKind: "nodeKind", - rid: "rid", - id: ko.observable("id"), - }), - tabsManager: { - activeTab: ko.observable({ - tabKind: ViewModels.CollectionTabKind.Documents, - } as TabsBase), - }, - isNotebookEnabled: ko.observable(true), - databases: ko.observable([]), - } as unknown) as Explorer); +describe("useSelectedNode", () => { + const mockTab = { + tabKind: ViewModels.CollectionTabKind.Documents, + } as TabsBase; // TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths describe("isDataNodeSelected", () => { + afterEach(() => { + useSelectedNode.getState().setSelectedNode(undefined); + useTabs.setState({ activeTab: undefined }); + }); it("it should not select if no selected node", () => { - const explorer = mockContainer(); - explorer.selectedNode(undefined); - const resourceTreeAdapter = new ResourceTreeAdapter(explorer); - const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); + useTabs.setState({ activeTab: mockTab }); + const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined); expect(isDataNodeSelected).toBeFalsy(); }); it("it should not select incorrect subnodekinds", () => { - const resourceTreeAdapter = new ResourceTreeAdapter(mockContainer()); - const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); + useTabs.setState({ activeTab: mockTab }); + useSelectedNode.getState().setSelectedNode({ + nodeKind: "nodeKind", + rid: "rid", + id: ko.observable("id"), + }); + const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined); expect(isDataNodeSelected).toBeFalsy(); }); it("it should not select if no active tab", () => { - const explorer = mockContainer(); - explorer.tabsManager.activeTab(undefined); - const resourceTreeAdapter = new ResourceTreeAdapter(explorer); - const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); + useSelectedNode.getState().setSelectedNode({ + nodeKind: "nodeKind", + rid: "rid", + id: ko.observable("id"), + }); + const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("foo", "bar", undefined); expect(isDataNodeSelected).toBeFalsy(); }); it("should select if correct database node regardless of subnodekinds", () => { + useTabs.setState({ activeTab: mockTab }); const subNodeKind = ViewModels.CollectionTabKind.Documents; - const explorer = mockContainer(); - explorer.selectedNode(({ + useSelectedNode.getState().setSelectedNode({ nodeKind: "Database", rid: "dbrid", id: ko.observable("dbid"), selectedSubnodeKind: ko.observable(subNodeKind), - } as unknown) as ViewModels.TreeNode); - const resourceTreeAdapter = new ResourceTreeAdapter(explorer); - const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", undefined, [ - ViewModels.CollectionTabKind.Documents, - ]); + } as ViewModels.TreeNode); + const isDataNodeSelected = useSelectedNode + .getState() + .isDataNodeSelected("dbid", undefined, [ViewModels.CollectionTabKind.Documents]); expect(isDataNodeSelected).toBeTruthy(); }); it("should select correct collection node (documents or graph node)", () => { let subNodeKind = ViewModels.CollectionTabKind.Documents; - const explorer = mockContainer(); - explorer.tabsManager.activeTab({ + let activeTab = { tabKind: subNodeKind, - } as TabsBase); - explorer.selectedNode(({ + } as TabsBase; + useTabs.setState({ activeTab }); + useSelectedNode.getState().setSelectedNode({ nodeKind: "Collection", rid: "collrid", databaseId: "dbid", id: ko.observable("collid"), selectedSubnodeKind: ko.observable(subNodeKind), - } as unknown) as ViewModels.TreeNode); - const resourceTreeAdapter = new ResourceTreeAdapter(explorer); - let isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); + } as ViewModels.TreeNode); + let isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("dbid", "collid", [subNodeKind]); expect(isDataNodeSelected).toBeTruthy(); subNodeKind = ViewModels.CollectionTabKind.Graph; - explorer.tabsManager.activeTab({ + activeTab = { tabKind: subNodeKind, - } as TabsBase); - explorer.selectedNode(({ + } as TabsBase; + useTabs.setState({ activeTab }); + useSelectedNode.getState().setSelectedNode({ nodeKind: "Collection", rid: "collrid", databaseId: "dbid", id: ko.observable("collid"), selectedSubnodeKind: ko.observable(subNodeKind), - } as unknown) as ViewModels.TreeNode); - isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); + } as ViewModels.TreeNode); + isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected("dbid", "collid", [subNodeKind]); expect(isDataNodeSelected).toBeTruthy(); }); it("should not select incorrect collection node (e.g. Settings)", () => { - const explorer = mockContainer(); - explorer.selectedNode(({ + useSelectedNode.getState().setSelectedNode({ nodeKind: "Collection", rid: "collrid", databaseId: "dbid", id: ko.observable("collid"), selectedSubnodeKind: ko.observable(ViewModels.CollectionTabKind.Documents), - } as unknown) as ViewModels.TreeNode); - explorer.tabsManager.activeTab({ + } as ViewModels.TreeNode); + const activeTab = { tabKind: ViewModels.CollectionTabKind.Documents, - } as TabsBase); - const resourceTreeAdapter = new ResourceTreeAdapter(explorer); - const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [ - ViewModels.CollectionTabKind.Settings, - ]); + } as TabsBase; + useTabs.setState({ activeTab }); + const isDataNodeSelected = useSelectedNode + .getState() + .isDataNodeSelected("dbid", "collid", [ViewModels.CollectionTabKind.Settings]); expect(isDataNodeSelected).toBeFalsy(); }); }); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx index a161eaf1d..44609d95c 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx @@ -1,12 +1,11 @@ -import * as ko from "knockout"; +import { shallow } from "enzyme"; +import React from "react"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import React from "react"; -import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; -import { shallow } from "enzyme"; -import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeComponentProps, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import Collection from "./Collection"; +import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; const schema: DataModels.ISchema = { id: "fakeSchemaId", @@ -208,16 +207,6 @@ const schema: DataModels.ISchema = { ], }; -const createMockContainer = (): Explorer => { - const mockContainer = new Explorer(); - mockContainer.selectedNode = ko.observable(); - mockContainer.onUpdateTabsButtons = () => { - return; - }; - - return mockContainer; -}; - const createMockCollection = (): ViewModels.Collection => { const mockCollection = {} as DataModels.Collection; mockCollection._rid = "fakeRid"; @@ -226,17 +215,13 @@ const createMockCollection = (): ViewModels.Collection => { mockCollection.analyticalStorageTtl = 0; mockCollection.schema = schema; - const mockCollectionVM: ViewModels.Collection = new Collection( - createMockContainer(), - "fakeDatabaseId", - mockCollection - ); + const mockCollectionVM: ViewModels.Collection = new Collection(new Explorer(), "fakeDatabaseId", mockCollection); return mockCollectionVM; }; describe("Resource tree for schema", () => { - const mockContainer: Explorer = createMockContainer(); + const mockContainer = new Explorer(); const resourceTree = new ResourceTreeAdapter(mockContainer); it("should render", () => { diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 3d7281acc..6a00e92e9 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -12,24 +12,32 @@ import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { ArrayHashMap } from "../../Common/ArrayHashMap"; import { Areas } from "../../Common/Constants"; +import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useSidePanel } from "../../hooks/useSidePanel"; +import { useTabs } from "../../hooks/useTabs"; import { IPinnedRepo } from "../../Juno/JunoClient"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; +import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory"; +import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; +import { useNotebook } from "../Notebook/useNotebook"; +import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; +import { useDatabases } from "../useDatabases"; +import { useSelectedNode } from "../useSelectedNode"; import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; @@ -48,30 +56,20 @@ export class ResourceTreeAdapter implements ReactAdapter { public myNotebooksContentRoot: NotebookContentItem; public gitHubNotebooksContentRoot: NotebookContentItem; - private koSubsDatabaseIdMap: ArrayHashMap; // database id -> ko subs - private koSubsCollectionIdMap: ArrayHashMap; // collection id -> ko subs - private databaseCollectionIdMap: ArrayHashMap; // database id -> collection ids - public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); - this.container.selectedNode.subscribe((newValue: any) => this.triggerRender()); - this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender()); - this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender()); + useSelectedNode.subscribe(() => this.triggerRender()); + useTabs.subscribe( + () => this.triggerRender(), + (state) => state.activeTab + ); + useNotebook.subscribe( + () => this.triggerRender(), + (state) => state.isNotebookEnabled + ); - this.koSubsDatabaseIdMap = new ArrayHashMap(); - this.koSubsCollectionIdMap = new ArrayHashMap(); - this.databaseCollectionIdMap = new ArrayHashMap(); - - this.container.databases.subscribe((databases: ViewModels.Database[]) => { - // Clean up old databases - this.cleanupDatabasesKoSubs(); - - databases.forEach((database: ViewModels.Database) => this.watchDatabase(database)); - this.triggerRender(); - }); - - this.container.databases().forEach((database: ViewModels.Database) => this.watchDatabase(database)); + useDatabases.subscribe(() => this.triggerRender()); this.triggerRender(); } @@ -103,7 +101,7 @@ export class ResourceTreeAdapter implements ReactAdapter { const dataRootNode = this.buildDataTree(); const notebooksRootNode = this.buildNotebooksTrees(); - if (this.container.isNotebookEnabled()) { + if (useNotebook.getState().isNotebookEnabled) { return ( <> @@ -134,12 +132,12 @@ export class ResourceTreeAdapter implements ReactAdapter { this.myNotebooksContentRoot = { name: ResourceTreeAdapter.MyNotebooksTitle, - path: this.container.getNotebookBasePath(), + path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; // Only if notebook server is available we can refresh - if (this.container.notebookServerInfo().notebookServerEndpoint) { + if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) { refreshTasks.push( this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => { this.triggerRender(); @@ -189,14 +187,14 @@ export class ResourceTreeAdapter implements ReactAdapter { } private buildDataTree(): TreeNode { - const databaseTreeNodes: TreeNode[] = this.container.databases().map((database: ViewModels.Database) => { + const databaseTreeNodes: TreeNode[] = useDatabases.getState().databases.map((database: ViewModels.Database) => { const databaseNode: TreeNode = { label: database.id(), iconSrc: CosmosDBIcon, isExpanded: false, className: "databaseHeader", children: [], - isSelected: () => this.isDataNodeSelected(database.id()), + isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()), onClick: async (isExpanded) => { // Rewritten version of expandCollapseDatabase(): @@ -209,18 +207,20 @@ export class ResourceTreeAdapter implements ReactAdapter { await database.expandDatabase(); } databaseNode.isLoading = false; - database.selectDatabase(); - this.container.onUpdateTabsButtons([]); - this.container.tabsManager.refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); + useSelectedNode.getState().setSelectedNode(database); + useCommandBar.getState().setContextButtons([]); + useTabs.getState().refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); }, - onContextMenuOpen: () => this.container.selectedNode(database), + onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database), }; if (database.isDatabaseShared()) { databaseNode.children.push({ label: "Scale", isSelected: () => - this.isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettings]), + useSelectedNode + .getState() + .isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettings]), onClick: database.onSettingsClick.bind(database), }); } @@ -266,34 +266,38 @@ export class ResourceTreeAdapter implements ReactAdapter { mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); }, isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.Documents, - ViewModels.CollectionTabKind.Graph, - ]), + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.Documents, + ViewModels.CollectionTabKind.Graph, + ]), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), }); if ( + useNotebook.getState().isNotebookEnabled && userContext.apiType === "Mongo" && - this.container.isNotebookEnabled() && - userContext.features.enableSchemaAnalyzer + isPublicInternetAccessAllowed() ) { children.push({ label: "Schema (Preview)", onClick: collection.onSchemaAnalyzerClick.bind(collection), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.SchemaAnalyzer, - ]), + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), }); } - if (userContext.apiType !== "Cassandra" || !this.container.isServerlessEnabled()) { + if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { children.push({ - label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", + label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings", onClick: collection.onSettingsClick.bind(collection), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]), + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]), }); } @@ -319,7 +323,9 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Conflicts", onClick: collection.onConflictsClick.bind(collection), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]), + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]), }); } @@ -332,12 +338,14 @@ export class ResourceTreeAdapter implements ReactAdapter { contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), onClick: () => { // Rewritten version of expandCollapseCollection - this.container.selectedNode(collection); - this.container.onUpdateTabsButtons([]); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useSelectedNode.getState().setSelectedNode(collection); + useCommandBar.getState().setContextButtons([]); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, onExpanded: () => { if (ResourceTreeAdapter.showScriptNodes(this.container)) { @@ -346,8 +354,8 @@ export class ResourceTreeAdapter implements ReactAdapter { collection.loadTriggers(); } }, - isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id()), - onContextMenuOpen: () => this.container.selectedNode(collection), + isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), + onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), }; } @@ -358,17 +366,21 @@ export class ResourceTreeAdapter implements ReactAdapter { label: sp.id(), onClick: sp.open.bind(sp), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.StoredProcedures, - ]), + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.StoredProcedures, + ]), contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp), })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, }; } @@ -380,9 +392,11 @@ export class ResourceTreeAdapter implements ReactAdapter { label: udf.id(), onClick: udf.open.bind(udf), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ - ViewModels.CollectionTabKind.UserDefinedFunctions, - ]), + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ + ViewModels.CollectionTabKind.UserDefinedFunctions, + ]), contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems( this.container, udf @@ -390,10 +404,12 @@ export class ResourceTreeAdapter implements ReactAdapter { })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, }; } @@ -405,15 +421,19 @@ export class ResourceTreeAdapter implements ReactAdapter { label: trigger.id(), onClick: trigger.open.bind(trigger), isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]), + useSelectedNode + .getState() + .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]), contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger), })), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => - tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); + useTabs + .getState() + .refreshActiveTab( + (tab: TabsBase) => + tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId + ); }, }; } @@ -432,9 +452,7 @@ export class ResourceTreeAdapter implements ReactAdapter { children: this.getSchemaNodes(collection.schema.fields), onClick: () => { collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema); - this.container.tabsManager.refreshActiveTab( - (tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid - ); + useTabs.getState().refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid); }, }; } @@ -564,7 +582,7 @@ export class ResourceTreeAdapter implements ReactAdapter { className: "notebookHeader galleryHeader", onClick: () => this.container.openGallery(), isSelected: () => { - const activeTab = this.container.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; }, }; @@ -608,7 +626,17 @@ export class ResourceTreeAdapter implements ReactAdapter { gitHubNotebooksTree.contextMenu = [ { label: "Manage GitHub settings", - onClick: () => this.container.openGitHubReposPanel("Manage GitHub settings"), + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Manage GitHub settings", + + ), }, { label: "Disconnect from GitHub", @@ -658,7 +686,7 @@ export class ResourceTreeAdapter implements ReactAdapter { className: "notebookHeader", onClick: () => onFileClick(item), isSelected: () => { - const activeTab = this.container.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; return ( activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && @@ -813,7 +841,7 @@ export class ResourceTreeAdapter implements ReactAdapter { } }, isSelected: () => { - const activeTab = this.container.tabsManager.activeTab(); + const activeTab = useTabs.getState().activeTab; return ( activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && @@ -835,133 +863,4 @@ export class ResourceTreeAdapter implements ReactAdapter { public triggerRender() { window.requestAnimationFrame(() => this.parameters(Date.now())); } - - /** - * public for testing purposes - * @param databaseId - * @param collectionId - * @param subnodeKinds - */ - public isDataNodeSelected( - databaseId: string, - collectionId?: string, - subnodeKinds?: ViewModels.CollectionTabKind[] - ): boolean { - if (!this.container.selectedNode || !this.container.selectedNode()) { - return false; - } - const selectedNode = this.container.selectedNode(); - const isNodeSelected = collectionId - ? (selectedNode as ViewModels.Collection).databaseId === databaseId && selectedNode.id() === collectionId - : selectedNode.id() === databaseId; - - if (!isNodeSelected) { - return false; - } - - if (subnodeKinds === undefined || !Array.isArray(subnodeKinds)) { - return true; - } - - const activeTab = this.container.tabsManager.activeTab(); - const selectedSubnodeKind = collectionId - ? (selectedNode as ViewModels.Collection).selectedSubnodeKind() - : (selectedNode as ViewModels.Database).selectedSubnodeKind(); - - return ( - activeTab && - subnodeKinds.includes(activeTab.tabKind) && - selectedSubnodeKind !== undefined && - subnodeKinds.includes(selectedSubnodeKind) - ); - } - - // *************** watch all nested ko's inside database - // TODO Simplify so we don't have to do this - - private watchCollection(databaseId: string, collection: ViewModels.Collection) { - this.addKoSubToCollectionId( - databaseId, - collection.id(), - collection.storedProcedures.subscribe(() => { - this.triggerRender(); - }) - ); - - this.addKoSubToCollectionId( - databaseId, - collection.id(), - collection.isCollectionExpanded.subscribe(() => { - this.triggerRender(); - }) - ); - - this.addKoSubToCollectionId( - databaseId, - collection.id(), - collection.isStoredProceduresExpanded.subscribe(() => { - this.triggerRender(); - }) - ); - } - - private watchDatabase(database: ViewModels.Database) { - const databaseId = database.id(); - const koSub = database.collections.subscribe((collections: ViewModels.Collection[]) => { - this.cleanupCollectionsKoSubs( - databaseId, - collections.map((collection: ViewModels.Collection) => collection.id()) - ); - - collections.forEach((collection: ViewModels.Collection) => this.watchCollection(databaseId, collection)); - this.triggerRender(); - }); - this.addKoSubToDatabaseId(databaseId, koSub); - - database.collections().forEach((collection: ViewModels.Collection) => this.watchCollection(databaseId, collection)); - } - - private addKoSubToDatabaseId(databaseId: string, sub: ko.Subscription): void { - this.koSubsDatabaseIdMap.push(databaseId, sub); - } - - private addKoSubToCollectionId(databaseId: string, collectionId: string, sub: ko.Subscription): void { - this.databaseCollectionIdMap.push(databaseId, collectionId); - this.koSubsCollectionIdMap.push(collectionId, sub); - } - - private cleanupDatabasesKoSubs(): void { - for (const databaseId of this.koSubsDatabaseIdMap.keys()) { - this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose()); - this.koSubsDatabaseIdMap.delete(databaseId); - - if (this.databaseCollectionIdMap.has(databaseId)) { - this.databaseCollectionIdMap - .get(databaseId) - .forEach((collectionId: string) => this.cleanupKoSubsForCollection(databaseId, collectionId)); - } - } - } - - private cleanupCollectionsKoSubs(databaseId: string, existingCollectionIds: string[]): void { - if (!this.databaseCollectionIdMap.has(databaseId)) { - return; - } - - const collectionIdsToRemove = this.databaseCollectionIdMap - .get(databaseId) - .filter((id: string) => existingCollectionIds.indexOf(id) === -1); - - collectionIdsToRemove.forEach((id: string) => this.cleanupKoSubsForCollection(databaseId, id)); - } - - private cleanupKoSubsForCollection(databaseId: string, collectionId: string) { - if (!this.koSubsCollectionIdMap.has(collectionId)) { - return; - } - - this.koSubsCollectionIdMap.get(collectionId).forEach((sub: ko.Subscription) => sub.dispose()); - this.koSubsCollectionIdMap.delete(collectionId); - this.databaseCollectionIdMap.remove(databaseId, collectionId); - } } diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx deleted file mode 100644 index 9398747fa..000000000 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as ko from "knockout"; -import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import React from "react"; -import ResourceTokenCollection from "./ResourceTokenCollection"; -import { ResourceTreeAdapterForResourceToken } from "./ResourceTreeAdapterForResourceToken"; -import { shallow } from "enzyme"; -import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent"; -import Explorer from "../Explorer"; - -const createMockContainer = (): Explorer => { - let mockContainer = {} as Explorer; - mockContainer.resourceTokenCollection = createMockCollection(mockContainer); - mockContainer.selectedNode = ko.observable(); - mockContainer.onUpdateTabsButtons = () => {}; - - return mockContainer; -}; - -const createMockCollection = (container: Explorer): ko.Observable => { - let mockCollection = {} as DataModels.Collection; - mockCollection._rid = "fakeRid"; - mockCollection._self = "fakeSelf"; - mockCollection.id = "fakeId"; - - const mockResourceTokenCollection: ViewModels.CollectionBase = new ResourceTokenCollection( - container, - "fakeDatabaseId", - mockCollection - ); - return ko.observable(mockResourceTokenCollection); -}; - -describe("Resource tree for resource token", () => { - const mockContainer: Explorer = createMockContainer(); - const resourceTree = new ResourceTreeAdapterForResourceToken(mockContainer); - - it("should render", () => { - const rootNode: TreeNode = resourceTree.buildCollectionNode(); - const props: TreeComponentProps = { - rootNode, - className: "dataResourceTree", - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx b/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx deleted file mode 100644 index 2c106e6f9..000000000 --- a/src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as ko from "knockout"; -import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import * as React from "react"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { NotebookContentItem } from "../Notebook/NotebookContentItem"; -import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; -import CollectionIcon from "../../../images/tree-collection.svg"; -import Explorer from "../Explorer"; -import { userContext } from "../../UserContext"; - -export class ResourceTreeAdapterForResourceToken implements ReactAdapter { - public parameters: ko.Observable; - public myNotebooksContentRoot: NotebookContentItem; - - public constructor(private container: Explorer) { - this.parameters = ko.observable(Date.now()); - - this.container.resourceTokenCollection.subscribe(() => this.triggerRender()); - this.container.selectedNode.subscribe((newValue: any) => this.triggerRender()); - this.container.tabsManager && this.container.tabsManager.activeTab.subscribe(() => this.triggerRender()); - - this.triggerRender(); - } - - public renderComponent(): JSX.Element { - const dataRootNode = this.buildCollectionNode(); - return ; - } - - public buildCollectionNode(): TreeNode { - const collection: ViewModels.CollectionBase = this.container.resourceTokenCollection(); - if (!collection) { - return { - label: undefined, - isExpanded: true, - children: [], - }; - } - - const children: TreeNode[] = []; - children.push({ - label: "Items", - onClick: () => { - collection.onDocumentDBDocumentsClick(); - // push to most recent - mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); - }, - isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents), - }); - - const collectionNode: TreeNode = { - label: collection.id(), - iconSrc: CollectionIcon, - isExpanded: true, - children, - className: "collectionHeader", - onClick: () => { - // Rewritten version of expandCollapseCollection - this.container.selectedNode(collection); - this.container.onUpdateTabsButtons([]); - this.container.tabsManager.refreshActiveTab( - (tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId - ); - }, - isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id()), - }; - - return { - label: undefined, - isExpanded: true, - children: [collectionNode], - }; - } - - public isDataNodeSelected( - databaseId: string, - collectionId?: string, - subnodeKind?: ViewModels.CollectionTabKind - ): boolean { - if (!this.container.selectedNode || !this.container.selectedNode()) { - return false; - } - const selectedNode = this.container.selectedNode(); - const isNodeSelected = collectionId - ? (selectedNode as ViewModels.Collection).databaseId === databaseId && selectedNode.id() === collectionId - : selectedNode.id() === databaseId; - - if (!isNodeSelected) { - return false; - } - - if (!subnodeKind) { - return true; - } - - const activeTab = this.container.tabsManager.activeTab(); - const selectedSubnodeKind = collectionId - ? (selectedNode as ViewModels.Collection).selectedSubnodeKind() - : (selectedNode as ViewModels.Database).selectedSubnodeKind(); - - return activeTab && activeTab.tabKind === subnodeKind && selectedSubnodeKind === subnodeKind; - } - - public triggerRender() { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index e85a39391..8e6f77ece 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -3,14 +3,16 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import Explorer from "../Explorer"; -import StoredProcedureTab from "../Tabs/StoredProcedureTab"; +import { getErrorMessage } from "../Tables/Utilities"; +import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab"; import TabsBase from "../Tabs/TabsBase"; +import { useSelectedNode } from "../useSelectedNode"; const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE function sample(prefix) { @@ -61,29 +63,33 @@ export default class StoredProcedure { } public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1; + const id = useTabs.getState().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`, - onUpdateTabsButtons: source.container.onUpdateTabsButtons, - }); + const storedProcedureTab: NewStoredProcedureTab = new NewStoredProcedureTab( + { + 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, + }, + { + collection: source, + container: source.container, + } + ); - source.container.tabsManager.activateNewTab(storedProcedureTab); + useTabs.getState().activateNewTab(storedProcedureTab); } public select() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Stored procedure node", @@ -94,14 +100,16 @@ export default class StoredProcedure { 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]; + const storedProcedureTabs: NewStoredProcedureTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + (tab: TabsBase) => tab.node && tab.node.rid === this.rid + ) as NewStoredProcedureTab[]; + let storedProcedureTab: NewStoredProcedureTab = storedProcedureTabs && storedProcedureTabs[0]; if (storedProcedureTab) { - this.container.tabsManager.activateTab(storedProcedureTab); + useTabs.getState().activateTab(storedProcedureTab); } else { const storedProcedureData = { _rid: this.rid, @@ -110,25 +118,25 @@ export default class StoredProcedure { 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()}`, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, - }); + storedProcedureTab = new NewStoredProcedureTab( + { + 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, + }, + { + collection: this.collection, + container: this.container, + } + ); - this.container.tabsManager.activateNewTab(storedProcedureTab); + useTabs.getState().activateNewTab(storedProcedureTab); } }; - public delete() { if (!window.confirm("Are you sure you want to delete the stored procedure?")) { return; @@ -136,7 +144,7 @@ export default class StoredProcedure { deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( () => { - this.container.tabsManager.closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); + useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); this.collection.children.remove(this); }, (reason) => {} @@ -144,19 +152,21 @@ export default class StoredProcedure { } 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]; + const sprocTabs: NewStoredProcedureTab[] = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + (tab: TabsBase) => tab.node && tab.node.rid === this.rid + ) as NewStoredProcedureTab[]; + const sprocTab: NewStoredProcedureTab = 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); + (result) => { + sprocTab.onExecuteSprocsResult(result); }, - (error: any) => { + (error) => { sprocTab.onExecuteSprocsError(getErrorMessage(error)); } ) diff --git a/src/Explorer/Tree/Trigger.ts b/src/Explorer/Tree/Trigger.ts index 5f6e0b877..d90428b65 100644 --- a/src/Explorer/Tree/Trigger.ts +++ b/src/Explorer/Tree/Trigger.ts @@ -3,10 +3,12 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import { deleteTrigger } from "../../Common/dataAccess/deleteTrigger"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import Explorer from "../Explorer"; import TriggerTab from "../Tabs/TriggerTab"; +import { useSelectedNode } from "../useSelectedNode"; export default class Trigger { public nodeKind: string; @@ -32,7 +34,7 @@ export default class Trigger { } public select() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "Trigger node", @@ -41,7 +43,7 @@ export default class Trigger { } public static create(source: ViewModels.Collection, event: MouseEvent) { - const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; const trigger = { id: "", body: "function trigger(){}", @@ -57,24 +59,21 @@ export default class Trigger { tabPath: "", collection: source, node: source, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`, - onUpdateTabsButtons: source.container.onUpdateTabsButtons, }); - source.container.tabsManager.activateNewTab(triggerTab); + useTabs.getState().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[]; + const triggerTabs: TriggerTab[] = useTabs + .getState() + .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); + useTabs.getState().activateTab(triggerTab); } else { const triggerData = { _rid: this.rid, @@ -93,14 +92,9 @@ export default class Trigger { tabPath: "", collection: this.collection, node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( - this.collection.databaseId, - this.collection.id() - )}/triggers/${this.id()}`, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(triggerTab); + useTabs.getState().activateNewTab(triggerTab); } }; @@ -111,7 +105,7 @@ export default class Trigger { deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( () => { - this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + useTabs.getState().closeTabsByComparator((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 270afe34f..4264c0954 100644 --- a/src/Explorer/Tree/UserDefinedFunction.ts +++ b/src/Explorer/Tree/UserDefinedFunction.ts @@ -3,10 +3,12 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import { deleteUserDefinedFunction } from "../../Common/dataAccess/deleteUserDefinedFunction"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import Explorer from "../Explorer"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; +import { useSelectedNode } from "../useSelectedNode"; export default class UserDefinedFunction { public nodeKind: string; @@ -28,8 +30,8 @@ export default class UserDefinedFunction { 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; + public static create(source: ViewModels.Collection) { + const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1; const userDefinedFunction = { id: "", body: "function userDefinedFunction(){}", @@ -43,24 +45,24 @@ export default class UserDefinedFunction { tabPath: "", collection: source, node: source, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`, - onUpdateTabsButtons: source.container.onUpdateTabsButtons, }); - source.container.tabsManager.activateNewTab(userDefinedFunctionTab); + useTabs.getState().activateNewTab(userDefinedFunctionTab); } public open = () => { this.select(); - const userDefinedFunctionTabs: UserDefinedFunctionTab[] = this.container.tabsManager.getTabs( - ViewModels.CollectionTabKind.UserDefinedFunctions, - (tab) => tab.node?.rid === this.rid - ) as UserDefinedFunctionTab[]; + const userDefinedFunctionTabs: UserDefinedFunctionTab[] = useTabs + .getState() + .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); + useTabs.getState().activateTab(userDefinedFunctionTab); } else { const userDefinedFunctionData = { _rid: this.rid, @@ -77,19 +79,14 @@ export default class UserDefinedFunction { tabPath: "", collection: this.collection, node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds( - this.collection.databaseId, - this.collection.id() - )}/udfs/${this.id()}`, - onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); - this.container.tabsManager.activateNewTab(userDefinedFunctionTab); + useTabs.getState().activateNewTab(userDefinedFunctionTab); } }; public select() { - this.container.selectedNode(this); + useSelectedNode.getState().setSelectedNode(this); TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { description: "UDF item node", @@ -104,10 +101,12 @@ export default class UserDefinedFunction { deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( () => { - this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); this.collection.children.remove(this); }, - (reason) => {} + () => { + /**/ + } ); } } diff --git a/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap b/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap index e769c8764..dbb9ba112 100644 --- a/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap +++ b/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap @@ -3,6 +3,7 @@ exports[`Resource tree for schema should render 1`] = `
- -
-`; diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts new file mode 100644 index 000000000..1f9886a7d --- /dev/null +++ b/src/Explorer/useDatabases.ts @@ -0,0 +1,148 @@ +import _ from "underscore"; +import create, { UseStore } from "zustand"; +import * as Constants from "../Common/Constants"; +import * as ViewModels from "../Contracts/ViewModels"; +import { userContext } from "../UserContext"; +import { useSelectedNode } from "./useSelectedNode"; + +interface DatabasesState { + databases: ViewModels.Database[]; + resourceTokenCollection: ViewModels.CollectionBase; + updateDatabase: (database: ViewModels.Database) => void; + addDatabases: (databases: ViewModels.Database[]) => void; + deleteDatabase: (database: ViewModels.Database) => void; + clearDatabases: () => void; + isSaveQueryEnabled: () => boolean; + findDatabaseWithId: (databaseId: string) => ViewModels.Database; + isLastNonEmptyDatabase: () => boolean; + findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection; + isLastCollection: () => boolean; + loadDatabaseOffers: () => Promise; + isFirstResourceCreated: () => boolean; + findSelectedDatabase: () => ViewModels.Database; + validateDatabaseId: (id: string) => boolean; + validateCollectionId: (databaseId: string, collectionId: string) => Promise; +} + +export const useDatabases: UseStore = create((set, get) => ({ + databases: [], + resourceTokenCollection: undefined, + updateDatabase: (updatedDatabase: ViewModels.Database) => + set((state) => { + const updatedDatabases = state.databases.map((database: ViewModels.Database) => { + if (database.id() === updatedDatabase.id()) { + return updatedDatabase; + } + + return database; + }); + return { databases: updatedDatabases }; + }), + addDatabases: (databases: ViewModels.Database[]) => + set((state) => ({ + databases: [...state.databases, ...databases].sort((db1, db2) => db1.id().localeCompare(db2.id())), + })), + deleteDatabase: (database: ViewModels.Database) => + set((state) => ({ databases: state.databases.filter((db) => database.id() !== db.id()) })), + clearDatabases: () => set(() => ({ databases: [] })), + isSaveQueryEnabled: () => { + const savedQueriesDatabase: ViewModels.Database = _.find( + get().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; + }, + findDatabaseWithId: (databaseId: string) => get().databases.find((db) => databaseId === db.id()), + isLastNonEmptyDatabase: () => { + const databases = get().databases; + return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer()); + }, + findCollection: (databaseId: string, collectionId: string) => { + const database = get().findDatabaseWithId(databaseId); + return database?.collections()?.find((collection) => collection.id() === collectionId); + }, + isLastCollection: () => { + const databases = get().databases; + if (databases.length === 0) { + return false; + } + + let collectionCount = 0; + for (let i = 0; i < databases.length; i++) { + const database = databases[i]; + collectionCount += database.collections().length; + if (collectionCount > 1) { + return false; + } + } + + return true; + }, + loadDatabaseOffers: async () => { + await Promise.all( + get().databases?.map(async (database: ViewModels.Database) => { + await database.loadOffer(); + }) + ); + }, + isFirstResourceCreated: () => { + const databases = get().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; + }); + }, + findSelectedDatabase: (): ViewModels.Database => { + const selectedNode = useSelectedNode.getState().selectedNode; + if (!selectedNode) { + return undefined; + } + if (selectedNode.nodeKind === "Database") { + return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id()); + } + + if (selectedNode.nodeKind === "Collection") { + return selectedNode.database; + } + + return selectedNode.collection?.database; + }, + validateDatabaseId: (id: string): boolean => { + return !get().databases.some((database) => database.id() === id); + }, + validateCollectionId: async (databaseId: string, collectionId: string): Promise => { + const database = get().databases.find((db) => db.id() === databaseId); + // For a new tables account, database is undefined when creating the first table + if (!database && userContext.apiType === "Tables") { + return true; + } + + await database.loadCollections(); + return !database.collections().some((collection) => collection.id() === collectionId); + }, +})); diff --git a/src/Explorer/useSelectedNode.ts b/src/Explorer/useSelectedNode.ts new file mode 100644 index 000000000..15f953641 --- /dev/null +++ b/src/Explorer/useSelectedNode.ts @@ -0,0 +1,62 @@ +import create, { UseStore } from "zustand"; +import * as ViewModels from "../Contracts/ViewModels"; +import { useTabs } from "../hooks/useTabs"; + +export interface SelectedNodeState { + selectedNode: ViewModels.TreeNode; + setSelectedNode: (node: ViewModels.TreeNode) => void; + isDatabaseNodeOrNoneSelected: () => boolean; + findSelectedCollection: () => ViewModels.Collection; + isDataNodeSelected: ( + databaseId: string, + collectionId?: string, + subnodeKinds?: ViewModels.CollectionTabKind[] + ) => boolean; +} + +export const useSelectedNode: UseStore = create((set, get) => ({ + selectedNode: undefined, + setSelectedNode: (node: ViewModels.TreeNode) => set({ selectedNode: node }), + isDatabaseNodeOrNoneSelected: (): boolean => { + const selectedNode = get().selectedNode; + return !selectedNode || selectedNode.nodeKind === "Database"; + }, + findSelectedCollection: (): ViewModels.Collection => { + const selectedNode = get().selectedNode; + return (selectedNode.nodeKind === "Collection" ? selectedNode : selectedNode.collection) as ViewModels.Collection; + }, + isDataNodeSelected: ( + databaseId: string, + collectionId?: string, + subnodeKinds?: ViewModels.CollectionTabKind[] + ): boolean => { + const selectedNode = get().selectedNode; + if (!selectedNode) { + return false; + } + + const isNodeSelected = collectionId + ? (selectedNode as ViewModels.Collection).databaseId === databaseId && selectedNode.id() === collectionId + : selectedNode.id() === databaseId; + + if (!isNodeSelected) { + return false; + } + + if (subnodeKinds === undefined || !Array.isArray(subnodeKinds)) { + return true; + } + + const activeTab = useTabs.getState().activeTab; + const selectedSubnodeKind = collectionId + ? (selectedNode as ViewModels.Collection).selectedSubnodeKind() + : (selectedNode as ViewModels.Database).selectedSubnodeKind(); + + return ( + activeTab && + subnodeKinds.includes(activeTab.tabKind) && + selectedSubnodeKind !== undefined && + subnodeKinds.includes(selectedSubnodeKind) + ); + }, +})); diff --git a/src/GalleryViewer/GalleryViewer.tsx b/src/GalleryViewer/GalleryViewer.tsx index 07fcb733e..e630ee7b2 100644 --- a/src/GalleryViewer/GalleryViewer.tsx +++ b/src/GalleryViewer/GalleryViewer.tsx @@ -42,10 +42,10 @@ const onInit = async () => { practices, and how to get started with Azure Cosmos DB. - If you'd like to run or edit the notebook in your own Azure Cosmos DB account,{" "} + If {`you'd`} like to run or edit the notebook in your own Azure Cosmos DB account,{" "} sign in and select an account with{" "} notebooks enabled. From there, you can download the sample to your - account. If you don't have an account yet, you can{" "} + account. If you {`don't`} have an account yet, you can{" "} create one from the Azure portal.
diff --git a/src/GitHub/GitHubConnector.ts b/src/GitHub/GitHubConnector.ts index 5c9cbff37..e5c95a425 100644 --- a/src/GitHub/GitHubConnector.ts +++ b/src/GitHub/GitHubConnector.ts @@ -7,10 +7,12 @@ export interface IGitHubConnectorParams { export const GitHubConnectorMsgType = "GitHubConnectorMsgType"; -export class GitHubConnector { - public async start(params: URLSearchParams, window: Window & typeof globalThis): Promise { +window.addEventListener("load", async () => { + const openerWindow = window.opener; + if (openerWindow) { + const params = new URLSearchParams(document.location.search); await postRobot.send( - window, + openerWindow, GitHubConnectorMsgType, { state: params.get("state"), @@ -20,14 +22,6 @@ export class GitHubConnector { domain: window.location.origin, } ); - } -} - -var connector = new GitHubConnector(); -window.addEventListener("load", async () => { - const openerWindow = window.opener; - if (openerWindow) { - await 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 c7311bbf6..129863df3 100644 --- a/src/GitHub/GitHubContentProvider.test.ts +++ b/src/GitHub/GitHubContentProvider.test.ts @@ -1,11 +1,13 @@ import { IContent } from "@nteract/core"; import { fixture } from "@nteract/fixtures"; import { HttpStatusCodes } from "../Common/Constants"; +import * as GitHubUtils from "../Utils/GitHubUtils"; import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient"; import { GitHubContentProvider } from "./GitHubContentProvider"; -import * as GitHubUtils from "../Utils/GitHubUtils"; -const gitHubClient = new GitHubClient(() => {}); +const gitHubClient = new GitHubClient(() => { + /**/ +}); const gitHubContentProvider = new GitHubContentProvider({ gitHubClient, promptForCommitMsg: () => Promise.resolve("commit msg"), @@ -46,7 +48,7 @@ const sampleNotebookModel: IContent<"notebook"> = { created: "", last_modified: "date", mimetype: "application/x-ipynb+json", - content: sampleFile.content ? JSON.parse(sampleFile.content) : null, + content: sampleFile.content ? JSON.parse(sampleFile.content) : undefined, format: "json", }; @@ -54,7 +56,7 @@ describe("GitHubContentProvider remove", () => { it("errors on invalid path", async () => { spyOn(GitHubClient.prototype, "getContentsAsync"); - const response = await gitHubContentProvider.remove(null, "invalid path").toPromise(); + const response = await gitHubContentProvider.remove(undefined, "invalid path").toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(gitHubClient.getContentsAsync).not.toBeCalled(); @@ -63,7 +65,7 @@ describe("GitHubContentProvider remove", () => { it("errors on failed read", async () => { spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); - const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); + const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -75,7 +77,7 @@ describe("GitHubContentProvider remove", () => { ); spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 })); - const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); + const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -90,7 +92,7 @@ describe("GitHubContentProvider remove", () => { Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) ); - const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); + const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(HttpStatusCodes.NoContent); expect(gitHubClient.deleteFileAsync).toBeCalled(); @@ -102,7 +104,7 @@ describe("GitHubContentProvider get", () => { it("errors on invalid path", async () => { spyOn(GitHubClient.prototype, "getContentsAsync"); - const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise(); + const response = await gitHubContentProvider.get(undefined, "invalid path", undefined).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(gitHubClient.getContentsAsync).not.toBeCalled(); @@ -111,7 +113,7 @@ describe("GitHubContentProvider get", () => { 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(); + const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, undefined).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -122,7 +124,7 @@ describe("GitHubContentProvider get", () => { Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) ); - const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise(); + const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, {}).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(HttpStatusCodes.OK); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -134,7 +136,7 @@ describe("GitHubContentProvider update", () => { it("errors on invalid path", async () => { spyOn(GitHubClient.prototype, "getContentsAsync"); - const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise(); + const response = await gitHubContentProvider.update(undefined, "invalid path", undefined).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(gitHubClient.getContentsAsync).not.toBeCalled(); @@ -143,7 +145,7 @@ describe("GitHubContentProvider update", () => { 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(); + const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, undefined).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -155,7 +157,7 @@ describe("GitHubContentProvider update", () => { ); spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 })); - const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -170,7 +172,7 @@ describe("GitHubContentProvider update", () => { Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) ); - const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(HttpStatusCodes.OK); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -186,7 +188,7 @@ describe("GitHubContentProvider create", () => { it("errors on invalid path", async () => { spyOn(GitHubClient.prototype, "createOrUpdateFileAsync"); - const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise(); + const response = await gitHubContentProvider.create(undefined, "invalid path", sampleNotebookModel).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled(); @@ -195,7 +197,7 @@ describe("GitHubContentProvider create", () => { 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(); + const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); @@ -206,7 +208,7 @@ describe("GitHubContentProvider create", () => { Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit }) ); - const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(HttpStatusCodes.Created); expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); @@ -221,7 +223,7 @@ describe("GitHubContentProvider save", () => { it("errors on invalid path", async () => { spyOn(GitHubClient.prototype, "getContentsAsync"); - const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise(); + const response = await gitHubContentProvider.save(undefined, "invalid path", undefined).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(gitHubClient.getContentsAsync).not.toBeCalled(); @@ -230,7 +232,7 @@ describe("GitHubContentProvider save", () => { 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(); + const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, undefined).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -242,7 +244,7 @@ describe("GitHubContentProvider save", () => { ); spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 })); - const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(888); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -257,7 +259,7 @@ describe("GitHubContentProvider save", () => { Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) ); - const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); + const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(HttpStatusCodes.OK); expect(gitHubClient.getContentsAsync).toBeCalled(); @@ -271,7 +273,7 @@ describe("GitHubContentProvider save", () => { describe("GitHubContentProvider listCheckpoints", () => { it("errors for everything", async () => { - const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise(); + const response = await gitHubContentProvider.listCheckpoints().toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); }); @@ -279,7 +281,7 @@ describe("GitHubContentProvider listCheckpoints", () => { describe("GitHubContentProvider createCheckpoint", () => { it("errors for everything", async () => { - const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise(); + const response = await gitHubContentProvider.createCheckpoint().toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); }); @@ -287,7 +289,7 @@ describe("GitHubContentProvider createCheckpoint", () => { describe("GitHubContentProvider deleteCheckpoint", () => { it("errors for everything", async () => { - const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise(); + const response = await gitHubContentProvider.deleteCheckpoint().toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); }); @@ -295,7 +297,7 @@ describe("GitHubContentProvider deleteCheckpoint", () => { describe("GitHubContentProvider restoreFromCheckpoint", () => { it("errors for everything", async () => { - const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise(); + const response = await gitHubContentProvider.restoreFromCheckpoint().toPromise(); expect(response).toBeDefined(); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); }); diff --git a/src/GitHub/GitHubContentProvider.ts b/src/GitHub/GitHubContentProvider.ts index ac45ce9de..57c4d0a7c 100644 --- a/src/GitHub/GitHubContentProvider.ts +++ b/src/GitHub/GitHubContentProvider.ts @@ -1,15 +1,15 @@ -import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable"; +import { makeNotebookRecord, Notebook, stringifyNotebook, 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 * as UrlUtility from "../Common/UrlUtility"; import { getErrorMessage } from "../Common/ErrorHandlingUtils"; +import * as Logger from "../Common/Logger"; +import * as UrlUtility from "../Common/UrlUtility"; +import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; +import * as Base64Utils from "../Utils/Base64Utils"; +import * as GitHubUtils from "../Utils/GitHubUtils"; +import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient"; export interface GitHubContentProviderParams { gitHubClient: GitHubClient; @@ -267,25 +267,25 @@ export class GitHubContentProvider implements IContentProvider { ); } - public listCheckpoints(_: ServerConfig, path: string): Observable { + public listCheckpoints(): 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 { + public createCheckpoint(): 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 { + public deleteCheckpoint(): 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 { + public restoreFromCheckpoint(): Observable { const error = new GitHubContentProviderError("Not implemented"); Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno); return of(this.createErrorAjaxResponse(error)); diff --git a/src/GitHub/GitHubOAuthService.test.ts b/src/GitHub/GitHubOAuthService.test.ts index b043f958a..79030271e 100644 --- a/src/GitHub/GitHubOAuthService.test.ts +++ b/src/GitHub/GitHubOAuthService.test.ts @@ -1,6 +1,5 @@ import { HttpStatusCodes } from "../Common/Constants"; import Explorer from "../Explorer/Explorer"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import NotebookManager from "../Explorer/Notebook/NotebookManager"; import { JunoClient } from "../Juno/JunoClient"; import { IGitHubConnectorParams } from "./GitHubConnector"; @@ -17,8 +16,6 @@ describe("GitHubOAuthService", () => { 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; diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 9f73ecdc4..04abb1dd5 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -1,24 +1,25 @@ -import { useBoolean } from "@fluentui/react-hooks"; import { initializeIcons } from "@fluentui/react"; +import { useBoolean } from "@fluentui/react-hooks"; 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 { useConfig } from "./hooks/useConfig"; +import { useTokenMetadata } from "./hooks/usePortalAccessToken"; import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; +import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; +import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; +import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel"; +import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; +import { MeControl } from "./Platform/Hosted/Components/MeControl"; +import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; +import "./Platform/Hosted/ConnectScreen.less"; import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; +import "./Shared/appInsights"; initializeIcons(); @@ -30,7 +31,7 @@ const App: React.FunctionComponent = () => { // For showing/hiding panel const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); - + const config = useConfig(); const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); const [databaseAccount, setDatabaseAccount] = React.useState(); const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); @@ -74,7 +75,7 @@ const App: React.FunctionComponent = () => { }); const showExplorer = - (isLoggedIn && databaseAccount) || + (config && isLoggedIn && databaseAccount) || (encryptedTokenMetadata && encryptedTokenMetadata) || (authType === AuthType.ResourceToken && connectionString); diff --git a/src/Index.ts b/src/Index.ts deleted file mode 100644 index 9eb33943c..000000000 --- a/src/Index.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/Index.tsx b/src/Index.tsx new file mode 100644 index 000000000..d660eaed0 --- /dev/null +++ b/src/Index.tsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; +import ReactDOM from "react-dom"; +import Arrow from "../images/Arrow.svg"; +import CosmosDB_20170829 from "../images/CosmosDB_20170829.svg"; +import Explorer from "../images/Explorer.svg"; +import Feedback from "../images/Feedback.svg"; +import Quickstart from "../images/Quickstart.svg"; +import "../less/index.less"; + +const Index = (): JSX.Element => { + const [navigationSelection, setNavigationSelection] = useState("quickstart"); + + const quickstart_click = () => { + setNavigationSelection("quickstart"); + }; + + const explorer_click = () => { + setNavigationSelection("explorer"); + }; + + return ( + +
+
+ Azure Cosmos DB + + Create an Azure Cosmos DB account + + Azure Cosmos DB Emulator +
+
+ + + {navigationSelection === "quickstart" && ( + + )} + + {navigationSelection === "explorer" && ( + + )} +
+ ); +}; + +ReactDOM.render(, document.getElementById("root")); diff --git a/src/Localization/en/SqlX.json b/src/Localization/en/SqlX.json index 3c1d81dac..9c0ac667e 100644 --- a/src/Localization/en/SqlX.json +++ b/src/Localization/en/SqlX.json @@ -37,16 +37,20 @@ "CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment.", "DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint", "NoValue": "", - "SKUDetails": "SKU Details:", "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", - "CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory", - "Cost": "Cost", + "ApproximateCost": "Approximate Cost Per Hour", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "ConnectionString": "Connection String", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", - "KeysBlade": "the keys blade", + "KeysBlade": "the keys blade.", + "MetricsString": "Metrics", + "MetricsText": "Monitor the CPU and memory usage for the dedicated gateway instances in ", + "MetricsBlade": "the metrics blade.", + "MonitorUsage": "Monitor Usage", + "ResizingDecisionText": "To understand if the dedicated gateway is the right size, ", + "ResizingDecisionLink": "learn more about dedicated gateway sizing.", "WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.", "WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string." } \ No newline at end of file diff --git a/src/Main.tsx b/src/Main.tsx index c4c3be41c..80d008b17 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -26,30 +26,27 @@ import "../less/TableStyles/fulldatatables.less"; import "../less/TableStyles/queryBuilder.less"; import "../less/tree.less"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; -import { ResourceTree } from "./Common/ResourceTree"; +import { ResourceTreeContainer } from "./Common/ResourceTreeContainer"; import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; -import { Dialog, DialogProps } from "./Explorer/Controls/Dialog"; -import "./Explorer/Controls/DynamicList/DynamicListComponent.less"; +import { Dialog } from "./Explorer/Controls/Dialog"; import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; import "./Explorer/Controls/TreeComponent/treeComponent.less"; -import { ExplorerParams } from "./Explorer/Explorer"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; +import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; -import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import "./Explorer/Panes/PanelComponent.less"; -import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent"; +import { SidePanel } from "./Explorer/Panes/PanelContainerComponent"; import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen"; import "./Explorer/SplashScreen/SplashScreen.less"; -import "./Explorer/Tabs/QueryTab.less"; import { Tabs } from "./Explorer/Tabs/Tabs"; import { useConfig } from "./hooks/useConfig"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; -import { useSidePanel } from "./hooks/useSidePanel"; import { useTabs } from "./hooks/useTabs"; import "./Libs/jquery"; import "./Shared/appInsights"; @@ -57,39 +54,11 @@ import "./Shared/appInsights"; initializeIcons(); const App: React.FunctionComponent = () => { - const [isNotificationConsoleExpanded, setIsNotificationConsoleExpanded] = useState(false); - const [notificationConsoleData, setNotificationConsoleData] = useState(undefined); - //TODO: Refactor so we don't need to pass the id to remove a console data - const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState(""); const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState(true); - - const [dialogProps, setDialogProps] = useState(); - const [showDialog, setShowDialog] = useState(false); - - const openDialog = (props: DialogProps) => { - setDialogProps(props); - setShowDialog(true); - }; - const closeDialog = () => { - setShowDialog(false); - }; - - const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel(); - const { tabs, activeTab, tabsManager } = useTabs(); - - const explorerParams: ExplorerParams = { - setIsNotificationConsoleExpanded, - setNotificationConsoleData, - setInProgressConsoleDataIdToBeDeleted, - openSidePanel, - closeSidePanel, - openDialog, - closeDialog, - tabsManager, - }; + const openedTabs = useTabs((state) => state.openedTabs); const config = useConfig(); - const explorer = useKnockoutExplorer(config?.platform, explorerParams); + const explorer = useKnockoutExplorer(config?.platform); const toggleLeftPaneExpanded = () => { setIsLeftPaneExpanded(!isLeftPaneExpanded); @@ -106,16 +75,20 @@ const App: React.FunctionComponent = () => { return (
-
+
{/* Main Command Bar - Start */} -
+ {/* Collections Tree and Tabs - Begin */}
{/* Collections Tree - Start */}
{/* Collections Tree Expanded - Start */} - + {/* Collections Tree Expanded - End */} {/* Collections Tree Collapsed - Start */} { /> {/* Collections Tree Collapsed - End */}
- {/* Splitter - Start */} -
- {/* Splitter - End */}
{/* Collections Tree - End */} - {tabs.length === 0 && } - + {openedTabs.length === 0 && } +
{/* Collections Tree and Tabs - End */}
{ aria-label="Notification console" id="explorerNotificationConsole" > - +
- -
- {showDialog && } + +
); }; diff --git a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts b/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts deleted file mode 100644 index 171cb5cf4..000000000 --- a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts +++ /dev/null @@ -1,98 +0,0 @@ -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 deleted file mode 100644 index f6416fe1c..000000000 --- a/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts +++ /dev/null @@ -1,50 +0,0 @@ -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(): Promise { - throw new Error("Not yet implemented"); - } - - public async putAsync(): Promise { - throw new Error("Not yet implemented"); - } - - public async patchAsync(): Promise { - throw new Error("Not yet implemented"); - } -} - -export class NotebookWorkspaceResourceProviderClient implements IResourceProviderClient { - public async deleteAsync(): Promise { - throw new Error("Not yet implemented"); - } - - public async postAsync(): Promise { - throw new Error("Not yet implemented"); - } - - public async getAsync(): Promise { - throw new Error("Not yet implemented"); - } - - public async putAsync(): Promise { - throw new Error("Not yet implemented"); - } - - public async patchAsync(): Promise { - throw new Error("Not yet implemented"); - } -} diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index e7a4cbecc..3639bd113 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -1,9 +1,11 @@ -import * as React from "react"; import { useBoolean } from "@fluentui/react-hooks"; -import { HttpHeaders } from "../../../Common/Constants"; -import { GenerateTokenResponse } from "../../../Contracts/DataModels"; -import { configContext } from "../../../ConfigContext"; +import * as React from "react"; +import ErrorImage from "../../../../images/error.svg"; +import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import { AuthType } from "../../../AuthType"; +import { HttpHeaders } from "../../../Common/Constants"; +import { configContext } from "../../../ConfigContext"; +import { GenerateTokenResponse } from "../../../Contracts/DataModels"; import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; interface Props { @@ -28,7 +30,7 @@ export const ConnectExplorer: React.FunctionComponent = ({

- Azure Cosmos DB + Azure Cosmos DB

Welcome to Azure Cosmos DB

{isFormVisible ? ( @@ -68,7 +70,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ }} /> - Error notification + Error notification

diff --git a/src/Platform/Hosted/Components/MeControl.test.tsx b/src/Platform/Hosted/Components/MeControl.test.tsx index 4c5823e07..b2911d145 100644 --- a/src/Platform/Hosted/Components/MeControl.test.tsx +++ b/src/Platform/Hosted/Components/MeControl.test.tsx @@ -1,12 +1,12 @@ jest.mock("../../../hooks/useDirectories"); +import { AccountInfo } from "@azure/msal-browser"; import "@testing-library/jest-dom"; import { fireEvent, render, screen } from "@testing-library/react"; import React from "react"; import { MeControl } from "./MeControl"; -import { Account } from "msal"; it("renders", () => { - const account = {} as Account; + const account = {} as AccountInfo; const logout = jest.fn(); const openPanel = jest.fn(); diff --git a/src/Platform/Hosted/Components/MeControl.tsx b/src/Platform/Hosted/Components/MeControl.tsx index 4765256aa..4432d0226 100644 --- a/src/Platform/Hosted/Components/MeControl.tsx +++ b/src/Platform/Hosted/Components/MeControl.tsx @@ -1,11 +1,11 @@ -import { FocusZone, DefaultButton, DirectionalHint, Persona, PersonaInitialsColor, PersonaSize } from "@fluentui/react"; +import { AccountInfo } from "@azure/msal-browser"; +import { DefaultButton, DirectionalHint, FocusZone, Persona, PersonaInitialsColor, PersonaSize } from "@fluentui/react"; import * as React from "react"; -import { Account } from "msal"; import { useGraphPhoto } from "../../../hooks/useGraphPhoto"; interface Props { graphToken: string; - account: Account; + account: AccountInfo; openPanel: () => void; logout: () => void; } @@ -48,7 +48,7 @@ export const MeControl: React.FunctionComponent = ({ openPanel, logout, a = ({ data: account, }))} onChange={(_, option) => { - setSelectedAccountName(String(option.key)); + setSelectedAccountName(String(option?.key)); dismissMenu(); }} defaultSelectedKey={selectedAccount?.name} diff --git a/src/Platform/Hosted/Components/SwitchSubscription.tsx b/src/Platform/Hosted/Components/SwitchSubscription.tsx index 9ec98fc27..c784c5f5b 100644 --- a/src/Platform/Hosted/Components/SwitchSubscription.tsx +++ b/src/Platform/Hosted/Components/SwitchSubscription.tsx @@ -26,7 +26,7 @@ export const SwitchSubscription: FunctionComponent = ({ }; })} onChange={(_, option) => { - setSelectedSubscriptionId(String(option.key)); + setSelectedSubscriptionId(String(option?.key)); }} defaultSelectedKey={selectedSubscription?.subscriptionId} placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"} diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts index 8762171e7..bd1447891 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts @@ -2,8 +2,8 @@ import * as DataModels from "../../../Contracts/DataModels"; import { parseConnectionString } from "./ConnectionStringParser"; describe("ConnectionStringParser", () => { - const mockAccountName: string = "Test"; - const mockMasterKey: string = "some-key"; + const mockAccountName = "Test"; + const mockMasterKey = "some-key"; it("should parse a valid sql account connection string", () => { const metadata = parseConnectionString( diff --git a/src/Platform/Hosted/HostedUtils.ts b/src/Platform/Hosted/HostedUtils.ts index 8cc59daa4..f39e318b1 100644 --- a/src/Platform/Hosted/HostedUtils.ts +++ b/src/Platform/Hosted/HostedUtils.ts @@ -40,7 +40,7 @@ export function getDatabaseAccountKindFromExperience(apiExperience: typeof userC return AccountKind.GlobalDocumentDB; } -export function extractMasterKeyfromConnectionString(connectionString: string): string { +export function extractMasterKeyfromConnectionString(connectionString: string): string | undefined { // Only Gremlin uses the actual master key for connection to cosmos const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$"); return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 18ff57cc2..673d0c255 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -8,11 +8,14 @@ export type Features = { readonly enableReactPane: boolean; readonly enableRightPanelV2: boolean; readonly enableSchema: boolean; - enableSchemaAnalyzer: boolean; + autoscaleDefault: boolean; + partitionKeyDefault: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; readonly enableTtl: boolean; readonly executeSproc: boolean; + readonly enableAadDataPlane: boolean; + readonly enableKOResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -43,6 +46,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear return { canExceedMaximumValue: "true" === get("canexceedmaximumvalue"), cosmosdb: "true" === get("cosmosdb"), + enableAadDataPlane: "true" === get("enableaaddataplane"), enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"), enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"), enableKOPanel: "true" === get("enablekopanel"), @@ -50,10 +54,10 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableReactPane: "true" === get("enablereactpane"), enableRightPanelV2: "true" === get("enablerightpanelv2"), enableSchema: "true" === get("enableschema"), - enableSchemaAnalyzer: "true" === get("enableschemaanalyzer"), enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), + enableKOResourceTree: "true" === get("enablekoresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), junoEndpoint: get("junoendpoint"), @@ -66,5 +70,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear pr: get("pr"), showMinRUSurvey: "true" === get("showminrusurvey"), ttl90Days: "true" === get("ttl90days"), + autoscaleDefault: "true" === get("autoscaledefault"), + partitionKeyDefault: "true" === get("partitionkeytest"), }; } diff --git a/src/ResourceProvider/IResourceProviderClient.test.ts b/src/ResourceProvider/IResourceProviderClient.test.ts deleted file mode 100644 index b775564c1..000000000 --- a/src/ResourceProvider/IResourceProviderClient.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { IResourceProviderClient, IResourceProviderClientFactory } from "./IResourceProviderClient"; - -describe("IResourceProviderClient", () => { - interface TestResource { - id: string; - } - - const expectedResult: TestResource = { id: "a" }; - const expectedReason: any = "error"; - - class SuccessClient implements IResourceProviderClient { - public deleteAsync(url: string, apiVersion?: string): Promise { - return new Promise((resolve, reject) => { - resolve(); - }); - } - - public getAsync(url: string, apiVersion?: string): Promise { - return new Promise((resolve, reject) => { - resolve(expectedResult); - }); - } - - public postAsync(url: string, apiVersion: string, body: TestResource): Promise { - return new Promise((resolve, reject) => { - resolve(expectedResult); - }); - } - - public putAsync(url: string, apiVersion: string, body: TestResource): Promise { - return new Promise((resolve, reject) => { - resolve(expectedResult); - }); - } - - public patchAsync(url: string, apiVersion: string, body: TestResource): Promise { - return new Promise((resolve, reject) => { - resolve(expectedResult); - }); - } - } - - class ErrorClient implements IResourceProviderClient { - public patchAsync(url: string, apiVersion: string, body: TestResource): Promise { - return new Promise((resolve, reject) => { - reject(expectedReason); - }); - } - - public putAsync(url: string, apiVersion: string, body: TestResource): Promise { - return new Promise((resolve, reject) => { - reject(expectedReason); - }); - } - - public postAsync(url: string, apiVersion: string, body: TestResource): Promise { - return new Promise((resolve, reject) => { - reject(expectedReason); - }); - } - - public getAsync(url: string, apiVersion?: string): Promise { - return new Promise((resolve, reject) => { - reject(expectedReason); - }); - } - - public deleteAsync(url: string, apiVersion?: string): Promise { - return new Promise((resolve, reject) => { - reject(expectedReason); - }); - } - } - - class TestResourceProviderClientFactory implements IResourceProviderClientFactory { - public getOrCreate(url: string): IResourceProviderClient { - switch (url) { - case "reject": - return new ErrorClient(); - case "fulfill": - default: - return new SuccessClient(); - } - } - } - - const factory = new TestResourceProviderClientFactory(); - const fulfillClient = factory.getOrCreate("fulfill"); - const rejectClient = factory.getOrCreate("reject"); - const testApiVersion = "apiversion"; - - describe("deleteAsync", () => { - it("returns a fulfilled promise on success", async () => { - const result = await fulfillClient.deleteAsync("/foo", testApiVersion); - expect(result).toEqual(undefined); - }); - - it("returns a rejected promise with a reason on error", async () => { - let result: any; - try { - result = await rejectClient.deleteAsync("/foo", testApiVersion); - } catch (reason) { - result = reason; - } - - expect(result).toEqual(expectedReason); - }); - }); - - describe("getAsync", () => { - it("returns a fulfilled promise with a value on success", async () => { - const result = await fulfillClient.getAsync("/foo", testApiVersion); - expect(result).toEqual(expectedResult); - }); - - it("returns a rejected promise with a reason on error", async () => { - let result: any; - try { - result = await rejectClient.getAsync("/foo", testApiVersion); - } catch (reason) { - result = reason; - } - - expect(result).toEqual(expectedReason); - }); - }); - - describe("postAsync", () => { - it("returns a fulfilled promise with a value on success", async () => { - const result = await fulfillClient.postAsync("/foo", testApiVersion, {}); - expect(result).toEqual(expectedResult); - }); - - it("returns a rejected promise with a reason on error", async () => { - let result: any; - try { - result = await rejectClient.postAsync("/foo", testApiVersion, {}); - } catch (reason) { - result = reason; - } - - expect(result).toEqual(expectedReason); - }); - }); - - describe("putAsync", () => { - it("returns a fulfilled promise with a value on success", async () => { - const result = await fulfillClient.putAsync("/foo", testApiVersion, {}); - expect(result).toEqual(expectedResult); - }); - - it("returns a rejected promise with a reason on error", async () => { - let result: any; - try { - result = await rejectClient.putAsync("/foo", testApiVersion, {}); - } catch (reason) { - result = reason; - } - - expect(result).toEqual(expectedReason); - }); - }); -}); diff --git a/src/ResourceProvider/IResourceProviderClient.ts b/src/ResourceProvider/IResourceProviderClient.ts deleted file mode 100644 index b9205be4b..000000000 --- a/src/ResourceProvider/IResourceProviderClient.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface IResourceProviderClient { - deleteAsync(url: string, apiVersion: string, requestOptions?: IResourceProviderRequestOptions): Promise; - getAsync( - url: string, - apiVersion: string, - queryString?: string, - requestOptions?: IResourceProviderRequestOptions - ): Promise; - postAsync(url: string, apiVersion: string, body: any, requestOptions?: IResourceProviderRequestOptions): Promise; - putAsync( - url: string, - apiVersion: string, - body: any, - requestOptions?: IResourceProviderRequestOptions - ): Promise; - patchAsync( - url: string, - apiVersion: string, - body: any, - requestOptions?: IResourceProviderRequestOptions - ): Promise; -} - -export interface IResourceProviderRequestOptions { - skipResourceValidation: boolean; -} - -export interface IResourceProviderClientFactory { - getOrCreate(url: string): IResourceProviderClient; -} diff --git a/src/ResourceProvider/ResourceProviderClient.ts b/src/ResourceProvider/ResourceProviderClient.ts deleted file mode 100644 index 10f3c8fae..000000000 --- a/src/ResourceProvider/ResourceProviderClient.ts +++ /dev/null @@ -1,217 +0,0 @@ -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 * as 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 deleted file mode 100644 index 4ecc128d1..000000000 --- a/src/ResourceProvider/ResourceProviderClientFactory.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index ade05e7df..000000000 --- a/src/RouteHandlers/RouteHandler.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 0d2031efd..000000000 --- a/src/RouteHandlers/TabRouteHandler.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index 6795fae8b..000000000 --- a/src/RouteHandlers/TabRouteHandler.ts +++ /dev/null @@ -1,407 +0,0 @@ -import crossroads from "crossroads"; -import hasher from "hasher"; -import * as _ from "underscore"; -import * as Constants from "../Common/Constants"; -import * as ViewModels from "../Contracts/ViewModels"; -import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase"; -import TabsBase from "../Explorer/Tabs/TabsBase"; -import { userContext } from "../UserContext"; - -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}/schemaAnalyzer`, - (db_id: string, coll_id: string) => { - this._openSchemaAnalyzerTabForResource(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 - ); - userContext.apiType === "SQL" && collection.onDocumentDBDocumentsClick(); - }); - } - - private _openEntitiesTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && - collection.container && - (userContext.apiType === "Tables" || userContext.apiType === "Cassandra") && - collection.onTableEntitiesClick(); - }); - } - - private _openGraphTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - userContext.apiType === "Gremlin" && collection.onGraphDocumentsClick(); - }); - } - - private _openMongoDocumentsTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - userContext.apiType === "Mongo" && collection.onMongoDBDocumentsClick(); - }); - } - - private _openSchemaAnalyzerTabForResource(databaseId: string, collectionId: string): void { - this._executeActionHelper(() => { - const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( - databaseId, - collectionId - ); - collection && userContext.apiType === "Mongo" && collection.onSchemaAnalyzerClick(); - }); - } - - 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 { - userContext.apiType === "Mongo" && 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 { - userContext.apiType === "Mongo" && 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(); - 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(); - 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(); - 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.isAccountReady()) { - action(); - } - } -} diff --git a/src/SelfServe/Decorators.tsx b/src/SelfServe/Decorators.tsx index a855533ec..3d85024a0 100644 --- a/src/SelfServe/Decorators.tsx +++ b/src/SelfServe/Decorators.tsx @@ -1,5 +1,9 @@ -import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput, RefreshParams } from "./SelfServeTypes"; -import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils"; +/** + * @module SelfServe/Decorators + */ + +import { ChoiceItem, Description, Info, NumberUiType, OnChangeCallback, RefreshParams } from "./SelfServeTypes"; +import { addPropertyToMap, buildSmartUiDescriptor, DecoratorProperties } from "./SelfServeUtils"; type ValueOf = T[keyof T]; interface Decorator { @@ -8,37 +12,99 @@ interface Decorator { } interface InputOptionsBase { + /** + * Key used to pickup the string corresponding to the label of the UI element, from the strings JSON file. + */ labelTKey: string; } +/** + * Numeric input UI element is rendered. The current options are to render it as a slider or a spinner. + */ export interface NumberInputOptions extends InputOptionsBase { + /** + * Min value of the numeric input UI element + */ min: (() => Promise) | number; + /** + * Max value of the numeric input UI element + */ max: (() => Promise) | number; + /** + * Value by which the numeric input is incremented or decremented in the UI. + */ step: (() => Promise) | number; + /** + * The type of the numeric input UI element + */ uiType: NumberUiType; } +/** + * Text box is rendered. + */ export interface StringInputOptions extends InputOptionsBase { + /** + * Key used to pickup the string corresponding to the place holder text of the text box, from the strings JSON file. + */ placeholderTKey?: (() => Promise) | string; } +/** + * Toggle is rendered. + */ export interface BooleanInputOptions extends InputOptionsBase { + /** + * Key used to pickup the string corresponding to the true label of the toggle, from the strings JSON file. + */ trueLabelTKey: (() => Promise) | string; + /** + * Key used to pickup the string corresponding to the false label of the toggle, from the strings JSON file. + */ falseLabelTKey: (() => Promise) | string; } +/** + * Dropdown is rendered. + */ export interface ChoiceInputOptions extends InputOptionsBase { + /** + * Choices to be shown in the dropdown + */ choices: (() => Promise) | ChoiceItem[]; + /** + * Key used to pickup the string corresponding to the placeholder text of the dropdown, from the strings JSON file. + */ placeholderTKey?: (() => Promise) | string; } +/** + * Text is rendered. + */ export interface DescriptionDisplayOptions { + /** + * Optional heading for the text displayed by this description element. + */ labelTKey?: string; + /** + * Static description to be shown as text. + */ description?: (() => Promise) | Description; + /** + * If true, Indicates that the Description will be populated dynamically and that it may not be present in some scenarios. + */ isDynamicDescription?: boolean; } -type InputOptions = +/** + * Interprets the type of the UI element and correspondingly renders + * - slider or spinner + * - text box + * - toggle + * - drop down + * - plain text or message bar + */ +export type InputOptions = | NumberInputOptions | StringInputOptions | BooleanInputOptions @@ -81,20 +147,24 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => { }; }; -export const OnChange = ( - onChange: ( - newValue: InputType, - currentState: Map, - baselineValues: ReadonlyMap - ) => Map -): PropertyDecorator => { +/** + * Indicates the callback to be fired when the UI element corresponding to the property is changed. + */ +export const OnChange = (onChange: OnChangeCallback): PropertyDecorator => { return addToMap({ name: "onChange", value: onChange }); }; +/** + * Indicates that the UI element corresponding to the property should have an Info bubble. The Info + * bubble is the icon that looks like an "i" which users click on to get more information about the UI element. + */ export const PropertyInfo = (info: (() => Promise) | Info): PropertyDecorator => { return addToMap({ name: "info", value: info }); }; +/** + * Indicates that this property should correspond to a UI element with the given parameters. + */ export const Values = (inputOptions: InputOptions): PropertyDecorator => { if (isNumberInputOptions(inputOptions)) { return addToMap( @@ -130,12 +200,20 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => { } }; +/** + * Indicates to the compiler that UI should be generated from this class. + */ export const IsDisplayable = (): ClassDecorator => { return (target) => { buildSmartUiDescriptor(target.name, target.prototype); }; }; +/** + * If there is a long running operation in your page after the {@linkcode onSave} action, the page can + * optionally auto refresh itself using the {@linkcode onRefresh} action. The 'RefreshOptions' indicate + * how often the auto refresh of the page occurs. + */ export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => { return (target) => { addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams); diff --git a/src/SelfServe/Documentation/Documentation.ts b/src/SelfServe/Documentation/Documentation.ts new file mode 100644 index 000000000..ecc294b1d --- /dev/null +++ b/src/SelfServe/Documentation/Documentation.ts @@ -0,0 +1,5 @@ +/** + * [[include: README.md]] + * + * @module SelfServe + */ diff --git a/src/SelfServe/Documentation/README.md b/src/SelfServe/Documentation/README.md new file mode 100644 index 000000000..884c9eb7b --- /dev/null +++ b/src/SelfServe/Documentation/README.md @@ -0,0 +1,357 @@ +# Self Serve Model + +The Self Serve Model allows you to write classes that auto generate UI components for your feature. The idea is to allow developers from other feature teams, who may not be familiar with writing UI, to develop and own UX components. This is accomplished by just writing simpler TypeScript classes for their features. + +What this means for the feature team +- Can concentrate just on the logic behind showing, hiding and disabling UI components +- Need not worry about specifics of the UI language or UX requirements (Accessibility, Localization, Themes, etc.) +- Can own the REST API calls made as part of the feature, which can change in the future +- Quicker turn around time for development and bug fixes since they have deeper knowledge of the feature + +What this means for the UI team +- No need to ramp up on the intricacies of every feature which requires UI changes +- Own only the framework and not every feature, giving more bandwidth to prioritize inhouse features as well + +## Getting Started + +Clone the cosmos-explorer repo and run + +- `npm install` +- `npm run build` + +[Click here](../index.html) for more info on setting up the cosmos-explorer repo. + +## Code Changes + +Code changes need to be made only in the following files +- A JSON file - for strings to be displayed +- A Types File - for defining the data models +- A RP file - for defining the REST calls +- A Class file - for defining the UI +- [SelfServeUtils.tsx](https://github.com/Azure/cosmos-explorer/blob/master/src/SelfServe/SelfServeUtils.tsx) and [SelfServe.tsx](https://github.com/Azure/cosmos-explorer/blob/master/src/SelfServe/SelfServe.tsx) - for defning the entrypoint for the UI + +### 1. JSON file for UI strings + +#### Naming Convention +`Localization/en/.json`\ +Please place your files only under "Localization/en" folder. If not, the UI strings will not be picked up by the framework. + +#### Example +[SelfServeExample.json](https://github.com/Azure/cosmos-explorer/blob/master/src/Localization/en/SelfServeExample.json) + +#### Description +This is a JSON file where the values are the strings that needs to be displayed in the UI. These strings are referenced using their corresponding unique keys. + +For example, If your class file defines properties as follows +```ts + @Values({ + labelTKey: "stringPropertylabel" + }) + stringProperty: string; + + @Values({ + labelTKey: "booleanPropertyLabel", + trueLabelTKey: "trueLabel", + falseLabelTKey: "falseLabel", + }) + booleanProperty: boolean; +``` + +Then the content of `Localization/en/FeatureName.json` should be + +```json +{ + stringPropertyLabel: "string property", + booleanPropertyLabel: "boolean property", + trueLabel: "Enable", + falseLabel: "Disable" +} +``` +You can learn more on how to define the class file [here](./selfserve.html#4-class-file). + +### 2. Types file + +#### Naming Convention +`.types.ts` + +#### Example +[SelfServeExample.types.ts](https://github.com/Azure/cosmos-explorer/blob/master/src/SelfServe/Example/SelfServeExample.types.ts) + +#### Description +This file contains the definitions of all the data models to be used in your Class file and RP file. + +For example, if your RP call takes/returns the `stringProperty` and `booleanProperty` of your SelfServe class, then you can define an interface in your `FeatureName.types.ts` file like this. + +```ts +export RpDataModel { + stringProperty: string, + booleanProperty: boolean +} +``` + +### 3. RP file + +#### Naming Convention +`.rp.ts` + +#### Example +[SelfServeExample.rp.ts](https://github.com/Azure/cosmos-explorer/blob/master/src/SelfServe/Example/SelfServeExample.rp.ts) + +#### Description +The RP file will host the REST calls needed for the initialize, save and refresh functions. This decouples the view and the model of the feature. + +To make the ARM call, we need some information about the Azure Cosmos DB databaseAccount - the subscription id, resource group name and database account name. These are readily available through the `userContext` object, exposed through + +* `userContext.subscriptionId` +* `userContext.resourceGroup` +* `userContext.databaseAccount.name` + +You can use the `armRequestWithoutPolling` function to make the ARM api call. + +Your `FeatureName.rp.ts` file can look like the following. + +```ts +import { userContext } from "../../UserContext"; +import { armRequestWithoutPolling } from "../../Utils/arm/request"; +import { configContext } from "../../ConfigContext"; + +const apiVersion = "2020-06-01-preview"; + +export const saveData = async (properties: RpDataModel): Promise => { + const path = `/subscriptions/${userContext.subscriptionId}/resourceGroups/${userContext.resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/` + const body = { + data : properties + } + const armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "PUT", + apiVersion, + body, + }); + + return armRequestResult.operationStatusUrl; +}; + +``` + +### 4. Class file + +#### Naming Convention +`.tsx` + +#### Example +[SelfServeExample.tsx](https://github.com/Azure/cosmos-explorer/blob/master/src/SelfServe/Example/SelfServeExample.tsx) + +#### Description +This file will contain the actual code that is translated into the UI component by the Self Serve framework. +* Each Self Serve class + * Needs to extends the [SelfServeBase](../classes/selfserve_selfservetypes.selfservebaseclass.html) class. + * Needs to have the [@IsDisplayable()](./selfserve_decorators.html#isdisplayable) decorator to tell the compiler that UI needs to be generated from this class. + * Needs to define an [initialize()](../classes/selfserve_selfservetypes.selfservebaseclass.html#initialize) function, to set default values for the inputs. + * Needs to define an [onSave()](../classes/selfserve_selfservetypes.selfservebaseclass.html#onsave) function, a callback for when the save button is clicked. + * Needs to define an [onRefresh()](../classes/selfserve_selfservetypes.selfservebaseclass.html#onrefresh) function, a callback for when the refresh button is clicked. + * Can have an optional [@RefreshOptions()](./selfserve_decorators.html#refreshoptions) decorator that determines how often the auto refresh of the UI component should take place. + +* For every UI element needed, add a property to the Self Serve class. Each of these properties + * Needs to have a [@Values()](./selfserve_decorators.html#values) decorator. + * Can have an optional [@PropertyInfo()](./selfserve_decorators.html#propertyinfo) decorator that describes it's info bubble. + * Can have an optional [@OnChange()](./selfserve_decorators.html#onchange) decorator that dictates the effects of the change of the UI element tied to this property. + +Your `FeatureName.tsx` file will look like the following. +```ts +@IsDisplayable() +@RefreshOptions({ retryIntervalInMs: 2000 }) +export default class FeatureName extends SelfServeBaseClass { + + public initialize = async (): Promise> => { + // initialize RP call and processing logic + } + + public onSave = async ( + currentValues: Map, + baselineValues: ReadonlyMap + ): Promise => { + // onSave RP call and processing logic + } + + public onRefresh = async (): Promise => { + // refresh RP call and processing logic + }; + + @Values(...) + stringProperty: string; + + @OnChange(...) + @PropertyInfo(...) + @Values(...) + booleanProperty: boolean; +} +``` + +### 5. Update SelfServeType + +Once you have written your Self Serve Class, add a corresponding type to [SelfServeType](../enums/selfserve_selfserveutils.selfservetype.html) + +```ts +export enum SelfServeType { + invalid = "invalid", + example = "example", + ... + // Add the type for your new feature + featureName = "featurename" +} +``` + +### 6. Update SelfServe.tsx (landing page) + +Once the SelfServeType has been updated, update [SelfServe.tsx](https://github.com/Azure/cosmos-explorer/blob/master/src/SelfServe/SelfServe.tsx) for your feature. This ensures that the framework picks up your SelfServe Class. + +```ts +const getDescriptor = async (selfServeType: SelfServeType): Promise => { + switch (selfServeType) { + case SelfServeType.example: { + .... + } + ... + ... + ... + // Add this for your new feature + case SelfServeType.featureName: { + // The 'webpackChunkName' is used during debugging, to identify if the correct class has been loaded + const FeatureName = await import(/* webpackChunkName: "FeatureName" */ "./FeatureName/FeatureName"); + const featureName = new FeatureName.default(); + await loadTranslations(featureName.constructor.name); + return featureName.toSelfServeDescriptor(); + } + ... + ... + default: + return undefined; + } +}; + +``` + +## Telemetry +You can add telemetry for your feature using the functions in [SelfServeTelemetryProcessor](./selfserve_selfservetelemetryprocessor.html) + +For example, in your SelfServe class, you can call the trace method in your `onSave` function. + +```ts +import { saveData } from "./FeatureName.rp" +import { RpDataModel } from "./FeatureName.types" + +@IsDisplayable() +export default class FeatureName extends SelfServeBaseClass { + + . + . + . + + public onSave = async ( + currentValues: Map, + baselineValues: ReadonlyMap + ): Promise => { + + stringPropertyValue = currentValues.get("stringProperty") + booleanPropertyValue = currentValues.get("booleanProperty") + + const propertiesToSave : RpDataModel = { + stringProperty: stringPropertyValue, + booleanProperty: booleanPropertyValue + } + const telemetryData = { ...propertiesToSave, selfServeClassName: FeatureName.name } + const onSaveTimeStamp = selfServeTraceStart(telemetryData) + + await saveData(propertiesToSave) + + selfServeTraceSuccess(telemetryData, onSaveTimeStamp) + + // return required values + } + + . + . + . + + @Values(...) + stringProperty: string; + + @Values(...) + booleanProperty: boolean; +} +``` +## Portal Notifications +You can enable portal notifications for your feature by passing in the required strings as part of the [portalNotification](../interfaces/selfserve_selfservetypes.onsaveresult.html#portalnotification) property of the [onSaveResult](../interfaces/selfserve_selfservetypes.onsaveresult.html). + +```ts +@IsDisplayable() +export default class SqlX extends SelfServeBaseClass { + +. +. +. + + public onSave = async ( + currentValues: Map, + baselineValues: Map + ): Promise => { + + stringPropertyValue = currentValues.get("stringProperty") + booleanPropertyValue = currentValues.get("booleanProperty") + + const propertiesToSave : RpDataModel = { + stringProperty: stringPropertyValue, + booleanProperty: booleanPropertyValue + } + + const operationStatusUrl = await saveData(propertiesToSave); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "DeleteInitializeTitle", + messageTKey: "DeleteInitializeMessage", + }, + success: { + titleTKey: "DeleteSuccessTitle", + messageTKey: "DeleteSuccesseMessage", + }, + failure: { + titleTKey: "DeleteFailureTitle", + messageTKey: "DeleteFailureMessage", + }, + }, + }; + } + + . + . + . + + @Values(...) + stringProperty: string; + + @Values(...) + booleanProperty: boolean; +} +``` + +## Execution + +### Watch mode + +Run `npm start` to start the development server and automatically rebuild on changes + +### Local Development + +Ensure that you have made the [Code changes](./selfserve.html#code-changes). + +- Go to `https://ms.portal.azure.com/` +- Add the query string `feature.showSelfServeExample=true&feature.selfServeSource=https://localhost:1234/selfServe.html?selfServeType%3D` +- Click on the `Self Serve Example` menu item on the left panel. + +For example, if you want to open up the the UI of a class with the type `sqlx`, then visit `https://ms.portal.azure.com/?feature.showSelfServeExample=true&feature.selfServeSource=https://localhost:1234/selfServe.html?selfServeType%3Dsqlx` + +![](https://sdkctlstore.blob.core.windows.net/exe/selfserveDev.PNG) diff --git a/src/SelfServe/Documentation/SupportedFeatures.md b/src/SelfServe/Documentation/SupportedFeatures.md new file mode 100644 index 000000000..f4820cc1d --- /dev/null +++ b/src/SelfServe/Documentation/SupportedFeatures.md @@ -0,0 +1,12 @@ +The Self Serve framework has integrated support for + +1. [Portal Notifications](./selfserve.html#portal-notifications) +2. [Telemetry](./selfserve.html#telemetry) +3. the following UI controls: + * [Slider](https://developer.microsoft.com/en-us/fluentui#/controls/web/slider) + * [SpinButton](https://developer.microsoft.com/en-us/fluentui#/controls/web/spinbutton) + * [TextField](https://developer.microsoft.com/en-us/fluentui#/controls/web/textfield) + * [Toggle](https://developer.microsoft.com/en-us/fluentui#/controls/web/toggle) + * [Dropdown](https://developer.microsoft.com/en-us/fluentui#/controls/web/dropdown) + * [Link](https://developer.microsoft.com/en-us/fluentui#/controls/web/link) + * [MessageBar](https://developer.microsoft.com/en-us/fluentui#/controls/web/messagebar) diff --git a/src/SelfServe/Documentation/SupportedFeatures.ts b/src/SelfServe/Documentation/SupportedFeatures.ts new file mode 100644 index 000000000..5e23dde1c --- /dev/null +++ b/src/SelfServe/Documentation/SupportedFeatures.ts @@ -0,0 +1,5 @@ +/** + * [[include: SupportedFeatures.md]] + * + * @module SelfServe - What is currently supported? + */ diff --git a/src/SelfServe/Example/SelfServeExample.rp.ts b/src/SelfServe/Example/SelfServeExample.rp.ts index f0da48641..8762c1862 100644 --- a/src/SelfServe/Example/SelfServeExample.rp.ts +++ b/src/SelfServe/Example/SelfServeExample.rp.ts @@ -1,20 +1,8 @@ import { SessionStorageUtility } from "../../Shared/StorageUtility"; import { userContext } from "../../UserContext"; -import { get } from "../../Utils/arm/generatedClients/2020-04-01/databaseAccounts"; +import { get } from "../../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { RefreshResult } from "../SelfServeTypes"; -export enum Regions { - NorthCentralUS = "NorthCentralUS", - WestUS = "WestUS", - EastUS2 = "EastUS2", -} - -export interface InitializeResponse { - regions: Regions; - enableLogging: boolean; - accountName: string; - collectionThroughput: number; - dbThroughput: number; -} +import { AccountProps, Regions } from "./SelfServeExample.types"; export const getMaxCollectionThroughput = async (): Promise => { return 10000; @@ -32,21 +20,19 @@ export const getMinDatabaseThroughput = async (): Promise => { return 400; }; -export const update = async ( - regions: Regions, - enableLogging: boolean, - accountName: string, - collectionThroughput: number, - dbThoughput: number -): Promise => { - SessionStorageUtility.setEntry("regions", regions); - SessionStorageUtility.setEntry("enableLogging", enableLogging?.toString()); - SessionStorageUtility.setEntry("accountName", accountName); - SessionStorageUtility.setEntry("collectionThroughput", collectionThroughput?.toString()); - SessionStorageUtility.setEntry("dbThroughput", dbThoughput?.toString()); +export const update = async (accountProps: AccountProps): Promise => { + // This is only an example. DO NOT store actual data in the Session/Local storage for your SelfServe feature. + + SessionStorageUtility.setEntry("regions", accountProps.regions); + SessionStorageUtility.setEntry("enableLogging", accountProps.enableLogging?.toString()); + SessionStorageUtility.setEntry("accountName", accountProps.accountName); + SessionStorageUtility.setEntry("collectionThroughput", accountProps.collectionThroughput?.toString()); + SessionStorageUtility.setEntry("dbThroughput", accountProps.dbThroughput?.toString()); }; -export const initialize = async (): Promise => { +export const initialize = async (): Promise => { + // This is only an example. DO NOT store actual data in the Session/Local storage for your SelfServe feature. + const regions = Regions[SessionStorageUtility.getEntry("regions") as keyof typeof Regions]; const enableLogging = SessionStorageUtility.getEntry("enableLogging") === "true"; const accountName = SessionStorageUtility.getEntry("accountName"); @@ -64,6 +50,8 @@ export const initialize = async (): Promise => { }; export const onRefreshSelfServeExample = async (): Promise => { + // This is only an example. DO NOT store actual data in the Session/Local storage for your SelfServe feature. + const refreshCountString = SessionStorageUtility.getEntry("refreshCount"); const refreshCount = refreshCountString ? parseInt(refreshCountString) : 0; diff --git a/src/SelfServe/Example/SelfServeExample.tsx b/src/SelfServe/Example/SelfServeExample.tsx index a533a26f7..004ff993d 100644 --- a/src/SelfServe/Example/SelfServeExample.tsx +++ b/src/SelfServe/Example/SelfServeExample.tsx @@ -1,4 +1,5 @@ import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators"; +import { selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; import { ChoiceItem, Description, @@ -18,9 +19,9 @@ import { getMinDatabaseThroughput, initialize, onRefreshSelfServeExample, - Regions, update, } from "./SelfServeExample.rp"; +import { AccountProps, Regions } from "./SelfServeExample.types"; const regionDropdownItems: ChoiceItem[] = [ { labelTKey: "NorthCentralUS", key: Regions.NorthCentralUS }, @@ -73,61 +74,16 @@ const validate = ( } }; -/* - This is an example self serve class that auto generates UI components for your feature. - - Each self serve class - - Needs to extends the SelfServeBase class. - - Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class. - - Needs to define an onSave() function, a callback for when the submit button is clicked. - - Needs to define an initialize() function, to set default values for the inputs. - - Needs to define an onRefresh() function, a callback for when the refresh button is clicked. - - You can test this self serve UI by using the featureflag '?feature.selfServeType=example' - and plumb in similar feature flags for your own self serve class. - - All string to be used should be present in the "src/Localization" folder, in the language specific json files. The - corresponding key should be given as the value for the fields like "label", the error message etc. -*/ - -/* - @IsDisplayable() - - role: Indicates to the compiler that UI should be generated from this class. -*/ @IsDisplayable() -/* - @RefreshOptions() - - role: Passes the refresh options to be used by the self serve model. - - inputs: - retryIntervalInMs - The time interval between refresh attempts when an update in ongoing. -*/ @RefreshOptions({ retryIntervalInMs: 2000 }) export default class SelfServeExample extends SelfServeBaseClass { - /* - onRefresh() - - role : Callback that is triggerrd when the refresh button is clicked. You should perform the your rest API - call to check if the update action is completed. - - returns: - RefreshResult - - isComponentUpdating: Indicated if the state is still being updated - notificationMessage: Notification message to be shown in case the component is still being updated - i.e, isComponentUpdating is true - */ public onRefresh = async (): Promise => { return onRefreshSelfServeExample(); }; /* - onSave() - - input: (currentValues: Map, baselineValues: ReadonlyMap) => Promise - - role: Callback that is triggerred when the submit button is clicked. You should perform your rest API - calls here using the data from the different inputs passed as a Map to this callback function. - - In this example, the onSave callback simply sets the value for keys corresponding to the field name - in the SessionStorage. It uses the currentValues and baselineValues maps to perform custom validations - as well. - - - returns: The initialize, success and failure messages to be displayed in the Portal Notification blade after the operation is completed. + In this example, the onSave callback simply sets the value for keys corresponding to the field name in the SessionStorage. + It uses the currentValues and baselineValues maps to perform custom validations as well. */ public onSave = async ( currentValues: Map, @@ -142,7 +98,13 @@ export default class SelfServeExample extends SelfServeBaseClass { let dbThroughput = currentValues.get("dbThroughput")?.value as number; dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined; try { - await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput); + const accountProps: AccountProps = { regions, enableLogging, accountName, collectionThroughput, dbThroughput }; + const telemetryData = { ...accountProps, selfServeClassName: SelfServeExample.name }; + + const onSaveTimeStamp = selfServeTraceStart(telemetryData); + await update(accountProps); + selfServeTraceSuccess(telemetryData, onSaveTimeStamp); + if (currentValues.get("regions") === baselineValues.get("regions")) { return { operationStatusUrl: undefined, @@ -186,19 +148,8 @@ export default class SelfServeExample extends SelfServeBaseClass { }; /* - initialize() - - role: Set default values for the properties of this class. - - The properties of this class (namely regions, enableLogging, accountName, dbThroughput, collectionThroughput), - having the @Values decorator, will each correspond to an UI element. Their values can be of 'InputType'. Their - defaults can be set by setting values in a Map corresponding to the field's name. - - Typically, you can make rest calls in the async initialize function, to fetch the initial values for - these fields. This is called after the onSave callback, to reinitialize the defaults. - - In this example, the initialize function simply reads the SessionStorage to fetch the default values - for these fields. These are then set when the changes are submitted. - - returns: () => Promise> + In this example, the initialize function simply reads the SessionStorage to fetch the default values + for these fields. These are then set when the changes are submitted. */ public initialize = async (): Promise> => { const initializeResponse = await initialize(); @@ -215,16 +166,6 @@ export default class SelfServeExample extends SelfServeBaseClass { return defaults; }; - /* - @Values() : - - input: NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions | DescriptionDisplay - - role: Specifies the required options to display the property as - a) TextBox for text input - b) Spinner/Slider for number input - c) Radio buton/Toggle for boolean input - d) Dropdown for choice input - e) Text (with optional hyperlink) for descriptions - */ @Values({ labelTKey: "DescriptionLabel", description: { @@ -244,28 +185,12 @@ export default class SelfServeExample extends SelfServeBaseClass { }) currentRegionText: string; - /* - @PropertyInfo() - - optional - - input: Info | () => Promise - - role: Display an Info bar above the UI element for this property. - */ @PropertyInfo(regionDropdownInfo) /* - @OnChange() - - optional - - input: (currentValues: Map, newValue: InputType, baselineValues: ReadonlyMap) => Map - - role: Takes a Map of current values, the newValue for this property and a ReadonlyMap of baselineValues as inputs. This is called when a property, - say prop1, changes its value in the UI. This can be used to - a) Change the value (and reflect it in the UI) for prop2 based on prop1. - b) Change the visibility for prop2 in the UI, based on prop1 - - The new Map of propertyName -> value is returned. - - In this example, the onRegionsChange function sets the enableLogging property to false (and disables - the corresponsing toggle UI) when "regions" is set to "North Central US", and enables the toggle for - any other value of "regions" + In this example, the onRegionsChange function sets the enableLogging property to false (and disables + the corresponsing toggle UI) when "regions" is set to "North Central US", and enables the toggle for + any other value of "regions" */ @OnChange(onRegionsChange) @Values({ labelTKey: "Regions", choices: regionDropdownItems, placeholderTKey: "RegionsPlaceholder" }) diff --git a/src/SelfServe/Example/SelfServeExample.types.ts b/src/SelfServe/Example/SelfServeExample.types.ts new file mode 100644 index 000000000..79e2726fa --- /dev/null +++ b/src/SelfServe/Example/SelfServeExample.types.ts @@ -0,0 +1,13 @@ +export enum Regions { + NorthCentralUS = "NorthCentralUS", + WestUS = "WestUS", + EastUS2 = "EastUS2", +} + +export interface AccountProps { + regions: Regions; + enableLogging: boolean; + accountName: string; + collectionThroughput: number; + dbThroughput: number; +} diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index 27d868756..50f6eecf8 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -86,7 +86,7 @@ const handleMessage = async (event: MessageEvent): Promise => { } const urlSearchParams = new URLSearchParams(window.location.search); - const selfServeTypeText = inputs.selfServeType || urlSearchParams.get("selfServeType"); + const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType; const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType]; if ( !inputs.subscriptionId || diff --git a/src/SelfServe/SelfServeComponent.tsx b/src/SelfServe/SelfServeComponent.tsx index 732c5df17..41ae52e02 100644 --- a/src/SelfServe/SelfServeComponent.tsx +++ b/src/SelfServe/SelfServeComponent.tsx @@ -17,6 +17,8 @@ import * as _ from "underscore"; import { sendMessage } from "../Common/MessageHandler"; import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts"; import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent"; +import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; +import { trace } from "../Shared/Telemetry/TelemetryProcessor"; import { commandBarItemStyles, commandBarStyles, containerStackTokens, separatorStyles } from "./SelfServeStyles"; import { AnyDisplay, @@ -27,6 +29,7 @@ import { Node, NumberInput, RefreshResult, + SelfServeComponentTelemetryType, SelfServeDescriptor, SmartUiInput, StringInput, @@ -87,6 +90,12 @@ export class SelfServeComponent extends React.Component => { + const telemetryData = { + selfServeClassName: this.props.descriptor.root.id, + eventType: SelfServeComponentTelemetryType.Save, + }; + trace(Action.SelfServeComponent, ActionModifiers.Mark, telemetryData, SelfServeMessageTypes.TelemetryInfo); + this.setState({ isSaving: true, notification: undefined }); try { const onSaveResult = await this.props.descriptor.onSave( diff --git a/src/SelfServe/SelfServeTelemetryProcessor.ts b/src/SelfServe/SelfServeTelemetryProcessor.ts index 0cdf81a15..ac5376abc 100644 --- a/src/SelfServe/SelfServeTelemetryProcessor.ts +++ b/src/SelfServe/SelfServeTelemetryProcessor.ts @@ -1,24 +1,53 @@ +/** + * @module SelfServe/SelfServeTelemetryProcessor + */ + import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { trace, traceCancel, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; import { SelfServeTelemetryMessage } from "./SelfServeTypes"; +/** + * Log an action. + * @param data Data to be sent as part of the Self Serve Telemetry. + */ export const selfServeTrace = (data: SelfServeTelemetryMessage): void => { trace(Action.SelfServe, ActionModifiers.Mark, data, SelfServeMessageTypes.TelemetryInfo); }; +/** + * Start logging an action. + * @param data Data to be sent as part of the Self Serve Telemetry. + * @returns Timestamp of the trace start, that can be used in other trace actions. + * The timestamp is used to identify all the logs associated with an action. + */ export const selfServeTraceStart = (data: SelfServeTelemetryMessage): number => { return traceStart(Action.SelfServe, data, SelfServeMessageTypes.TelemetryInfo); }; +/** + * Log an action as a success. + * @param data Data to be sent as part of the Self Serve Telemetry. + * @param timestamp Timestamp of the action's start trace. + */ export const selfServeTraceSuccess = (data: SelfServeTelemetryMessage, timestamp?: number): void => { traceSuccess(Action.SelfServe, data, timestamp, SelfServeMessageTypes.TelemetryInfo); }; +/** + * Log an action as a failure. + * @param data Data to be sent as part of the Self Serve Telemetry. + * @param timestamp Timestamp of the action's start trace. + */ export const selfServeTraceFailure = (data: SelfServeTelemetryMessage, timestamp?: number): void => { traceFailure(Action.SelfServe, data, timestamp, SelfServeMessageTypes.TelemetryInfo); }; +/** + * Log an action as cancelled. + * @param data Data to be sent as part of the Self Serve Telemetry. + * @param timestamp Timestamp of the action's start trace. + */ export const selfServeTraceCancel = (data: SelfServeTelemetryMessage, timestamp?: number): void => { traceCancel(Action.SelfServe, data, timestamp, SelfServeMessageTypes.TelemetryInfo); }; diff --git a/src/SelfServe/SelfServeTypes.ts b/src/SelfServe/SelfServeTypes.ts index 518d76fee..d6c96e031 100644 --- a/src/SelfServe/SelfServeTypes.ts +++ b/src/SelfServe/SelfServeTypes.ts @@ -1,3 +1,7 @@ +/** + * @module SelfServe/SelfServeTypes + */ + import { TelemetryData } from "../Shared/Telemetry/TelemetryProcessor"; interface BaseInput { @@ -13,6 +17,7 @@ interface BaseInput { placeholderTKey?: (() => Promise) | string; } +/**@internal */ export interface NumberInput extends BaseInput { min: (() => Promise) | number; max: (() => Promise) | number; @@ -21,25 +26,30 @@ export interface NumberInput extends BaseInput { uiType: NumberUiType; } +/**@internal */ export interface BooleanInput extends BaseInput { trueLabelTKey: (() => Promise) | string; falseLabelTKey: (() => Promise) | string; defaultValue?: boolean; } +/**@internal */ export interface StringInput extends BaseInput { defaultValue?: string; } +/**@internal */ export interface ChoiceInput extends BaseInput { choices: (() => Promise) | ChoiceItem[]; defaultKey?: string; } +/**@internal */ export interface DescriptionDisplay extends BaseInput { description: (() => Promise) | Description; } +/**@internal */ export interface Node { id: string; info?: (() => Promise) | Info; @@ -47,6 +57,7 @@ export interface Node { children?: Node[]; } +/**@internal */ export interface SelfServeDescriptor { root: Node; initialize?: () => Promise>; @@ -59,16 +70,57 @@ export interface SelfServeDescriptor { refreshParams?: RefreshParams; } +/**@internal */ +export enum SelfServeComponentTelemetryType { + Load = "Load", + Save = "Save", +} + +/**@internal */ export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay; -export abstract class SelfServeBaseClass { - public abstract initialize: () => Promise>; - public abstract onSave: ( +/**@internal */ +export type InputTypeValue = "number" | "string" | "boolean" | "object"; + +export type initializeCallback = + /** + * @returns Promise of Map of propertyName => {@linkcode SmartUiInput} which will become the current state of the UI. + */ + () => Promise>; + +export type onSaveCallback = + /** + * @param currentValues - The map of propertyName => {@linkcode SmartUiInput} corresponding to the current state of the UI + * @param baselineValues - The map of propertyName => {@linkcode SmartUiInput} corresponding to the initial state of the UI + */ + ( currentValues: Map, baselineValues: ReadonlyMap ) => Promise; + +/** + * All SelfServe feature classes need to derive from the SelfServeBaseClass + */ +export abstract class SelfServeBaseClass { + /** + * Sets default values for the properties of the Self Serve Class. Typically, you can make rest calls here + * to fetch the initial values for the properties. This is also called after the onSave callback, to reinitialize the defaults. + */ + public abstract initialize: initializeCallback; + + /** + * Callback that is triggerred when the submit button is clicked. You should perform your rest API + * calls here using the data from the different inputs passed as a Map to this callback function. + */ + public abstract onSave: onSaveCallback; + + /** + * Callback that is triggered when the refresh button is clicked. Here, you should perform the your rest API + * call to check if the update action is completed. + */ public abstract onRefresh: () => Promise; + /**@internal */ public toSelfServeDescriptor(): SelfServeDescriptor { const className = this.constructor.name; const selfServeDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor; @@ -95,73 +147,202 @@ export abstract class SelfServeBaseClass { } } -export type InputTypeValue = "number" | "string" | "boolean" | "object"; +/** + * Function that dictates how the overall UI should transform when the UI element corresponding to a property, say prop1, is changed. + * The callback can be used to\ + * * Change the value (and reflect it in the UI) for another property, say prop2\ + * * Change the visibility for prop2 in the UI\ + * * Disable or enable the UI element corresponding to prop2\ + * depending on logic based on the newValue of prop1, the currentValues Map and baselineValues Map. + */ +export type OnChangeCallback = + /** + * @param newValue - The newValue that the property needs to be set to, after the change in the UI element corresponding to this property. + * @param currentValues - The map of propertyName => {@linkcode SmartUiInput} corresponding to the current state of the UI. + * @param baselineValues - The map of propertyName => {@linkcode SmartUiInput} corresponding to the initial state of the UI. + * @returns A new Map of propertyName => {@linkcode SmartUiInput} corresponding to the new state of the overall UI + */ + ( + newValue: InputType, + currentValues: Map, + baselineValues: ReadonlyMap + ) => Map; export enum NumberUiType { + /** + * The numeric input UI element corresponding to the property is a Spinner + */ Spinner = "Spinner", + /** + * The numeric input UI element corresponding to the property is a Slider + */ Slider = "Slider", } -export type ChoiceItem = { labelTKey: string; key: string }; +export type ChoiceItem = { + /** + * Key used to pickup the string corresponding to the label of the dropdown choice item, from the strings JSON file. + */ + labelTKey: string; + /** + * Key used to pickup the string that uniquely identifies the dropdown choice item, from the strings JSON file. + */ + key: string; +}; export type InputType = number | string | boolean | ChoiceItem | Description; +/** + * Data to be shown within the info bubble of the property. + */ export interface Info { + /** + * Key used to pickup the string corresponding to the text to be shown within the info bubble, from the strings JSON file. + */ messageTKey: string; + /** + * Optional link to be shown within the info bubble, after the text. + */ link?: { + /** + * The URL of the link + */ href: string; + /** + * Key used to pickup the string corresponding to the text of the link, from the strings JSON file. + */ textTKey: string; }; } export enum DescriptionType { + /** + * Show the description as a text + */ Text, + /** + * Show the description as a Info Message bar. + */ InfoMessageBar, + /** + * Show the description as a Warning Message bar. + */ WarningMessageBar, } +/** + * Data to be shown as a description. + */ export interface Description { + /** + * Key used to pickup the string corresponding to the text to be shown as part of the description, from the strings JSON file. + */ textTKey: string; type: DescriptionType; + /** + * Optional link to be shown as part of the description, after the text. + */ link?: { + /** + * The URL of the link + */ href: string; + /** + * Key used to pickup the string corresponding to the text of the link, from the strings JSON file. + */ textTKey: string; }; } export interface SmartUiInput { + /** + * The value to be set for the UI element corresponding to the property + */ value: InputType; + /** + * Indicates whether the UI element corresponding to the property is hidden + */ hidden?: boolean; + /** + * Indicates whether the UI element corresponding to the property is disabled + */ disabled?: boolean; } export interface OnSaveResult { + /** + * The polling url returned by the RP call. + */ operationStatusUrl: string; + /** + * Notifications that need to be shown on the portal for different stages of a scenario (initialized, success/failure). + */ portalNotification?: { + /** + * Notification that need to be shown when the save operation has been triggered. + */ initialize: { + /** + * Key used to pickup the string corresponding to the notification title, from the strings JSON file. + */ titleTKey: string; + /** + * Key used to pickup the string corresponding to the notification message, from the strings JSON file. + */ messageTKey: string; }; + /** + * Notification that need to be shown when the save operation has successfully completed. + */ success: { + /** + * Key used to pickup the string corresponding to the notification title, from the strings JSON file. + */ titleTKey: string; + /** + * Key used to pickup the string corresponding to the notification message, from the strings JSON file. + */ messageTKey: string; }; + /** + * Notification that need to be shown when the save operation failed. + */ failure: { + /** + * Key used to pickup the string corresponding to the notification title, from the strings JSON file. + */ titleTKey: string; + /** + * Key used to pickup the string corresponding to the notification message, from the strings JSON file. + */ messageTKey: string; }; }; } export interface RefreshResult { + /** + * Indicate if the update is still ongoing + */ isUpdateInProgress: boolean; + + /** + * Key used to pickup the string corresponding to the message that will be shown on the UI if the update is still ongoing, from the strings JSON file. + * Will be shown only if {@linkcode isUpdateInProgress} is true. + */ updateInProgressMessageTKey: string; } export interface RefreshParams { + /** + * The time interval between refresh attempts when an update in ongoing + */ retryIntervalInMs: number; } export interface SelfServeTelemetryMessage extends TelemetryData { + /** + * The className used to identify a SelfServe telemetry record + */ selfServeClassName: string; } diff --git a/src/SelfServe/SelfServeUtils.test.tsx b/src/SelfServe/SelfServeUtils.test.tsx index 324d18f77..05016747c 100644 --- a/src/SelfServe/SelfServeUtils.test.tsx +++ b/src/SelfServe/SelfServeUtils.test.tsx @@ -190,7 +190,8 @@ describe("SelfServeUtils", () => { max: 5, step: 1, uiType: "Spinner", - errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidThroughput'.", + errorMessage: + "labelTkey, trueLabelTKey and falseLabelTKey are required for boolean input 'invalidThroughput'.", }, children: [] as Node[], }, @@ -225,7 +226,8 @@ describe("SelfServeUtils", () => { type: "boolean", labelTKey: "Invalid Enable Logging", placeholderTKey: "placeholder text", - errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.", + errorMessage: + "labelTkey, trueLabelTKey and falseLabelTKey are required for boolean input 'invalidEnableLogging'.", }, children: [] as Node[], }, @@ -252,7 +254,7 @@ describe("SelfServeUtils", () => { type: "object", labelTKey: "Invalid Regions", placeholderTKey: "placeholder text", - errorMessage: "label and choices are required for Choice input 'invalidRegions'.", + errorMessage: "labelTKey and choices are required for Choice input 'invalidRegions'.", }, children: [] as Node[], }, diff --git a/src/SelfServe/SelfServeUtils.tsx b/src/SelfServe/SelfServeUtils.tsx index e82084069..735cf5228 100644 --- a/src/SelfServe/SelfServeUtils.tsx +++ b/src/SelfServe/SelfServeUtils.tsx @@ -1,3 +1,7 @@ +/** + * @module SelfServe/SelfServeUtils + */ + import "reflect-metadata"; import { userContext } from "../UserContext"; import { @@ -18,9 +22,10 @@ import { StringInput, } from "./SelfServeTypes"; +/** + * The type used to identify the Self Serve Class + */ export enum SelfServeType { - // No self serve type passed, launch explorer - none = "none", // Unsupported self serve type passed as feature flag invalid = "invalid", // Add your self serve types here @@ -28,15 +33,47 @@ export enum SelfServeType { sqlx = "sqlx", } +/** + * Portal Blade types + */ export enum BladeType { + /** + * Keys blade of a SQL API account. + */ SqlKeys = "keys", + /** + * Keys blade of a Mongo API account. + */ MongoKeys = "mongoDbKeys", + /** + * Keys blade of a Cassandra API account. + */ CassandraKeys = "cassandraDbKeys", + /** + * Keys blade of a Gremlin API account. + */ GremlinKeys = "keys", + /** + * Keys blade of a Table API account. + */ TableKeys = "tableKeys", + /** + * Metrics blade of an Azure Cosmos DB account. + */ Metrics = "metrics", } +/** + * Generate the URL corresponding to the portal blade for the current Azure Cosmos DB account + */ +export const generateBladeLink = (blade: BladeType): string => { + const subscriptionId = userContext.subscriptionId; + const resourceGroupName = userContext.resourceGroup; + const databaseAccountName = userContext.databaseAccount.name; + return `${document.referrer}#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`; +}; + +/**@internal */ export interface DecoratorProperties { id: string; info?: (() => Promise) | Info; @@ -62,6 +99,7 @@ export interface DecoratorProperties { ) => Map; } +/**@internal */ const setValue = ( name: T, value: K, @@ -70,10 +108,12 @@ const setValue = (name: T, fieldObject: DecoratorProperties): unknown => { return fieldObject[name]; }; +/**@internal */ export const addPropertyToMap = ( target: unknown, propertyName: string, @@ -88,6 +128,7 @@ export const addPropertyToMap = ( context: Map, propertyName: string, @@ -111,12 +152,14 @@ export const updateContextWithDecorator = { const context = Reflect.getMetadata(className, target) as Map; const smartUiDescriptor = mapToSmartUiDescriptor(context); Reflect.defineMetadata(className, smartUiDescriptor, target); }; +/**@internal */ export const mapToSmartUiDescriptor = (context: Map): SelfServeDescriptor => { const inputNames: string[] = []; const root = context.get("root"); @@ -140,6 +183,7 @@ export const mapToSmartUiDescriptor = (context: Map return smartUiDescriptor; }; +/**@internal */ const addToDescriptor = ( context: Map, root: Node, @@ -158,11 +202,12 @@ const addToDescriptor = ( root.children.push(element); }; +/**@internal */ const getInput = (value: DecoratorProperties): AnyDisplay => { switch (value.type) { case "number": - if (!value.labelTKey || !value.step || !value.uiType || !value.min || !value.max) { - value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`; + if (!value.labelTKey || !value.uiType || !value.step || !value.max || value.min === undefined) { + value.errorMessage = `labelTkey, step, min, max and uiType are required for number input '${value.id}'.`; } return value as NumberInput; case "string": @@ -173,25 +218,18 @@ const getInput = (value: DecoratorProperties): AnyDisplay => { return value as DescriptionDisplay; } if (!value.labelTKey) { - value.errorMessage = `label is required for string input '${value.id}'.`; + value.errorMessage = `labelTKey is required for string input '${value.id}'.`; } return value as StringInput; case "boolean": if (!value.labelTKey || !value.trueLabelTKey || !value.falseLabelTKey) { - value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`; + value.errorMessage = `labelTkey, trueLabelTKey and falseLabelTKey are required for boolean input '${value.id}'.`; } return value as BooleanInput; default: if (!value.labelTKey || !value.choices) { - value.errorMessage = `label and choices are required for Choice input '${value.id}'.`; + value.errorMessage = `labelTKey and choices are required for Choice input '${value.id}'.`; } return value as ChoiceInput; } }; - -export const generateBladeLink = (blade: BladeType): string => { - const { subscriptionId, resourceGroup, databaseAccount } = userContext; - const databaseAccountName = databaseAccount.name; - - return `${document.referrer}#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`; -}; diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index b5a097fca..61080763e 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -1,10 +1,17 @@ -import { RefreshResult } from "../SelfServeTypes"; +import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; import { armRequestWithoutPolling } from "../../Utils/arm/request"; -import { configContext } from "../../ConfigContext"; -import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; +import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; +import { RefreshResult } from "../SelfServeTypes"; +import SqlX from "./SqlX"; +import { + FetchPricesResponse, + RegionsResponse, + SqlxServiceResource, + UpdateDedicatedGatewayRequestParameters, +} from "./SqlxTypes"; -const apiVersion = "2020-06-01-preview"; +const apiVersion = "2021-04-01-preview"; export enum ResourceStatus { Running = "Running", @@ -21,7 +28,7 @@ export interface DedicatedGatewayResponse { } export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => { - return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/sqlx`; + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/SqlDedicatedGateway`; }; export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise => { @@ -30,39 +37,69 @@ export const updateDedicatedGatewayResource = async (sku: string, instances: num properties: { instanceSize: sku, instanceCount: instances, - serviceType: "Sqlx", + serviceType: "SqlDedicatedGateway", }, }; - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "PUT", - apiVersion, - body, - }); - return armRequestResult.operationStatusUrl; + const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: SqlX.name }; + const updateTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "PUT", + apiVersion, + body, + }); + selfServeTraceSuccess(telemetryData, updateTimeStamp); + } catch (e) { + const failureTelemetry = { ...body, e, selfServeClassName: SqlX.name }; + selfServeTraceFailure(failureTelemetry, updateTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; }; export const deleteDedicatedGatewayResource = async (): Promise => { const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "DELETE", - apiVersion, - }); - return armRequestResult.operationStatusUrl; + const telemetryData = { httpMethod: "DELETE", selfServeClassName: SqlX.name }; + const deleteTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "DELETE", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, deleteTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: SqlX.name }; + selfServeTraceFailure(failureTelemetry, deleteTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; }; export const getDedicatedGatewayResource = async (): Promise => { const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "GET", - apiVersion, - }); - return armRequestResult.result; + const telemetryData = { httpMethod: "GET", selfServeClassName: SqlX.name }; + const getResourceTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "GET", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, getResourceTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: SqlX.name }; + selfServeTraceFailure(failureTelemetry, getResourceTimeStamp); + throw e; + } + return armRequestResult?.result; }; export const getCurrentProvisioningState = async (): Promise => { @@ -96,3 +133,67 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; +}; + +export const getReadRegions = async (): Promise> => { + try { + const readRegions = new Array(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name), + method: "GET", + apiVersion: "2021-04-01-preview", + }); + + if (response.result.location !== undefined) { + readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + } else { + for (const location of response.result.locations) { + readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + } + } + return readRegions; + } catch (err) { + return new Array(); + } +}; + +const getFetchPricesPathForRegion = (subscriptionId: string): string => { + return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; +}; + +export const getPriceMap = async (regions: Array): Promise>> => { + try { + const priceMap = new Map>(); + + for (const region of regions) { + const regionPriceMap = new Map(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getFetchPricesPathForRegion(userContext.subscriptionId), + method: "POST", + apiVersion: "2020-01-01-preview", + queryParams: { + filter: + "armRegionName eq '" + + region + + "' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'", + }, + }); + + for (const item of response.result.Items) { + regionPriceMap.set(item.skuName, item.retailPrice); + } + priceMap.set(region, regionPriceMap); + } + + return priceMap; + } catch (err) { + return undefined; + } +}; diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index a532c5244..ca1177fa3 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -1,9 +1,10 @@ -import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators"; +import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators"; import { selfServeTrace } from "../SelfServeTelemetryProcessor"; import { ChoiceItem, Description, DescriptionType, + Info, InputType, NumberUiType, OnSaveResult, @@ -15,15 +16,17 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils"; import { deleteDedicatedGatewayResource, getCurrentProvisioningState, + getPriceMap, + getReadRegions, refreshDedicatedGatewayProvisioning, updateDedicatedGatewayResource, } from "./SqlX.rp"; -const costPerHourValue: Description = { +const costPerHourDefaultValue: Description = { textTKey: "CostText", type: DescriptionType.Text, link: { - href: "https://azure.microsoft.com/en-us/pricing/details/cosmos-db/", + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", textTKey: "DedicatedGatewayPricing", }, }; @@ -37,41 +40,53 @@ const connectionStringValue: Description = { }, }; +const metricsStringValue: Description = { + textTKey: "MetricsText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.Metrics), + textTKey: "MetricsBlade", + }, +}; + const CosmosD4s = "Cosmos.D4s"; const CosmosD8s = "Cosmos.D8s"; const CosmosD16s = "Cosmos.D16s"; -const CosmosD32s = "Cosmos.D32s"; - -const getSKUDetails = (sku: string): string => { - if (sku === CosmosD4s) { - return "CosmosD4Details"; - } else if (sku === CosmosD8s) { - return "CosmosD8Details"; - } else if (sku === CosmosD16s) { - return "CosmosD16Details"; - } else if (sku === CosmosD32s) { - return "CosmosD32Details"; - } - return "Not Supported Yet"; -}; const onSKUChange = (newValue: InputType, currentValues: Map): Map => { currentValues.set("sku", { value: newValue }); - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${newValue.toString()}`), type: DescriptionType.Text } as Description, + currentValues.set("costPerHour", { + value: calculateCost(newValue as string, currentValues.get("instances").value as number), }); - currentValues.set("costPerHour", { value: costPerHourValue }); + return currentValues; }; const onNumberOfInstancesChange = ( newValue: InputType, - currentValues: Map + currentValues: Map, + baselineValues: Map ): Map => { currentValues.set("instances", { value: newValue }); - currentValues.set("warningBanner", { - value: { textTKey: "WarningBannerOnUpdate" } as Description, - hidden: false, + const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; + const baselineInstances = baselineValues.get("instances")?.value as number; + if (!dedicatedGatewayOriginallyEnabled || baselineInstances !== newValue) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", + textTKey: "DedicatedGatewayPricing", + }, + } as Description, + hidden: false, + }); + } else { + currentValues.set("warningBanner", undefined); + } + + currentValues.set("costPerHour", { + value: calculateCost(currentValues.get("sku").value as string, newValue as number), }); return currentValues; @@ -87,10 +102,10 @@ const onEnableDedicatedGatewayChange = ( if (dedicatedGatewayOriginallyEnabled === newValue) { currentValues.set("sku", baselineValues.get("sku")); currentValues.set("instances", baselineValues.get("instances")); - currentValues.set("skuDetails", baselineValues.get("skuDetails")); currentValues.set("costPerHour", baselineValues.get("costPerHour")); currentValues.set("warningBanner", baselineValues.get("warningBanner")); currentValues.set("connectionString", baselineValues.get("connectionString")); + currentValues.set("metricsString", baselineValues.get("metricsString")); return currentValues; } @@ -100,23 +115,30 @@ const onEnableDedicatedGatewayChange = ( value: { textTKey: "WarningBannerOnUpdate", link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", textTKey: "DedicatedGatewayPricing", }, } as Description, hidden: false, }); + + currentValues.set("costPerHour", { + value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number), + hidden: false, + }); } else { currentValues.set("warningBanner", { value: { textTKey: "WarningBannerOnDelete", link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", textTKey: "DeprovisioningDetailsText", }, } as Description, hidden: false, }); + + currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true }); } const sku = currentValues.get("sku"); const instances = currentValues.get("instances"); @@ -132,18 +154,16 @@ const onEnableDedicatedGatewayChange = ( disabled: dedicatedGatewayOriginallyEnabled, }); - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${currentValues.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: hideAttributes, - disabled: dedicatedGatewayOriginallyEnabled, - }); - - currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes }); currentValues.set("connectionString", { value: connectionStringValue, hidden: !newValue || !dedicatedGatewayOriginallyEnabled, }); + currentValues.set("metricsString", { + value: metricsStringValue, + hidden: !newValue || !dedicatedGatewayOriginallyEnabled, + }); + return currentValues; }; @@ -151,7 +171,6 @@ const skuDropDownItems: ChoiceItem[] = [ { labelTKey: "CosmosD4s", key: CosmosD4s }, { labelTKey: "CosmosD8s", key: CosmosD8s }, { labelTKey: "CosmosD16s", key: CosmosD16s }, - { labelTKey: "CosmosD32s", key: CosmosD32s }, ]; const getSkus = async (): Promise => { @@ -166,6 +185,48 @@ const getInstancesMax = async (): Promise => { return 5; }; +const NumberOfInstancesDropdownInfo: Info = { + messageTKey: "ResizingDecisionText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-size", + textTKey: "ResizingDecisionLink", + }, +}; + +const ApproximateCostDropDownInfo: Info = { + messageTKey: "CostText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", + textTKey: "DedicatedGatewayPricing", + }, +}; + +let priceMap: Map>; +let regions: Array; + +const calculateCost = (skuName: string, instanceCount: number): Description => { + try { + let costPerHour = 0; + for (const region of regions) { + const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", "")); + if (incrementalCost === undefined) { + throw new Error("Value not found in map."); + } + costPerHour += incrementalCost; + } + + costPerHour *= instanceCount; + costPerHour = Math.round(costPerHour * 100) / 100; + + return { + textTKey: `${costPerHour} USD`, + type: DescriptionType.Text, + }; + } catch (err) { + return costPerHourDefaultValue; + } +}; + @IsDisplayable() @RefreshOptions({ retryIntervalInMs: 20000 }) export default class SqlX extends SelfServeBaseClass { @@ -184,7 +245,6 @@ export default class SqlX extends SelfServeBaseClass { currentValues.set("warningBanner", undefined); - //TODO : Add try catch for each RP call and return relevant notifications if (dedicatedGatewayOriginallyEnabled) { if (!dedicatedGatewayCurrentlyEnabled) { const operationStatusUrl = await deleteDedicatedGatewayResource(); @@ -206,9 +266,11 @@ export default class SqlX extends SelfServeBaseClass { }, }; } else { - // Check for scaling up/down/in/out + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateDedicatedGatewayResource(sku, instances); return { - operationStatusUrl: undefined, + operationStatusUrl: operationStatusUrl, portalNotification: { initialize: { titleTKey: "UpdateInitializeTitle", @@ -255,26 +317,32 @@ export default class SqlX extends SelfServeBaseClass { defaults.set("enableDedicatedGateway", { value: false }); defaults.set("sku", { value: CosmosD4s, hidden: true }); defaults.set("instances", { value: await getInstancesMin(), hidden: true }); - defaults.set("skuDetails", undefined); defaults.set("costPerHour", undefined); defaults.set("connectionString", undefined); + defaults.set("metricsString", { + value: undefined, + hidden: true, + }); + + regions = await getReadRegions(); + priceMap = await getPriceMap(regions); const response = await getCurrentProvisioningState(); if (response.status && response.status !== "Deleting") { defaults.set("enableDedicatedGateway", { value: true }); defaults.set("sku", { value: response.sku, disabled: true }); - defaults.set("instances", { value: response.instances, disabled: true }); - defaults.set("costPerHour", { value: costPerHourValue }); - defaults.set("skuDetails", { - value: { textTKey: getSKUDetails(`${defaults.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: false, - }); + defaults.set("instances", { value: response.instances, disabled: false }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); defaults.set("connectionString", { value: connectionStringValue, hidden: false, }); - } + defaults.set("metricsString", { + value: metricsStringValue, + hidden: false, + }); + } defaults.set("warningBanner", undefined); return defaults; }; @@ -289,7 +357,7 @@ export default class SqlX extends SelfServeBaseClass { textTKey: "DedicatedGatewayDescription", type: DescriptionType.Text, link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", textTKey: "LearnAboutDedicatedGateway", }, }, @@ -312,13 +380,8 @@ export default class SqlX extends SelfServeBaseClass { }) sku: ChoiceItem; - @Values({ - labelTKey: "SKUDetails", - isDynamicDescription: true, - }) - skuDetails: string; - @OnChange(onNumberOfInstancesChange) + @PropertyInfo(NumberOfInstancesDropdownInfo) @Values({ labelTKey: "NumberOfInstances", min: getInstancesMin, @@ -328,8 +391,9 @@ export default class SqlX extends SelfServeBaseClass { }) instances: number; + @PropertyInfo(ApproximateCostDropDownInfo) @Values({ - labelTKey: "Cost", + labelTKey: "ApproximateCost", isDynamicDescription: true, }) costPerHour: string; @@ -339,4 +403,10 @@ export default class SqlX extends SelfServeBaseClass { isDynamicDescription: true, }) connectionString: string; + + @Values({ + labelTKey: "MonitorUsage", + description: metricsStringValue, + }) + metricsString: string; } diff --git a/src/SelfServe/SqlX/SqlxTypes.ts b/src/SelfServe/SqlX/SqlxTypes.ts index 70557f4f4..a150ccbb1 100644 --- a/src/SelfServe/SqlX/SqlxTypes.ts +++ b/src/SelfServe/SqlX/SqlxTypes.ts @@ -29,3 +29,23 @@ export type UpdateDedicatedGatewayRequestProperties = { instanceCount: number; serviceType: string; }; + +export type FetchPricesResponse = { + Items: Array; + NextPageLink: string | undefined; + Count: number; +}; + +export type PriceItem = { + retailPrice: number; + skuName: string; +}; + +export type RegionsResponse = { + locations: Array; + location: string; +}; + +export type RegionItem = { + locationName: string; +}; diff --git a/src/Shared/AddCollectionUtility.test.ts b/src/Shared/AddCollectionUtility.test.ts deleted file mode 100644 index 610784027..000000000 --- a/src/Shared/AddCollectionUtility.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as ko from "knockout"; -import { Collection, Database } from "../Contracts/ViewModels"; -import { getMaxThroughput } from "./AddCollectionUtility"; -import Explorer from "../Explorer/Explorer"; - -describe("getMaxThroughput", () => { - it("default unlimited throughput setting", () => { - const defaults = { - storage: "100", - throughput: { - fixed: 400, - unlimited: 400, - unlimitedmax: 1000000, - unlimitedmin: 400, - shared: 400, - }, - }; - - expect(getMaxThroughput(defaults, {} as Explorer)).toEqual(defaults.throughput.unlimited); - }); - - describe("no unlimited throughput setting", () => { - const defaults = { - storage: "100", - throughput: { - fixed: 400, - unlimited: { - collectionThreshold: 3, - lessThanOrEqualToThreshold: 400, - greatThanThreshold: 500, - }, - unlimitedmax: 1000000, - unlimitedmin: 400, - shared: 400, - }, - }; - - const mockCollection1 = { id: ko.observable("collection1") } as Collection; - const mockCollection2 = { id: ko.observable("collection2") } as Collection; - const mockCollection3 = { id: ko.observable("collection3") } as Collection; - const mockCollection4 = { id: ko.observable("collection4") } as Collection; - const mockDatabase = {} as Database; - const mockContainer = { - databases: ko.observableArray([mockDatabase]), - } as Explorer; - - it("less than or equal to collection threshold", () => { - mockDatabase.collections = ko.observableArray([mockCollection1, mockCollection2]); - expect(getMaxThroughput(defaults, mockContainer)).toEqual( - defaults.throughput.unlimited.lessThanOrEqualToThreshold - ); - }); - - it("exceeds collection threshold", () => { - mockDatabase.collections = ko.observableArray([ - mockCollection1, - mockCollection2, - mockCollection3, - mockCollection4, - ]); - expect(getMaxThroughput(defaults, mockContainer)).toEqual(defaults.throughput.unlimited.greatThanThreshold); - }); - }); -}); diff --git a/src/Shared/AddCollectionUtility.ts b/src/Shared/AddCollectionUtility.ts deleted file mode 100644 index c81319a62..000000000 --- a/src/Shared/AddCollectionUtility.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 b242cb67f..97a9349c5 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -187,42 +187,6 @@ export const CollectionCreationDefaults = { }, } 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", @@ -237,3 +201,8 @@ export class SubscriptionUtilMappings { export class AutopilotDocumentation { public static Url: string = "https://aka.ms/cosmos-autoscale-info"; } + +export class FreeTierLimits { + public static RU: number = 1000; + public static Storage: number = 25; +} diff --git a/src/Shared/DefaultExperienceUtility.test.ts b/src/Shared/DefaultExperienceUtility.test.ts index c72b46823..2d23af40a 100644 --- a/src/Shared/DefaultExperienceUtility.test.ts +++ b/src/Shared/DefaultExperienceUtility.test.ts @@ -35,7 +35,7 @@ describe("Default Experience Utility", () => { }); describe("getApiKindFromDefaultExperience()", () => { - function runScenario(defaultExperience: typeof userContext.apiType, expectedApiKind: number): void { + function runScenario(defaultExperience: typeof userContext.apiType | null, expectedApiKind: number): void { const resolvedApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience); expect(resolvedApiKind).toEqual(expectedApiKind); } diff --git a/src/Shared/ExplorerSettings.ts b/src/Shared/ExplorerSettings.ts index b697dac83..3c6f9e3d2 100644 --- a/src/Shared/ExplorerSettings.ts +++ b/src/Shared/ExplorerSettings.ts @@ -1,22 +1,17 @@ import * as Constants from "../Common/Constants"; import { LocalStorageUtility, StorageKey } from "./StorageUtility"; -export class ExplorerSettings { - public static createDefaultSettings() { - LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, Constants.Queries.itemsPerPage); - LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage); - LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); - LocalStorageUtility.setEntryNumber( - StorageKey.MaxDegreeOfParellism, - Constants.Queries.DefaultMaxDegreeOfParallelism - ); - } +export const createDefaultSettings = () => { + LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, Constants.Queries.itemsPerPage); + LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage); + LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); + LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism); +}; - public static hasSettingsDefined(): boolean { - return ( - LocalStorageUtility.hasItem(StorageKey.ActualItemPerPage) && - LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled) && - LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism) - ); - } -} +export const hasSettingsDefined = (): boolean => { + return ( + LocalStorageUtility.hasItem(StorageKey.ActualItemPerPage) && + LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled) && + LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism) + ); +}; diff --git a/src/Shared/LocalStorageUtility.ts b/src/Shared/LocalStorageUtility.ts new file mode 100644 index 000000000..9fc2f4f7c --- /dev/null +++ b/src/Shared/LocalStorageUtility.ts @@ -0,0 +1,22 @@ +import { StorageKey } from "./StorageUtility"; +import * as StringUtility from "./StringUtility"; + +export const hasItem = (key: StorageKey): boolean => !!localStorage.getItem(StorageKey[key]); + +export const getEntryString = (key: StorageKey): string | null => localStorage.getItem(StorageKey[key]); + +export const getEntryNumber = (key: StorageKey): number => + StringUtility.toNumber(localStorage.getItem(StorageKey[key])); + +export const getEntryBoolean = (key: StorageKey): boolean => + StringUtility.toBoolean(localStorage.getItem(StorageKey[key])); + +export const setEntryString = (key: StorageKey, value: string): void => localStorage.setItem(StorageKey[key], value); + +export const removeEntry = (key: StorageKey): void => localStorage.removeItem(StorageKey[key]); + +export const setEntryNumber = (key: StorageKey, value: number): void => + localStorage.setItem(StorageKey[key], value.toString()); + +export const setEntryBoolean = (key: StorageKey, value: boolean): void => + localStorage.setItem(StorageKey[key], value.toString()); diff --git a/src/Shared/PriceEstimateCalculator.ts b/src/Shared/PriceEstimateCalculator.ts index 2e897ea66..a7f08d230 100644 --- a/src/Shared/PriceEstimateCalculator.ts +++ b/src/Shared/PriceEstimateCalculator.ts @@ -2,26 +2,26 @@ 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; + const 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; + const 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; + const 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; + const 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, + const usageInMB = usageInKB / 1024, usageInGB = usageInMB / 1024, displayUsageString = usageInGB > 0.1 @@ -33,7 +33,7 @@ export function computeDisplayUsageString(usageInKB: number): string { } export function usageInGB(usageInKB: number): number { - let usageInMB = usageInKB / 1024, + const usageInMB = usageInKB / 1024, usageInGB = usageInMB / 1024; return Math.ceil(usageInGB); } diff --git a/src/Shared/SessionStorageUtility.ts b/src/Shared/SessionStorageUtility.ts new file mode 100644 index 000000000..f3fb4abae --- /dev/null +++ b/src/Shared/SessionStorageUtility.ts @@ -0,0 +1,20 @@ +import { StorageKey } from "./StorageUtility"; +import * as StringUtility from "./StringUtility"; + +export const hasItem = (key: StorageKey): boolean => !!sessionStorage.getItem(StorageKey[key]); + +export const getEntryString = (key: StorageKey): string | null => sessionStorage.getItem(StorageKey[key]); + +export const getEntryNumber = (key: StorageKey): number => + StringUtility.toNumber(sessionStorage.getItem(StorageKey[key])); + +export const getEntry = (key: string): string | null => sessionStorage.getItem(key); + +export const removeEntry = (key: StorageKey): void => sessionStorage.removeItem(StorageKey[key]); + +export const setEntryString = (key: StorageKey, value: string): void => sessionStorage.setItem(StorageKey[key], value); + +export const setEntry = (key: string, value: string): void => sessionStorage.setItem(key, value); + +export const setEntryNumber = (key: StorageKey, value: number): void => + sessionStorage.setItem(StorageKey[key], value.toString()); diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index adf271cec..9c16467d2 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -1,73 +1,7 @@ -import * as StringUtility from "./StringUtility"; - -export class LocalStorageUtility { - public static hasItem(key: StorageKey): boolean { - return !!localStorage.getItem(StorageKey[key]); - } - - public static getEntryString(key: StorageKey): string | null { - return localStorage.getItem(StorageKey[key]); - } - - public static getEntryNumber(key: StorageKey): number { - return StringUtility.toNumber(localStorage.getItem(StorageKey[key])); - } - - public static getEntryBoolean(key: StorageKey): boolean { - return StringUtility.toBoolean(localStorage.getItem(StorageKey[key])); - } - - public static setEntryString(key: StorageKey, value: string): void { - localStorage.setItem(StorageKey[key], value); - } - - public static removeEntry(key: StorageKey): void { - return localStorage.removeItem(StorageKey[key]); - } - - public static setEntryNumber(key: StorageKey, value: number): void { - localStorage.setItem(StorageKey[key], value.toString()); - } - - public static setEntryBoolean(key: StorageKey, value: boolean): void { - localStorage.setItem(StorageKey[key], value.toString()); - } -} - -export class SessionStorageUtility { - public static hasItem(key: StorageKey): boolean { - return !!sessionStorage.getItem(StorageKey[key]); - } - - public static getEntryString(key: StorageKey): string | null { - return sessionStorage.getItem(StorageKey[key]); - } - - public static getEntryNumber(key: StorageKey): number { - return StringUtility.toNumber(sessionStorage.getItem(StorageKey[key])); - } - - public static getEntry(key: string): string | null { - return sessionStorage.getItem(key); - } - - public static removeEntry(key: StorageKey): void { - return sessionStorage.removeItem(StorageKey[key]); - } - - public static setEntryString(key: StorageKey, value: string): void { - sessionStorage.setItem(StorageKey[key], value); - } - - public static setEntry(key: string, value: string): void { - sessionStorage.setItem(key, value); - } - - public static setEntryNumber(key: StorageKey, value: number): void { - sessionStorage.setItem(StorageKey[key], value.toString()); - } -} +import * as LocalStorageUtility from "./LocalStorageUtility"; +import * as SessionStorageUtility from "./SessionStorageUtility"; +export { LocalStorageUtility, SessionStorageUtility }; export enum StorageKey { ActualItemPerPage, CustomItemPerPage, diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 058937df9..134340ad4 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -117,6 +117,7 @@ export enum Action { SelfServe, ExpandAddCollectionPaneAdvancedSection, SchemaAnalyzerClickAnalyze, + SelfServeComponent, } export const ActionModifiers = { diff --git a/src/SparkClusterManager/ArcadiaResourceManager.ts b/src/SparkClusterManager/ArcadiaResourceManager.ts deleted file mode 100644 index cbfa68a58..000000000 --- a/src/SparkClusterManager/ArcadiaResourceManager.ts +++ /dev/null @@ -1,78 +0,0 @@ -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/TerminalProps.ts b/src/Terminal/TerminalProps.ts new file mode 100644 index 000000000..4fe3c539c --- /dev/null +++ b/src/Terminal/TerminalProps.ts @@ -0,0 +1,13 @@ +import { AuthType } from "../AuthType"; +import * as DataModels from "../Contracts/DataModels"; +import { ApiType } from "../UserContext"; + +export interface TerminalProps { + authToken: string; + notebookServerEndpoint: string; + terminalEndpoint: string; + databaseAccount: DataModels.DatabaseAccount; + authType: AuthType; + apiType: ApiType; + subscriptionId: string; +} diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index eb272a1a5..dae3059b5 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -1,43 +1,36 @@ -import "@jupyterlab/terminal/style/index.css"; -import "./index.css"; import { ServerConnection } from "@jupyterlab/services"; -import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; +import "@jupyterlab/terminal/style/index.css"; +import postRobot from "post-robot"; +import { HttpHeaders } from "../Common/Constants"; import { Action } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../UserContext"; -import { HttpHeaders, TerminalQueryParams } from "../Common/Constants"; +import "./index.css"; +import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; +import { TerminalProps } from "./TerminalProps"; -const getUrlVars = (): { [key: string]: string } => { - const vars: { [key: string]: string } = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (_m, key, value): string => { - vars[key] = decodeURIComponent(value); - return value; - }); - return vars; -}; - -const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => { +const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => { let body: BodyInit | undefined; let headers: HeadersInit | undefined; - if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) { + if (props.terminalEndpoint) { body = JSON.stringify({ - endpoint: urlVars[TerminalQueryParams.TerminalEndpoint], + endpoint: props.terminalEndpoint, }); headers = { [HttpHeaders.contentType]: "application/json", }; } - const server = urlVars[TerminalQueryParams.Server]; + const server = props.notebookServerEndpoint; let options: Partial = { baseUrl: server, init: { body, headers }, fetch: window.parent.fetch, }; - if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) { + if (props.authToken) { options = { baseUrl: server, - token: urlVars[TerminalQueryParams.Token], + token: props.authToken, appendToken: true, init: { body, headers }, fetch: window.parent.fetch, @@ -47,30 +40,41 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect return ServerConnection.makeSettings(options); }; -const main = async (): Promise => { - const urlVars = getUrlVars(); - - // Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor +const initTerminal = async (props: TerminalProps) => { + // Initialize userContext (only properties which are needed by TelemetryProcessor) updateUserContext({ - subscriptionId: urlVars[TerminalQueryParams.SubscriptionId], + subscriptionId: props.subscriptionId, + apiType: props.apiType, + authType: props.authType, + databaseAccount: props.databaseAccount, }); - const serverSettings = createServerSettings(urlVars); - + const serverSettings = createServerSettings(props); const data = { baseUrl: serverSettings.baseUrl }; const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); try { - if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) { - await JupyterLabAppFactory.createTerminalApp(serverSettings); - } else { - throw new Error("Only terminal is supported"); - } - + await JupyterLabAppFactory.createTerminalApp(serverSettings); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); } catch (error) { TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); } }; +const main = async (): Promise => { + postRobot.on( + "props", + { + window: window.parent, + domain: window.location.origin, + }, + async (event) => { + // Typescript definition for event is wrong. So read props by casting to + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const props = (event as any).data as TerminalProps; + await initTerminal(props); + } + ); +}; + window.addEventListener("load", main); diff --git a/src/TokenProviders/PortalTokenProvider.ts b/src/TokenProviders/PortalTokenProvider.ts deleted file mode 100644 index c7b1480ad..000000000 --- a/src/TokenProviders/PortalTokenProvider.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 342661c3b..000000000 --- a/src/TokenProviders/TokenProviderFactory.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/UserContext.ts b/src/UserContext.ts index 0cbec9226..96bfc2ecf 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -2,7 +2,25 @@ import { AuthType } from "./AuthType"; import { DatabaseAccount } from "./Contracts/DataModels"; import { SubscriptionType } from "./Contracts/SubscriptionType"; import { extractFeatures, Features } from "./Platform/Hosted/extractFeatures"; -import { CollectionCreation } from "./Shared/Constants"; +import { CollectionCreation, CollectionCreationDefaults } from "./Shared/Constants"; + +interface ThroughputDefaults { + fixed: number; + unlimited: + | number + | { + collectionThreshold: number; + lessThanOrEqualToThreshold: number; + greatThanThreshold: number; + }; + unlimitedmax: number; + unlimitedmin: number; + shared: number; +} +export interface CollectionCreationDefaults { + storage: string; + throughput: ThroughputDefaults; +} interface UserContext { readonly authType?: AuthType; @@ -11,6 +29,7 @@ interface UserContext { readonly resourceGroup?: string; readonly databaseAccount?: DatabaseAccount; readonly endpoint?: string; + readonly aadToken?: string; readonly accessToken?: string; readonly authorizationToken?: string; readonly resourceToken?: string; @@ -25,6 +44,12 @@ interface UserContext { readonly features: Features; readonly addCollectionFlight: string; readonly hasWriteAccess: boolean; + readonly parsedResourceToken?: { + databaseId: string; + collectionId: string; + partitionKey?: string; + }; + collectionCreationDefaults: CollectionCreationDefaults; } export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra"; @@ -42,6 +67,7 @@ const userContext: UserContext = { useSDKOperations, addCollectionFlight: CollectionCreation.DefaultAddCollectionDefaultFlight, subscriptionType: CollectionCreation.DefaultSubscriptionType, + collectionCreationDefaults: CollectionCreationDefaults, }; function updateUserContext(newContext: Partial): void { diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 16d2082de..0da7e310f 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -1,6 +1,8 @@ +import * as msal from "@azure/msal-browser"; import { AuthType } from "../AuthType"; import * as Constants from "../Common/Constants"; import * as Logger from "../Common/Logger"; +import { configContext } from "../ConfigContext"; import * as ViewModels from "../Contracts/ViewModels"; import { userContext } from "../UserContext"; @@ -40,3 +42,21 @@ export function decryptJWTToken(token: string) { return JSON.parse(tokenPayload); } + +export function getMsalInstance() { + const config: msal.Configuration = { + cache: { + cacheLocation: "localStorage", + }, + auth: { + authority: `${configContext.AAD_ENDPOINT}common`, + clientId: "203f1145-856a-4232-83d4-a43568fba23d", + }, + }; + + if (process.env.NODE_ENV === "development") { + config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; + } + const msalInstance = new msal.PublicClientApplication(config); + return msalInstance; +} diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index 040766219..fc2de8149 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -1,7 +1,12 @@ import * as Constants from "../Common/Constants"; import { userContext } from "../UserContext"; -export const isCapabilityEnabled = (capabilityName: string): boolean => - userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName); +export const isCapabilityEnabled = (capabilityName: string): boolean => { + const { databaseAccount } = userContext; + if (databaseAccount && databaseAccount.properties && databaseAccount.properties.capabilities) { + return databaseAccount.properties.capabilities.some((capability) => capability.name === capabilityName); + } + return false; +}; export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless); diff --git a/src/Utils/CloudUtils.ts b/src/Utils/CloudUtils.ts new file mode 100644 index 000000000..089bbf0b6 --- /dev/null +++ b/src/Utils/CloudUtils.ts @@ -0,0 +1,9 @@ +import { userContext } from "../UserContext"; + +export function isRunningOnNationalCloud(): boolean { + return ( + userContext.portalEnv === "blackforest" || + userContext.portalEnv === "fairfax" || + userContext.portalEnv === "mooncake" + ); +} diff --git a/src/Utils/NotebookConfigurationUtils.ts b/src/Utils/NotebookConfigurationUtils.ts index 2159a9162..9151b6f40 100644 --- a/src/Utils/NotebookConfigurationUtils.ts +++ b/src/Utils/NotebookConfigurationUtils.ts @@ -1,6 +1,6 @@ -import * as DataModels from "../Contracts/DataModels"; -import * as Logger from "../Common/Logger"; import { getErrorMessage } from "../Common/ErrorHandlingUtils"; +import * as Logger from "../Common/Logger"; +import * as DataModels from "../Contracts/DataModels"; interface KernelConnectionMetadata { name: string; @@ -64,14 +64,13 @@ export const configureServiceEndpoints = async ( 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(), + token: "", // TODO. This was arcadiaToken() when our synapse/spark integration comes back }) ); const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = { diff --git a/src/Utils/NotificationConsoleUtils.ts b/src/Utils/NotificationConsoleUtils.ts index b661d7d78..b3c63e5a7 100644 --- a/src/Utils/NotificationConsoleUtils.ts +++ b/src/Utils/NotificationConsoleUtils.ts @@ -1,23 +1,17 @@ import * as _ from "underscore"; -import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; - -const _global = typeof self === "undefined" ? window : self; +import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/ConsoleData"; +import { useNotificationConsole } from "../hooks/useNotificationConsole"; function log(type: ConsoleDataType, message: string): () => void { - const dataExplorer = _global.dataExplorer; - if (dataExplorer) { - const id = _.uniqueId(); - const date = new Intl.DateTimeFormat("en-EN", { - hour12: true, - hour: "numeric", - minute: "numeric", - }).format(new Date()); + const id = _.uniqueId(); + const date = new Intl.DateTimeFormat("en-EN", { + hour12: true, + hour: "numeric", + minute: "numeric", + }).format(new Date()); - dataExplorer.logConsoleData({ type, date, message, id }); - return () => dataExplorer.deleteInProgressConsoleDataWithId(id); - } - - return () => undefined; + useNotificationConsole.getState().setNotificationConsoleData({ type, date, message, id }); + return () => useNotificationConsole.getState().setInProgressConsoleDataIdToBeDeleted(id); } export const logConsoleProgress = (msg: string): (() => void) => log(ConsoleDataType.InProgress, msg); diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index 582c40a37..de4efa90c 100644 --- a/src/Utils/PricingUtils.test.ts +++ b/src/Utils/PricingUtils.test.ts @@ -18,7 +18,7 @@ describe("PricingUtils Tests", () => { }); it("should return false if passed number is not number", () => { - const value = PricingUtils.isLargerThanDefaultMinRU(null); + const value = PricingUtils.isLargerThanDefaultMinRU(undefined); expect(value).toBe(false); }); }); @@ -28,7 +28,7 @@ describe("PricingUtils Tests", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, - numberOfRegions: null, + numberOfRegions: undefined, multimasterEnabled: false, isAutoscale: false, }); @@ -38,7 +38,7 @@ describe("PricingUtils Tests", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, - numberOfRegions: null, + numberOfRegions: undefined, multimasterEnabled: false, isAutoscale: true, }); @@ -264,11 +264,6 @@ describe("PricingUtils Tests", () => { describe("getRegionMultiplier()", () => { describe("without multimaster", () => { - it("should return 0 for null", () => { - const value = PricingUtils.getRegionMultiplier(null, false); - expect(value).toBe(0); - }); - it("should return 0 for undefined", () => { const value = PricingUtils.getRegionMultiplier(undefined, false); expect(value).toBe(0); @@ -296,11 +291,6 @@ describe("PricingUtils Tests", () => { }); describe("with multimaster", () => { - it("should return 0 for null", () => { - const value = PricingUtils.getRegionMultiplier(null, true); - expect(value).toBe(0); - }); - it("should return 0 for undefined", () => { const value = PricingUtils.getRegionMultiplier(undefined, true); expect(value).toBe(0); @@ -450,11 +440,6 @@ describe("PricingUtils Tests", () => { }); describe("normalizeNumberOfRegions()", () => { - it("should return 0 for null", () => { - const value = PricingUtils.normalizeNumber(null); - expect(value).toBe(0); - }); - it("should return 0 for undefined", () => { const value = PricingUtils.normalizeNumber(undefined); expect(value).toBe(0); diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index a3b75e2e7..09b16373e 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -267,9 +267,11 @@ export function getUpsellMessage( if (isFreeTier) { const collectionName = getCollectionName().toLocaleLowerCase(); const resourceType = isCollection ? collectionName : "database"; + const freeTierMaxRU = Constants.FreeTierLimits.RU; + const freeTierMaxStorage = Constants.FreeTierLimits.Storage; return isFirstResourceCreated - ? `The free tier discount of 400 RU/s has already been applied to a database or ${collectionName} in this account. Billing will apply to this ${resourceType} after it is created.` - : `With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Billing will apply if you provision more than 400 RU/s of manual throughput, or if the ${resourceType} scales beyond 400 RU/s with autoscale.`; + ? `Your account currently has at least 1 database or ${collectionName} with provisioned RU/s. Billing will apply to this ${resourceType} if the total RU/s in your account exceeds ${freeTierMaxRU} RU/s.` + : `With free tier, you'll get the first ${freeTierMaxRU} RU/s and ${freeTierMaxStorage} GB of storage in this account for free. Billing will apply if you provision more than ${freeTierMaxRU} RU/s of manual throughput, or if the ${resourceType} scales beyond ${freeTierMaxRU} RU/s with autoscale.`; } else { let price: number = Constants.OfferPricing.MonthlyPricing.default.Standard.StartingPrice; diff --git a/src/Utils/QueryUtils.test.ts b/src/Utils/QueryUtils.test.ts index 76e525a06..72a568a3a 100644 --- a/src/Utils/QueryUtils.test.ts +++ b/src/Utils/QueryUtils.test.ts @@ -1,27 +1,23 @@ -import * as DataModels from "../Contracts/DataModels"; import * as Q from "q"; import * as sinon from "sinon"; +import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import * as QueryUtils from "./QueryUtils"; describe("Query Utils", () => { - function generatePartitionKeyForPath(path: string): DataModels.PartitionKey { + const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => { return { paths: [path], - kind: "hash", + 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); @@ -91,97 +87,11 @@ describe("Query Utils", () => { expect(queryStub.getCall(1).args[0]).toBe(0); }); - it("should not perform multiple queries if the first page of results has items", (done) => { + it("should not perform multiple queries if the first page of results has items", async () => { 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(); - }); + await QueryUtils.queryPagesUntilContentPresent(0, queryStub); + expect(queryStub.callCount).toBe(1); + expect(queryStub.getCall(0).args[0]).toBe(0); }); }); }); diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index b652df70f..71d8a57a0 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -81,34 +81,3 @@ export const queryPagesUntilContentPresent = async ( return await doRequest(firstItemIndex); }; - -export const queryAllPages = async ( - 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 = 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 f725e9428..bd626f492 100644 --- a/src/Utils/StringUtils.test.ts +++ b/src/Utils/StringUtils.test.ts @@ -3,27 +3,27 @@ import * as StringUtils from "./StringUtils"; describe("StringUtils", () => { describe("stripSpacesFromString()", () => { it("should strip all spaces from input string", () => { - const transformedString: string = StringUtils.stripSpacesFromString("a b c"); + const transformedString: string | undefined = 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"); + const transformedString: string | undefined = StringUtils.stripSpacesFromString("abc"); expect(transformedString).toBe("abc"); }); it("should return undefined if input is undefined", () => { - const transformedString: string = StringUtils.stripSpacesFromString(undefined); + const transformedString: string | undefined = StringUtils.stripSpacesFromString(undefined); expect(transformedString).toBeUndefined(); }); it("should return undefined if input is undefiend", () => { - const transformedString: string = StringUtils.stripSpacesFromString(undefined); + const transformedString: string | undefined = StringUtils.stripSpacesFromString(undefined); expect(transformedString).toBe(undefined); }); it("should return empty string if input is an empty string", () => { - const transformedString: string = StringUtils.stripSpacesFromString(""); + const transformedString: string | undefined = StringUtils.stripSpacesFromString(""); expect(transformedString).toBe(""); }); }); diff --git a/src/Utils/StringUtils.ts b/src/Utils/StringUtils.ts index 2618544c3..02ceba5f2 100644 --- a/src/Utils/StringUtils.ts +++ b/src/Utils/StringUtils.ts @@ -1,4 +1,4 @@ -export function stripSpacesFromString(inputString: string): string { +export function stripSpacesFromString(inputString?: string): string | undefined { if (inputString === undefined || typeof inputString !== "string") { return inputString; } diff --git a/src/Utils/arm/generatedClients/2020-04-01/cassandraResources.ts b/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts similarity index 94% rename from src/Utils/arm/generatedClients/2020-04-01/cassandraResources.ts rename to src/Utils/arm/generatedClients/cosmos/cassandraResources.ts index 924f30bb6..d843d4c15 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/cassandraResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */ export async function listCassandraKeyspaces( @@ -82,7 +84,7 @@ export async function migrateCassandraKeyspaceToAutoscale( resourceGroupName: string, accountName: string, keyspaceName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -93,7 +95,7 @@ export async function migrateCassandraKeyspaceToManualThroughput( resourceGroupName: string, accountName: string, keyspaceName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -178,7 +180,7 @@ export async function migrateCassandraTableToAutoscale( accountName: string, keyspaceName: string, tableName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}/tables/${tableName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -190,7 +192,7 @@ export async function migrateCassandraTableToManualThroughput( accountName: string, keyspaceName: string, tableName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}/tables/${tableName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } diff --git a/src/Utils/arm/generatedClients/2020-04-01/collection.ts b/src/Utils/arm/generatedClients/cosmos/collection.ts similarity index 86% rename from src/Utils/arm/generatedClients/2020-04-01/collection.ts rename to src/Utils/arm/generatedClients/cosmos/collection.ts index 307cd91f6..ab92b2219 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/collection.ts +++ b/src/Utils/arm/generatedClients/cosmos/collection.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given database account and collection. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/collectionPartition.ts b/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts similarity index 82% rename from src/Utils/arm/generatedClients/2020-04-01/collectionPartition.ts rename to src/Utils/arm/generatedClients/cosmos/collectionPartition.ts index d0dad687c..400502e7a 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/collectionPartition.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given collection, split by partition. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/collectionPartitionRegion.ts b/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts similarity index 73% rename from src/Utils/arm/generatedClients/2020-04-01/collectionPartitionRegion.ts rename to src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts index 8b49edc80..4ef525b35 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/collectionPartitionRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/collectionRegion.ts b/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts similarity index 73% rename from src/Utils/arm/generatedClients/2020-04-01/collectionRegion.ts rename to src/Utils/arm/generatedClients/cosmos/collectionRegion.ts index 2c149656e..9669d8b1f 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/collectionRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given database account, collection and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/database.ts b/src/Utils/arm/generatedClients/cosmos/database.ts similarity index 85% rename from src/Utils/arm/generatedClients/2020-04-01/database.ts rename to src/Utils/arm/generatedClients/cosmos/database.ts index eeec5ce07..1bf4cf8c5 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/database.ts +++ b/src/Utils/arm/generatedClients/cosmos/database.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given database account and database. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/databaseAccountRegion.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts similarity index 70% rename from src/Utils/arm/generatedClients/2020-04-01/databaseAccountRegion.ts rename to src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts index 1dbb7159b..ca69022b0 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/databaseAccountRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given database account and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/databaseAccounts.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts similarity index 96% rename from src/Utils/arm/generatedClients/2020-04-01/databaseAccounts.ts rename to src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts index 499213664..87b00b24b 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/databaseAccounts.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the properties of an existing Azure Cosmos DB database account. */ export async function get( diff --git a/src/Utils/arm/generatedClients/2020-04-01/gremlinResources.ts b/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts similarity index 94% rename from src/Utils/arm/generatedClients/2020-04-01/gremlinResources.ts rename to src/Utils/arm/generatedClients/cosmos/gremlinResources.ts index 26a1fa5b2..572ef4859 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/gremlinResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */ export async function listGremlinDatabases( @@ -82,7 +84,7 @@ export async function migrateGremlinDatabaseToAutoscale( resourceGroupName: string, accountName: string, databaseName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -93,7 +95,7 @@ export async function migrateGremlinDatabaseToManualThroughput( resourceGroupName: string, accountName: string, databaseName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -178,7 +180,7 @@ export async function migrateGremlinGraphToAutoscale( accountName: string, databaseName: string, graphName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/graphs/${graphName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -190,7 +192,7 @@ export async function migrateGremlinGraphToManualThroughput( accountName: string, databaseName: string, graphName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/graphs/${graphName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } diff --git a/src/Utils/arm/generatedClients/2020-04-01/mongoDBResources.ts b/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts similarity index 93% rename from src/Utils/arm/generatedClients/2020-04-01/mongoDBResources.ts rename to src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts index e3a3e9439..61f2ee5ae 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/mongoDBResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */ export async function listMongoDBDatabases( @@ -71,7 +73,7 @@ export async function updateMongoDBDatabaseThroughput( accountName: string, databaseName: string, body: Types.ThroughputSettingsUpdateParameters -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/throughputSettings/default`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); } @@ -82,7 +84,7 @@ export async function migrateMongoDBDatabaseToAutoscale( resourceGroupName: string, accountName: string, databaseName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -93,7 +95,7 @@ export async function migrateMongoDBDatabaseToManualThroughput( resourceGroupName: string, accountName: string, databaseName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -178,7 +180,7 @@ export async function migrateMongoDBCollectionToAutoscale( accountName: string, databaseName: string, collectionName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/collections/${collectionName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -190,7 +192,7 @@ export async function migrateMongoDBCollectionToManualThroughput( accountName: string, databaseName: string, collectionName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/collections/${collectionName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } diff --git a/src/Utils/arm/generatedClients/2020-04-01/operations.ts b/src/Utils/arm/generatedClients/cosmos/operations.ts similarity index 61% rename from src/Utils/arm/generatedClients/2020-04-01/operations.ts rename to src/Utils/arm/generatedClients/cosmos/operations.ts index 31de2d66e..db16f7bfc 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/operations.ts +++ b/src/Utils/arm/generatedClients/cosmos/operations.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Lists all of the available Cosmos DB Resource Provider operations. */ export async function list(): Promise { diff --git a/src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeId.ts b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts similarity index 73% rename from src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeId.ts rename to src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts index 4b6a64d64..20c85cdd2 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeId.ts +++ b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given partition key range id. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeIdRegion.ts b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts similarity index 74% rename from src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeIdRegion.ts rename to src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts index 7655610ef..eb8a00b9b 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeIdRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given partition key range id and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/percentile.ts b/src/Utils/arm/generatedClients/cosmos/percentile.ts similarity index 71% rename from src/Utils/arm/generatedClients/2020-04-01/percentile.ts rename to src/Utils/arm/generatedClients/cosmos/percentile.ts index 946b35219..f30a503fd 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/percentile.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentile.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/percentileSourceTarget.ts b/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts similarity index 74% rename from src/Utils/arm/generatedClients/2020-04-01/percentileSourceTarget.ts rename to src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts index df90f5ac7..37a3f841c 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/percentileSourceTarget.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/percentileTarget.ts b/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts similarity index 72% rename from src/Utils/arm/generatedClients/2020-04-01/percentileTarget.ts rename to src/Utils/arm/generatedClients/cosmos/percentileTarget.ts index f729c4a92..a318d5dd1 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/percentileTarget.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/2020-04-01/sqlResources.ts b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts similarity index 96% rename from src/Utils/arm/generatedClients/2020-04-01/sqlResources.ts rename to src/Utils/arm/generatedClients/cosmos/sqlResources.ts index 7755731c2..40c68fb86 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/sqlResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Lists the SQL databases under an existing Azure Cosmos DB database account. */ export async function listSqlDatabases( @@ -82,7 +84,7 @@ export async function migrateSqlDatabaseToAutoscale( resourceGroupName: string, accountName: string, databaseName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -93,7 +95,7 @@ export async function migrateSqlDatabaseToManualThroughput( resourceGroupName: string, accountName: string, databaseName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -178,7 +180,7 @@ export async function migrateSqlContainerToAutoscale( accountName: string, databaseName: string, containerName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -190,7 +192,7 @@ export async function migrateSqlContainerToManualThroughput( accountName: string, databaseName: string, containerName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } diff --git a/src/Utils/arm/generatedClients/2020-04-01/tableResources.ts b/src/Utils/arm/generatedClients/cosmos/tableResources.ts similarity index 90% rename from src/Utils/arm/generatedClients/2020-04-01/tableResources.ts rename to src/Utils/arm/generatedClients/cosmos/tableResources.ts index ede7fbcb4..b68c20b08 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/tableResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/tableResources.ts @@ -1,13 +1,15 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2020-04-01"; +const apiVersion = "2021-04-15"; /* Lists the Tables under an existing Azure Cosmos DB database account. */ export async function listTables( @@ -82,7 +84,7 @@ export async function migrateTableToAutoscale( resourceGroupName: string, accountName: string, tableName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/tables/${tableName}/throughputSettings/default/migrateToAutoscale`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } @@ -93,7 +95,7 @@ export async function migrateTableToManualThroughput( resourceGroupName: string, accountName: string, tableName: string -): Promise { +): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/tables/${tableName}/throughputSettings/default/migrateToManualThroughput`; return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); } diff --git a/src/Utils/arm/generatedClients/2020-04-01/types.ts b/src/Utils/arm/generatedClients/cosmos/types.ts similarity index 89% rename from src/Utils/arm/generatedClients/2020-04-01/types.ts rename to src/Utils/arm/generatedClients/cosmos/types.ts index 97eebbf13..9efc80d20 100644 --- a/src/Utils/arm/generatedClients/2020-04-01/types.ts +++ b/src/Utils/arm/generatedClients/cosmos/types.ts @@ -1,7 +1,9 @@ /* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json */ /* The List operation response, that contains the database accounts and their properties. */ @@ -90,6 +92,12 @@ export interface ErrorResponse { message?: string; } +/* An error response from the service. */ +export interface CloudError { + /* undocumented */ + error?: ErrorResponse; +} + /* The list of new failover policies for the failover priority change. */ export interface FailoverPolicies { /* List of failover policies. */ @@ -156,22 +164,23 @@ export interface ARMProxyResource { /* An Azure Cosmos DB database account. */ export type DatabaseAccountGetResults = ARMResourceProperties & { /* Indicates the type of database account. This can only be set at database account creation. */ - kind?: string; + kind?: "GlobalDocumentDB" | "MongoDB" | "Parse"; + + /* undocumented */ + identity?: ManagedServiceIdentity; + /* undocumented */ properties?: DatabaseAccountGetProperties; }; /* The system generated resource properties associated with SQL databases, SQL containers, Gremlin databases and Gremlin graphs. */ -// TODO: ExtendedResourceProperties was missing some properties such as _self which was manually added. Need to fix this in the RP spec. export interface ExtendedResourceProperties { /* A system generated property. A unique identifier. */ readonly _rid?: string; /* A system generated property that denotes the last updated timestamp of the resource. */ - readonly _ts?: unknown; + readonly _ts?: number; /* A system generated property representing the resource etag required for optimistic concurrency control. */ - readonly _etag: string; - // TODO: This property was manually added. It should be auto-generated like the other properties. - readonly _self: string; + readonly _etag?: string; } /* An Azure Cosmos DB resource throughput. */ @@ -351,7 +360,8 @@ export interface GremlinGraphGetProperties { /* The consistency policy for the Cosmos DB database account. */ export interface ConsistencyPolicy { /* The default consistency level and configuration settings of the Cosmos DB account. */ - defaultConsistencyLevel: string; + defaultConsistencyLevel: "Eventual" | "Session" | "BoundedStaleness" | "Strong" | "ConsistentPrefix"; + /* When used with the Bounded Staleness consistency level, this value represents the number of stale requests tolerated. Accepted range for this value is 1 – 2,147,483,647. Required when defaultConsistencyPolicy is set to 'BoundedStaleness'. */ maxStalenessPrefix?: number; /* When used with the Bounded Staleness consistency level, this value represents the time amount of staleness (in seconds) tolerated. Accepted range for this value is 5 - 86400. Required when defaultConsistencyPolicy is set to 'BoundedStaleness'. */ @@ -411,7 +421,7 @@ export interface DatabaseAccountGetProperties { virtualNetworkRules?: VirtualNetworkRule[]; /* List of Private Endpoint Connections configured for the Cosmos DB account. */ - readonly privateEndpointConnections?: PrivateEndpointConnection[]; + readonly privateEndpointConnections?: unknown[]; /* Enables the account to write in multiple locations */ enableMultipleWriteLocations?: boolean; @@ -424,6 +434,8 @@ export interface DatabaseAccountGetProperties { disableKeyBasedMetadataWriteAccess?: boolean; /* The URI of the key vault */ keyVaultKeyUri?: string; + /* The default identity for accessing key vault used in features like customer managed keys. The default identity needs to be explicitly set by the users. It can be "FirstPartyIdentity", "SystemAssignedIdentity" and more. */ + defaultIdentity?: string; /* Whether requests from Public Network are allowed */ publicNetworkAccess?: PublicNetworkAccess; @@ -434,8 +446,17 @@ export interface DatabaseAccountGetProperties { /* Flag to indicate whether to enable storage analytics. */ enableAnalyticalStorage?: boolean; + /* The object representing the policy for taking backups on an account. */ + backupPolicy?: BackupPolicy; + /* The CORS policy for the Cosmos DB database account. */ cors?: CorsPolicy[]; + + /* Indicates what services are allowed to bypass firewall checks. */ + networkAclBypass?: NetworkAclBypass; + + /* An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account. */ + networkAclBypassResourceIds?: unknown[]; } /* Properties to create and update Azure Cosmos DB database accounts. */ @@ -473,6 +494,8 @@ export interface DatabaseAccountCreateUpdateProperties { disableKeyBasedMetadataWriteAccess?: boolean; /* The URI of the key vault */ keyVaultKeyUri?: string; + /* The default identity for accessing key vault used in features like customer managed keys. The default identity needs to be explicitly set by the users. It can be "FirstPartyIdentity", "SystemAssignedIdentity" and more. */ + defaultIdentity?: string; /* Whether requests from Public Network are allowed */ publicNetworkAccess?: PublicNetworkAccess; @@ -483,14 +506,27 @@ export interface DatabaseAccountCreateUpdateProperties { /* Flag to indicate whether to enable storage analytics. */ enableAnalyticalStorage?: boolean; + /* The object representing the policy for taking backups on an account. */ + backupPolicy?: BackupPolicy; + /* The CORS policy for the Cosmos DB database account. */ cors?: CorsPolicy[]; + + /* Indicates what services are allowed to bypass firewall checks. */ + networkAclBypass?: NetworkAclBypass; + + /* An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account. */ + networkAclBypassResourceIds?: unknown[]; } /* Parameters to create and update Cosmos DB database accounts. */ export type DatabaseAccountCreateUpdateParameters = ARMResourceProperties & { /* Indicates the type of database account. This can only be set at database account creation. */ - kind?: string; + kind?: "GlobalDocumentDB" | "MongoDB" | "Parse"; + + /* undocumented */ + identity?: ManagedServiceIdentity; + /* undocumented */ properties: DatabaseAccountCreateUpdateProperties; }; @@ -527,6 +563,8 @@ export interface DatabaseAccountUpdateProperties { disableKeyBasedMetadataWriteAccess?: boolean; /* The URI of the key vault */ keyVaultKeyUri?: string; + /* The default identity for accessing key vault used in features like customer managed keys. The default identity needs to be explicitly set by the users. It can be "FirstPartyIdentity", "SystemAssignedIdentity" and more. */ + defaultIdentity?: string; /* Whether requests from Public Network are allowed */ publicNetworkAccess?: PublicNetworkAccess; @@ -537,8 +575,17 @@ export interface DatabaseAccountUpdateProperties { /* Flag to indicate whether to enable storage analytics. */ enableAnalyticalStorage?: boolean; + /* The object representing the policy for taking backups on an account. */ + backupPolicy?: BackupPolicy; + /* The CORS policy for the Cosmos DB database account. */ cors?: CorsPolicy[]; + + /* Indicates what services are allowed to bypass firewall checks. */ + networkAclBypass?: NetworkAclBypass; + + /* An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account. */ + networkAclBypassResourceIds?: unknown[]; } /* Parameters for patching Azure Cosmos DB database account properties. */ @@ -548,6 +595,9 @@ export interface DatabaseAccountUpdateParameters { /* The location of the resource group to which the resource belongs. */ location?: string; + /* undocumented */ + identity?: ManagedServiceIdentity; + /* undocumented */ properties?: DatabaseAccountUpdateProperties; } @@ -585,7 +635,7 @@ export interface DatabaseAccountListConnectionStringsResult { /* Parameters to regenerate the keys within the database account. */ export interface DatabaseAccountRegenerateKeyParameters { /* The access key to regenerate. */ - keyKind: string; + keyKind: "primary" | "secondary" | "primaryReadonly" | "secondaryReadonly"; } /* The offer type for the Cosmos DB database account. */ @@ -615,7 +665,7 @@ export interface SqlDatabaseCreateUpdateProperties { resource: SqlDatabaseResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB container. */ @@ -630,7 +680,7 @@ export interface SqlContainerCreateUpdateProperties { resource: SqlContainerResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB storedProcedure. */ @@ -645,7 +695,7 @@ export interface SqlStoredProcedureCreateUpdateProperties { resource: SqlStoredProcedureResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB userDefinedFunction. */ @@ -660,7 +710,7 @@ export interface SqlUserDefinedFunctionCreateUpdateProperties { resource: SqlUserDefinedFunctionResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB trigger. */ @@ -675,7 +725,7 @@ export interface SqlTriggerCreateUpdateProperties { resource: SqlTriggerResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB MongoDB database. */ @@ -690,7 +740,7 @@ export interface MongoDBDatabaseCreateUpdateProperties { resource: MongoDBDatabaseResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB MongoDB collection. */ @@ -705,7 +755,7 @@ export interface MongoDBCollectionCreateUpdateProperties { resource: MongoDBCollectionResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB Table. */ @@ -720,7 +770,7 @@ export interface TableCreateUpdateProperties { resource: TableResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB Cassandra keyspace. */ @@ -735,7 +785,7 @@ export interface CassandraKeyspaceCreateUpdateProperties { resource: CassandraKeyspaceResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB Cassandra table. */ @@ -750,7 +800,7 @@ export interface CassandraTableCreateUpdateProperties { resource: CassandraTableResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB Gremlin database. */ @@ -765,7 +815,7 @@ export interface GremlinDatabaseCreateUpdateProperties { resource: GremlinDatabaseResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Parameters to create and update Cosmos DB Gremlin graph. */ @@ -780,7 +830,7 @@ export interface GremlinGraphCreateUpdateProperties { resource: GremlinGraphResource; /* A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request. */ - options: CreateUpdateOptions; + options?: CreateUpdateOptions; } /* Cosmos DB resource throughput object. Either throughput is required or autoscaleSettings is required, but not both. */ @@ -853,8 +903,7 @@ export interface SqlContainerResource { /* The conflict resolution policy for the container. */ conflictResolutionPolicy?: ConflictResolutionPolicy; - //TODO: this property is manually added. It should be auto-generated instead. Need to be fixed in the API spec. - /* Analytical storage time to live */ + /* Analytical TTL. */ analyticalStorageTtl?: number; } @@ -863,7 +912,8 @@ export interface IndexingPolicy { /* Indicates if the indexing policy is automatic */ automatic?: boolean; /* Indicates the indexing mode. */ - indexingMode?: string; + indexingMode?: "consistent" | "lazy" | "none"; + /* List of paths to include in the indexing */ includedPaths?: IncludedPath[]; @@ -894,11 +944,12 @@ export interface IncludedPath { /* The indexes for the path. */ export interface Indexes { /* The datatype for which the indexing behavior is applied to. */ - dataType?: string; + dataType?: "String" | "Number" | "Point" | "Polygon" | "LineString" | "MultiPolygon"; + /* The precision of the index. -1 is maximum precision. */ precision?: number; /* Indicates the type of index. */ - kind?: string; + kind?: "Hash" | "Range" | "Spatial"; } /* List of composite path */ @@ -909,7 +960,7 @@ export interface CompositePath { /* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */ path?: string; /* Sort order for composite paths. */ - order?: string; + order?: "ascending" | "descending"; } /* undocumented */ @@ -928,10 +979,13 @@ export interface ContainerPartitionKey { /* List of paths using which data within the container can be partitioned */ paths?: Path[]; - /* Indicates the kind of algorithm used for partitioning */ - kind?: string; + /* Indicates the kind of algorithm used for partitioning. For MultiHash, multiple partition keys (upto three maximum) are supported for container create */ + kind?: "Hash" | "Range" | "MultiHash"; + /* Indicates the version of the partition key definition */ version?: number; + /* Indicates if the container is using a system generated partition key */ + readonly systemKey?: boolean; } /* A path. These typically start with root (/path) */ @@ -952,7 +1006,8 @@ export interface UniqueKey { /* The conflict resolution policy for the container. */ export interface ConflictResolutionPolicy { /* Indicates the conflict resolution mode. */ - mode?: string; + mode?: "LastWriterWins" | "Custom"; + /* The conflict resolution path in the case of LastWriterWins mode. */ conflictResolutionPath?: string; /* The procedure to resolve conflicts in the case of custom mode. */ @@ -980,11 +1035,12 @@ export interface SqlTriggerResource { /* Name of the Cosmos DB SQL trigger */ id: string; /* Body of the Trigger */ - body?: string; + body: string; /* Type of the Trigger */ - triggerType?: string; + triggerType?: "Pre" | "Post"; + /* The operation the trigger is associated with */ - triggerOperation?: string; + triggerOperation?: "All" | "Create" | "Update" | "Delete" | "Replace"; } /* Cosmos DB MongoDB database resource object */ @@ -1143,6 +1199,19 @@ export interface Capability { /* Tags are a list of key-value pairs that describe the resource. These tags can be used in viewing and grouping this resource (across resource groups). A maximum of 15 tags can be provided for a resource. Each tag must have a key no greater than 128 characters and value no greater than 256 characters. For example, the default experience for a template type is set with "defaultExperience": "Cassandra". Current "defaultExperience" values also include "Table", "Graph", "DocumentDB", and "MongoDB". */ export type Tags = { [key: string]: string }; +/* Identity for the resource. */ +export interface ManagedServiceIdentity { + /* The principal id of the system assigned identity. This property will only be provided for a system assigned identity. */ + readonly principalId?: string; + /* The tenant id of the system assigned identity. This property will only be provided for a system assigned identity. */ + readonly tenantId?: string; + /* The type of identity used for the resource. The type 'SystemAssigned,UserAssigned' includes both an implicitly created identity and a set of user assigned identities. The type 'None' will remove any identities from the service. */ + type?: "SystemAssigned" | "UserAssigned" | "SystemAssigned,UserAssigned" | "None"; + + /* The list of user identities associated with resource. The user identity dictionary key references will be ARM resource ids in the form: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}'. */ + userAssignedIdentities?: unknown; +} + /* The status of the Cosmos DB account at the time the operation was called. The status can be one of following. 'Creating' – the Cosmos DB account is being created. When an account is in Creating state, only properties that are specified as input for the Create Cosmos DB account operation are returned. 'Succeeded' – the Cosmos DB account is active for use. 'Updating' – the Cosmos DB account is being updated. 'Deleting' – the Cosmos DB account is being deleted. 'Failed' – the Cosmos DB account failed creation. 'DeletionFailed' – the Cosmos DB account deletion failed. */ export type ProvisioningState = string; @@ -1163,34 +1232,8 @@ export interface VirtualNetworkRule { ignoreMissingVNetServiceEndpoint?: boolean; } -/* A private endpoint connection */ -export type PrivateEndpointConnection = unknown & { - /* Resource properties. */ - properties?: PrivateEndpointConnectionProperties; -}; - -/* Properties of a private endpoint connection. */ -export interface PrivateEndpointConnectionProperties { - /* Private endpoint which the connection belongs to. */ - privateEndpoint?: PrivateEndpointProperty; - - /* Connection State of the Private Endpoint Connection. */ - privateLinkServiceConnectionState?: PrivateLinkServiceConnectionStateProperty; -} - -/* Private endpoint which the connection belongs to. */ -export interface PrivateEndpointProperty { - /* Resource id of the private endpoint. */ - id?: string; -} - -/* Connection State of the Private Endpoint Connection. */ -export interface PrivateLinkServiceConnectionStateProperty { - /* The private link service connection status. */ - status?: string; - /* Any action that is required beyond basic workflow (approve/ reject/ disconnect) */ - readonly actionsRequired?: string; -} +/* Indicates what services are allowed to bypass firewall checks. */ +export type NetworkAclBypass = "None" | "AzureServices"; /* REST API operation */ export interface Operation { @@ -1257,7 +1300,8 @@ export interface MetricDefinition { readonly metricAvailabilities?: MetricAvailability[]; /* The primary aggregation type of the metric. */ - readonly primaryAggregationType?: string; + readonly primaryAggregationType?: "None" | "Average" | "Total" | "Minimum" | "Maximum" | "Last"; + /* The unit of the metric. */ unit?: UnitType; @@ -1391,5 +1435,31 @@ export type PublicNetworkAccess = "Enabled" | "Disabled"; /* undocumented */ export interface ApiProperties { /* Describes the ServerVersion of an a MongoDB account. */ - serverVersion?: string; + serverVersion?: "3.2" | "3.6" | "4.0"; +} + +/* The object representing the policy for taking backups on an account. */ +export interface BackupPolicy { + /* undocumented */ + type: BackupPolicyType; +} + +/* Describes the mode of backups. */ +export type BackupPolicyType = "Periodic" | "Continuous"; + +/* The object representing periodic mode backup policy. */ +export type PeriodicModeBackupPolicy = BackupPolicy & { + /* Configuration values for periodic mode backup */ + periodicModeProperties?: PeriodicModeProperties; +}; + +/* The object representing continuous mode backup policy. */ +export type ContinuousModeBackupPolicy = BackupPolicy; + +/* Configuration values for periodic mode backup */ +export interface PeriodicModeProperties { + /* An integer representing the interval in minutes between two backups */ + backupIntervalInMinutes?: number; + /* An integer representing the time (in hours) that each backup is retained */ + backupRetentionIntervalInHours?: number; } diff --git a/src/Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces.ts b/src/Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces.ts new file mode 100644 index 000000000..a46b9aab0 --- /dev/null +++ b/src/Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces.ts @@ -0,0 +1,88 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/notebook.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2021-04-15"; + +/* Gets the notebook workspace resources of an existing Cosmos DB account. */ +export async function listByDatabaseAccount( + subscriptionId: string, + resourceGroupName: string, + accountName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Gets the notebook workspace for a Cosmos DB account. */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + notebookWorkspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces/${notebookWorkspaceName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Creates the notebook workspace for a Cosmos DB account. */ +export async function createOrUpdate( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + notebookWorkspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces/${notebookWorkspaceName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: {} }); +} + +/* Deletes the notebook workspace for a Cosmos DB account. */ +export async function destroy( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + notebookWorkspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces/${notebookWorkspaceName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "DELETE", apiVersion }); +} + +/* Retrieves the connection info for the notebook workspace */ +export async function listConnectionInfo( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + notebookWorkspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces/${notebookWorkspaceName}/listConnectionInfo`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Regenerates the auth token for the notebook workspace */ +export async function regenerateAuthToken( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + notebookWorkspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces/${notebookWorkspaceName}/regenerateAuthToken`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Starts the notebook workspace */ +export async function start( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + notebookWorkspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces/${notebookWorkspaceName}/start`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} diff --git a/src/Utils/arm/generatedClients/cosmosNotebooks/types.ts b/src/Utils/arm/generatedClients/cosmosNotebooks/types.ts new file mode 100644 index 000000000..9704f399a --- /dev/null +++ b/src/Utils/arm/generatedClients/cosmosNotebooks/types.ts @@ -0,0 +1,40 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/notebook.json +*/ + +import { ARMResourceProperties } from "../cosmos/types"; + +/* Parameters to create a notebook workspace resource */ +export type NotebookWorkspaceCreateUpdateParameters = unknown; + +/* A list of notebook workspace resources */ +export interface NotebookWorkspaceListResult { + /* Array of notebook workspace resources */ + value?: NotebookWorkspace[]; +} + +/* A notebook workspace resource */ +export type NotebookWorkspace = ARMResourceProperties & { + /* Resource properties. */ + properties?: NotebookWorkspaceProperties; +}; + +/* Properties of a notebook workspace resource. */ +export interface NotebookWorkspaceProperties { + /* Specifies the endpoint of Notebook server. */ + readonly notebookServerEndpoint?: string; + /* Status of the notebook workspace. Possible values are: Creating, Online, Deleting, Failed, Updating. */ + readonly status?: string; +} + +/* The connection info for the given notebook workspace */ +export interface NotebookWorkspaceConnectionInfoResult { + /* Specifies auth token used for connecting to Notebook server (uses token-based auth). */ + readonly authToken?: string; + /* Specifies the endpoint of Notebook server. */ + readonly notebookServerEndpoint?: string; +} diff --git a/src/Utils/arm/generatedClients/synapseSparkPools/bigDataPools.ts b/src/Utils/arm/generatedClients/synapseSparkPools/bigDataPools.ts new file mode 100644 index 000000000..db5ff876e --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseSparkPools/bigDataPools.ts @@ -0,0 +1,68 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/bigDataPool.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2021-03-01"; + +/* Get a Big Data pool. */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + bigDataPoolName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/bigDataPools/${bigDataPoolName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Patch a Big Data pool. */ +export async function update( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + bigDataPoolName: string, + body: Types.BigDataPoolPatchInfo +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/bigDataPools/${bigDataPoolName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body }); +} + +/* Create a new Big Data pool. */ +export async function createOrUpdate( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + bigDataPoolName: string, + body: Types.BigDataPoolResourceInfo +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/bigDataPools/${bigDataPoolName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); +} + +/* Delete a Big Data pool from the workspace. */ +export async function destroy( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + bigDataPoolName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/bigDataPools/${bigDataPoolName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "DELETE", apiVersion }); +} + +/* List Big Data pools in a workspace. */ +export async function listByWorkspace( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/bigDataPools`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} diff --git a/src/Utils/arm/generatedClients/synapseSparkPools/types.ts b/src/Utils/arm/generatedClients/synapseSparkPools/types.ts new file mode 100644 index 000000000..700dcadd7 --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseSparkPools/types.ts @@ -0,0 +1,127 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/bigDataPool.json +*/ + +/* Collection of Big Data pool information */ +export interface BigDataPoolResourceInfoListResult { + /* Link to the next page of results */ + nextLink?: string; + /* List of Big Data pools */ + value?: BigDataPoolResourceInfo[]; +} + +/* Properties patch for a Big Data pool */ +export interface BigDataPoolPatchInfo { + /* Updated tags for the Big Data pool */ + tags?: unknown; +} + +/* A Big Data pool */ +export type BigDataPoolResourceInfo = unknown & { + /* Big Data pool properties */ + properties?: BigDataPoolResourceProperties; +}; + +/* Properties of a Big Data pool powered by Apache Spark */ +export interface BigDataPoolResourceProperties { + /* The state of the Big Data pool. */ + provisioningState?: string; + /* Auto-scaling properties */ + autoScale?: AutoScaleProperties; + + /* The time when the Big Data pool was created. */ + creationDate?: string; + /* Auto-pausing properties */ + autoPause?: AutoPauseProperties; + + /* Whether compute isolation is required or not. */ + isComputeIsolationEnabled?: boolean; + /* Whether session level packages enabled. */ + sessionLevelPackagesEnabled?: boolean; + /* The cache size */ + cacheSize?: number; + /* Dynamic Executor Allocation */ + dynamicExecutorAllocation?: DynamicExecutorAllocation; + + /* The Spark events folder */ + sparkEventsFolder?: string; + /* The number of nodes in the Big Data pool. */ + nodeCount?: number; + /* Library version requirements */ + libraryRequirements?: LibraryRequirements; + + /* List of custom libraries/packages associated with the spark pool. */ + customLibraries?: LibraryInfo[]; + + /* Spark configuration file to specify additional properties */ + sparkConfigProperties?: LibraryRequirements; + + /* The Apache Spark version. */ + sparkVersion?: string; + /* The default folder where Spark logs will be written. */ + defaultSparkLogFolder?: string; + /* The level of compute power that each node in the Big Data pool has. */ + nodeSize?: "None" | "Small" | "Medium" | "Large" | "XLarge" | "XXLarge" | "XXXLarge"; + + /* The kind of nodes that the Big Data pool provides. */ + nodeSizeFamily?: "None" | "MemoryOptimized"; + + /* The time when the Big Data pool was updated successfully. */ + readonly lastSucceededTimestamp?: string; +} + +/* Auto-scaling properties of a Big Data pool powered by Apache Spark */ +export interface AutoScaleProperties { + /* The minimum number of nodes the Big Data pool can support. */ + minNodeCount?: number; + /* Whether automatic scaling is enabled for the Big Data pool. */ + enabled?: boolean; + /* The maximum number of nodes the Big Data pool can support. */ + maxNodeCount?: number; +} + +/* Auto-pausing properties of a Big Data pool powered by Apache Spark */ +export interface AutoPauseProperties { + /* Number of minutes of idle time before the Big Data pool is automatically paused. */ + delayInMinutes?: number; + /* Whether auto-pausing is enabled for the Big Data pool. */ + enabled?: boolean; +} + +/* Dynamic Executor Allocation Properties */ +export interface DynamicExecutorAllocation { + /* Indicates whether Dynamic Executor Allocation is enabled or not. */ + enabled?: boolean; +} + +/* Library/package information of a Big Data pool powered by Apache Spark */ +export interface LibraryInfo { + /* Name of the library. */ + name?: string; + /* Storage blob path of library. */ + path?: string; + /* Storage blob container name. */ + containerName?: string; + /* The last update time of the library. */ + readonly uploadedTimestamp?: string; + /* Type of the library. */ + type?: string; + /* Provisioning status of the library/package. */ + readonly provisioningStatus?: string; + /* Creator Id of the library/package. */ + readonly creatorId?: string; +} + +/* Library requirements for a Big Data pool powered by Apache Spark */ +export interface LibraryRequirements { + /* The last update time of the library requirements file. */ + readonly time?: string; + /* The library requirements. */ + content?: string; + /* The filename of the library requirements file. */ + filename?: string; +} diff --git a/src/Utils/arm/generatedClients/synapseWorkspaces/restorableDroppedSqlPools.ts b/src/Utils/arm/generatedClients/synapseWorkspaces/restorableDroppedSqlPools.ts new file mode 100644 index 000000000..a782ae809 --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseWorkspaces/restorableDroppedSqlPools.ts @@ -0,0 +1,33 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/workspace.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2021-03-01"; + +/* Gets a deleted sql pool that can be restored */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + restorableDroppedSqlPoolId: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/restorableDroppedSqlPools/${restorableDroppedSqlPoolId}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Gets a list of deleted Sql pools that can be restored */ +export async function listByWorkspace( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/restorableDroppedSqlPools`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} diff --git a/src/Utils/arm/generatedClients/synapseWorkspaces/types.ts b/src/Utils/arm/generatedClients/synapseWorkspaces/types.ts new file mode 100644 index 000000000..a806ec6f5 --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseWorkspaces/types.ts @@ -0,0 +1,251 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/workspace.json +*/ + +import { ARMResourceProperties } from "../cosmos/types"; + +/* Workspace active directory administrator properties */ +export interface AadAdminProperties { + /* Tenant ID of the workspace active directory administrator */ + tenantId?: string; + /* Login of the workspace active directory administrator */ + login?: string; + /* Workspace active directory administrator type */ + administratorType?: string; + /* Object ID of the workspace active directory administrator */ + sid?: string; +} + +/* List of workspaces */ +export interface WorkspaceInfoListResult { + /* Link to the next page of results */ + nextLink?: string; + /* List of workspaces */ + value?: Workspace[]; +} + +/* Details of the data lake storage account associated with the workspace */ +export interface DataLakeStorageAccountDetails { + /* Account URL */ + accountUrl?: string; + /* Filesystem name */ + filesystem?: string; +} + +/* Details of the encryption associated with the workspace */ +export interface EncryptionDetails { + /* Double Encryption enabled */ + readonly doubleEncryptionEnabled?: boolean; + /* Customer Managed Key Details */ + cmk?: CustomerManagedKeyDetails; +} + +/* Details of the customer managed key associated with the workspace */ +export interface CustomerManagedKeyDetails { + /* The customer managed key status on the workspace */ + readonly status?: string; + /* The key object of the workspace */ + key?: WorkspaceKeyDetails; +} + +/* Details of the customer managed key associated with the workspace */ +export interface WorkspaceKeyDetails { + /* Workspace Key sub-resource name */ + name?: string; + /* Workspace Key sub-resource key vault url */ + keyVaultUrl?: string; +} + +/* The workspace managed identity */ +export interface ManagedIdentity { + /* The principal ID of the workspace managed identity */ + readonly principalId?: string; + /* The tenant ID of the workspace managed identity */ + readonly tenantId?: string; + /* The type of managed identity for the workspace */ + type?: "None" | "SystemAssigned"; +} + +/* Virtual Network Profile */ +export interface VirtualNetworkProfile { + /* Subnet ID used for computes in workspace */ + computeSubnetId?: string; +} + +/* Managed Virtual Network Settings */ +export interface ManagedVirtualNetworkSettings { + /* Prevent Data Exfiltration */ + preventDataExfiltration?: boolean; + /* Linked Access Check On Target Resource */ + linkedAccessCheckOnTargetResource?: boolean; + /* Allowed Aad Tenant Ids For Linking */ + allowedAadTenantIdsForLinking?: unknown[]; +} + +/* Git integration settings */ +export interface WorkspaceRepositoryConfiguration { + /* Type of workspace repositoryID configuration. Example WorkspaceVSTSConfiguration, WorkspaceGitHubConfiguration */ + type?: string; + /* GitHub Enterprise host name. For example: https://github.mydomain.com */ + hostName?: string; + /* Account name */ + accountName?: string; + /* VSTS project name */ + projectName?: string; + /* Repository name */ + repositoryName?: string; + /* Collaboration branch */ + collaborationBranch?: string; + /* Root folder to use in the repository */ + rootFolder?: string; + /* The last commit ID */ + lastCommitId?: string; + /* The VSTS tenant ID */ + tenantId?: string; +} + +/* Purview Configuration */ +export interface PurviewConfiguration { + /* Purview Resource ID */ + purviewResourceId?: string; +} + +/* Workspace active directory administrator */ +export type WorkspaceAadAdminInfo = ARMResourceProperties & { + /* Workspace active directory administrator properties */ + properties?: AadAdminProperties; +}; + +/* A workspace */ +export type Workspace = ARMResourceProperties & { + /* Workspace resource properties */ + properties?: WorkspaceProperties; + + /* Identity of the workspace */ + identity?: ManagedIdentity; +}; + +/* Workspace properties */ +export interface WorkspaceProperties { + /* Workspace default data lake storage account details */ + defaultDataLakeStorage?: DataLakeStorageAccountDetails; + + /* SQL administrator login password */ + sqlAdministratorLoginPassword?: string; + /* Workspace managed resource group. The resource group name uniquely identifies the resource group within the user subscriptionId. The resource group name must be no longer than 90 characters long, and must be alphanumeric characters (Char.IsLetterOrDigit()) and '-', '_', '(', ')' and'.'. Note that the name cannot end with '.' */ + managedResourceGroupName?: string; + /* Resource provisioning state */ + readonly provisioningState?: string; + /* Login for workspace SQL active directory administrator */ + sqlAdministratorLogin?: string; + /* Virtual Network profile */ + virtualNetworkProfile?: VirtualNetworkProfile; + + /* Connectivity endpoints */ + connectivityEndpoints?: unknown; + + /* Setting this to 'default' will ensure that all compute for this workspace is in a virtual network managed on behalf of the user. */ + managedVirtualNetwork?: string; + /* Private endpoint connections to the workspace */ + privateEndpointConnections?: unknown[]; + + /* The encryption details of the workspace */ + encryption?: EncryptionDetails; + + /* The workspace unique identifier */ + readonly workspaceUID?: string; + /* Workspace level configs and feature flags */ + readonly extraProperties?: unknown; + + /* Managed Virtual Network Settings */ + managedVirtualNetworkSettings?: ManagedVirtualNetworkSettings; + + /* Git integration settings */ + workspaceRepositoryConfiguration?: WorkspaceRepositoryConfiguration; + + /* Purview Configuration */ + purviewConfiguration?: PurviewConfiguration; + + /* The ADLA resource ID. */ + readonly adlaResourceId?: string; + /* Enable or Disable public network access to workspace */ + publicNetworkAccess?: "Enabled" | "Disabled"; +} + +/* Workspace patch details */ +export interface WorkspacePatchInfo { + /* Resource tags */ + tags?: unknown; + + /* The identity of the workspace */ + identity?: ManagedIdentity; + + /* Workspace patch properties */ + properties?: WorkspacePatchProperties; +} + +/* Workspace patch properties */ +export interface WorkspacePatchProperties { + /* SQL administrator login password */ + sqlAdministratorLoginPassword?: string; + /* Managed Virtual Network Settings */ + managedVirtualNetworkSettings?: ManagedVirtualNetworkSettings; + + /* Git integration settings */ + workspaceRepositoryConfiguration?: WorkspaceRepositoryConfiguration; + + /* Purview Configuration */ + purviewConfiguration?: PurviewConfiguration; + + /* Resource provisioning state */ + readonly provisioningState?: string; + /* The encryption details of the workspace */ + encryption?: EncryptionDetails; + + /* Enable or Disable public network access to workspace */ + publicNetworkAccess?: "Enabled" | "Disabled"; +} + +/* Sql Control Settings for workspace managed identity */ +export type ManagedIdentitySqlControlSettingsModel = ARMResourceProperties & { + /* Sql Control Settings for workspace managed identity */ + properties?: unknown; +}; + +/* The properties of a restorable dropped Sql pool */ +export interface RestorableDroppedSqlPoolProperties { + /* The name of the database */ + readonly databaseName?: string; + /* The edition of the database */ + readonly edition?: string; + /* The max size in bytes of the database */ + readonly maxSizeBytes?: string; + /* The service level objective name of the database */ + readonly serviceLevelObjective?: string; + /* The elastic pool name of the database */ + readonly elasticPoolName?: string; + /* The creation date of the database (ISO8601 format) */ + readonly creationDate?: string; + /* The deletion date of the database (ISO8601 format) */ + readonly deletionDate?: string; + /* The earliest restore date of the database (ISO8601 format) */ + readonly earliestRestoreDate?: string; +} + +/* A restorable dropped Sql pool */ +export type RestorableDroppedSqlPool = ARMResourceProperties & { + /* The geo-location where the resource lives */ + readonly location?: string; + /* The properties of a restorable dropped Sql pool */ + properties?: RestorableDroppedSqlPoolProperties; +}; + +/* The response to a list restorable dropped Sql pools request */ +export interface RestorableDroppedSqlPoolListResult { + /* A list of restorable dropped Sql pools */ + value: RestorableDroppedSqlPool[]; +} diff --git a/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceAadAdmins.ts b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceAadAdmins.ts new file mode 100644 index 000000000..882bcd355 --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceAadAdmins.ts @@ -0,0 +1,39 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/workspace.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2021-03-01"; + +/* Gets a workspace active directory admin */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/administrators/activeDirectory`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Creates or updates a workspace active directory admin */ +export async function createOrUpdate( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + body: Types.WorkspaceAadAdminInfo +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/administrators/activeDirectory`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); +} + +/* Deletes a workspace active directory admin */ +export async function destroy(subscriptionId: string, resourceGroupName: string, workspaceName: string): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/administrators/activeDirectory`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "DELETE", apiVersion }); +} diff --git a/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceManagedIdentitySqlControlSettings.ts b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceManagedIdentitySqlControlSettings.ts new file mode 100644 index 000000000..c587de88f --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceManagedIdentitySqlControlSettings.ts @@ -0,0 +1,33 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/workspace.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2021-03-01"; + +/* undocumented */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/managedIdentitySqlControlSettings/default`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* undocumented */ +export async function createOrUpdate( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + body: Types.ManagedIdentitySqlControlSettingsModel +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/managedIdentitySqlControlSettings/default`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); +} diff --git a/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceSqlAadAdmins.ts b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceSqlAadAdmins.ts new file mode 100644 index 000000000..f5f1cd4c8 --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaceSqlAadAdmins.ts @@ -0,0 +1,39 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/workspace.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2021-03-01"; + +/* Gets a workspace SQL active directory admin */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/sqlAdministrators/activeDirectory`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Creates or updates a workspace SQL active directory admin */ +export async function createOrUpdate( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + body: Types.WorkspaceAadAdminInfo +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/sqlAdministrators/activeDirectory`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); +} + +/* Deletes a workspace SQL active directory admin */ +export async function destroy(subscriptionId: string, resourceGroupName: string, workspaceName: string): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}/sqlAdministrators/activeDirectory`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "DELETE", apiVersion }); +} diff --git a/src/Utils/arm/generatedClients/synapseWorkspaces/workspaces.ts b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaces.ts new file mode 100644 index 000000000..545264146 --- /dev/null +++ b/src/Utils/arm/generatedClients/synapseWorkspaces/workspaces.ts @@ -0,0 +1,65 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/synapse/resource-manager/Microsoft.Synapse/stable/2021-03-01/workspace.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2021-03-01"; + +/* Returns a list of workspaces in a resource group */ +export async function listByResourceGroup( + subscriptionId: string, + resourceGroupName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Gets a workspace */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Updates a workspace */ +export async function update( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + body: Types.WorkspacePatchInfo +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body }); +} + +/* Creates or updates a workspace */ +export async function createOrUpdate( + subscriptionId: string, + resourceGroupName: string, + workspaceName: string, + body: Types.Workspace +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); +} + +/* Deletes a workspace */ +export async function destroy(subscriptionId: string, resourceGroupName: string, workspaceName: string): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Synapse/workspaces/${workspaceName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "DELETE", apiVersion }); +} + +/* Returns a list of workspaces in a subscription */ +export async function list(subscriptionId: string): Promise { + const path = `/subscriptions/${subscriptionId}/providers/Microsoft.Synapse/workspaces`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts index 2d733cac4..e593633ac 100644 --- a/src/Utils/arm/request.ts +++ b/src/Utils/arm/request.ts @@ -144,13 +144,13 @@ async function getOperationStatus(operationStatusUrl: string) { const body = await response.json(); const status = body.status; - if (!status && response.status === 200) { - return body; - } if (status === "Canceled" || status === "Failed") { const errorMessage = body.error ? JSON.stringify(body.error) : "Operation could not be completed"; const error = new Error(errorMessage); throw new AbortError(error); } + if (response.status === 200) { + return body; + } throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`); } diff --git a/src/applyExplorerBindings.ts b/src/applyExplorerBindings.ts index ca28aea5c..895bc40ac 100644 --- a/src/applyExplorerBindings.ts +++ b/src/applyExplorerBindings.ts @@ -1,5 +1,5 @@ -import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer"; import * as ko from "knockout"; +import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer"; import Explorer from "./Explorer/Explorer"; export const applyExplorerBindings = (explorer: Explorer) => { @@ -7,8 +7,5 @@ export const applyExplorerBindings = (explorer: Explorer) => { window.dataExplorer = explorer; BindingHandlersRegisterer.registerBindingHandlers(); ko.applyBindings(explorer); - // This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times. - // TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal - $("#divExplorer").show(); } }; diff --git a/src/global.d.ts b/src/global.d.ts index 301c9209b..e54479c04 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -13,11 +13,6 @@ declare global { * No new usage of jQuery ($) * */ $: any; - /** - * @deprecated - * No new usage of jQuery - * */ - jQuery: any; gitSha: string; } } diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index 90023a184..630521f2d 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -1,24 +1,12 @@ -import * as React from "react"; +import * as msal from "@azure/msal-browser"; import { useBoolean } from "@fluentui/react-hooks"; -import { UserAgentApplication, Account, Configuration } from "msal"; +import * as React from "react"; +import { configContext } from "../ConfigContext"; +import { getMsalInstance } from "../Utils/AuthorizationUtils"; -const config: Configuration = { - cache: { - cacheLocation: "localStorage", - }, - auth: { - authority: "https://login.microsoftonline.com/common", - clientId: "203f1145-856a-4232-83d4-a43568fba23d", - }, -}; +const msalInstance = getMsalInstance(); -if (process.env.NODE_ENV === "development") { - config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; -} - -const msal = new UserAgentApplication(config); - -const cachedAccount = msal.getAllAccounts()?.[0]; +const cachedAccount = msalInstance.getAllAccounts()?.[0]; const cachedTenantId = localStorage.getItem("cachedTenantId"); interface ReturnType { @@ -28,7 +16,7 @@ interface ReturnType { login: () => void; logout: () => void; tenantId: string; - account: Account; + account: msal.AccountInfo; switchTenant: (tenantId: string) => void; } @@ -36,13 +24,17 @@ export function useAADAuth(): ReturnType { const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean( Boolean(cachedAccount && cachedTenantId) || false ); - const [account, setAccount] = React.useState(cachedAccount); + const [account, setAccount] = React.useState(cachedAccount); const [tenantId, setTenantId] = React.useState(cachedTenantId); const [graphToken, setGraphToken] = React.useState(); const [armToken, setArmToken] = React.useState(); + msalInstance.setActiveAccount(account); const login = React.useCallback(async () => { - const response = await msal.loginPopup(); + const response = await msalInstance.loginPopup({ + redirectUri: configContext.msalRedirectURI, + scopes: [], + }); setLoggedIn(); setAccount(response.account); setTenantId(response.tenantId); @@ -52,13 +44,15 @@ export function useAADAuth(): ReturnType { const logout = React.useCallback(() => { setLoggedOut(); localStorage.removeItem("cachedTenantId"); - msal.logout(); + msalInstance.logoutRedirect(); }, []); const switchTenant = React.useCallback( async (id) => { - const response = await msal.loginPopup({ - authority: `https://login.microsoftonline.com/${id}`, + const response = await msalInstance.loginPopup({ + redirectUri: configContext.msalRedirectURI, + authority: `${configContext.AAD_ENDPOINT}${id}`, + scopes: [], }); setTenantId(response.tenantId); setAccount(response.account); @@ -69,17 +63,13 @@ export function useAADAuth(): ReturnType { React.useEffect(() => { if (account && tenantId) { Promise.all([ - 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://graph.windows.net//.default"], + msalInstance.acquireTokenSilent({ + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.GRAPH_ENDPOINT}/.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"], + msalInstance.acquireTokenSilent({ + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.ARM_ENDPOINT}/.default`], }), ]).then(([graphTokenResponse, armTokenResponse]) => { setGraphToken(graphTokenResponse.accessToken); diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 27cce3c2d..fc29e39dd 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -4,8 +4,8 @@ import { ConfigContext, initializeConfiguration } from "../ConfigContext"; // This hook initializes global configuration from a config.json file that is injected at deploy time // This allows the same main Data Explorer build to be exactly the same in all clouds/platforms, // but override some of the configuration as nesssary -export function useConfig(): Readonly { - const [state, setState] = useState(); +export function useConfig(): Readonly { + const [state, setState] = useState(); useEffect(() => { initializeConfiguration().then((response) => setState(response)); diff --git a/src/hooks/useDatabaseAccounts.tsx b/src/hooks/useDatabaseAccounts.tsx index 97ced2799..c120d6a71 100644 --- a/src/hooks/useDatabaseAccounts.tsx +++ b/src/hooks/useDatabaseAccounts.tsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { configContext } from "../ConfigContext"; import { DatabaseAccount } from "../Contracts/DataModels"; interface AccountListResult { @@ -14,7 +15,7 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: let accounts: Array = []; - let nextLink = `https://management.azure.com/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview`; + let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2021-06-15`; while (nextLink) { const response: Response = await fetch(nextLink, { headers }); diff --git a/src/hooks/useDirectories.tsx b/src/hooks/useDirectories.tsx index e78ff5a14..2073cf81a 100644 --- a/src/hooks/useDirectories.tsx +++ b/src/hooks/useDirectories.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { configContext } from "../ConfigContext"; import { Tenant } from "../Contracts/DataModels"; interface TenantListResult { @@ -13,7 +14,7 @@ export async function fetchDirectories(accessToken: string): Promise { headers.append("Authorization", bearer); let tenents: Array = []; - let nextLink = `https://management.azure.com/tenants?api-version=2020-01-01`; + let nextLink = `${configContext.ARM_ENDPOINT}/tenants?api-version=2020-01-01`; while (nextLink) { const response = await fetch(nextLink, { headers }); diff --git a/src/hooks/useGraphPhoto.tsx b/src/hooks/useGraphPhoto.tsx index e09efeb04..b47d8d536 100644 --- a/src/hooks/useGraphPhoto.tsx +++ b/src/hooks/useGraphPhoto.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { configContext } from "../ConfigContext"; export async function fetchPhoto(accessToken: string): Promise { const headers = new Headers(); @@ -12,7 +13,7 @@ export async function fetchPhoto(accessToken: string): Promise { headers: headers, }; - return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options).then((response) => + return fetch(`${configContext.GRAPH_ENDPOINT}/me/thumbnailPhoto?api-version=1.6`, options).then((response) => response.blob() ); } diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 25807a8c9..86200808c 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -1,15 +1,16 @@ import { useEffect, useState } from "react"; import { applyExplorerBindings } from "../applyExplorerBindings"; import { AuthType } from "../AuthType"; -import { AccountKind } from "../Common/Constants"; +import { AccountKind, Flights } from "../Common/Constants"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { sendMessage, sendReadyMessage } from "../Common/MessageHandler"; import { configContext, Platform, updateConfigContext } from "../ConfigContext"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; -import Explorer, { ExplorerParams } from "../Explorer/Explorer"; -import { handleOpenAction } from "../Explorer/OpenActions"; +import Explorer from "../Explorer/Explorer"; +import { handleOpenAction } from "../Explorer/OpenActions/OpenActions"; +import { useDatabases } from "../Explorer/useDatabases"; import { AAD, ConnectionString, @@ -27,27 +28,29 @@ import { import { CollectionCreation } from "../Shared/Constants"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { PortalEnv, updateUserContext, userContext } from "../UserContext"; -import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts"; +import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; +import { getMsalInstance } from "../Utils/AuthorizationUtils"; import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; // This hook will create a new instance of Explorer.ts and bind it to the DOM // This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React -// Pleas tread carefully :) +// Please tread carefully :) -export function useKnockoutExplorer(platform: Platform, explorerParams: ExplorerParams): Explorer { +export function useKnockoutExplorer(platform: Platform): Explorer { const [explorer, setExplorer] = useState(); useEffect(() => { const effect = async () => { if (platform) { if (platform === Platform.Hosted) { - const explorer = await configureHosted(explorerParams); + const explorer = await configureHosted(); setExplorer(explorer); } else if (platform === Platform.Emulator) { - const explorer = configureEmulator(explorerParams); + const explorer = configureEmulator(); setExplorer(explorer); } else if (platform === Platform.Portal) { - const explorer = await configurePortal(explorerParams); + const explorer = await configurePortal(); setExplorer(explorer); } } @@ -64,21 +67,21 @@ export function useKnockoutExplorer(platform: Platform, explorerParams: Explorer return explorer; } -async function configureHosted(explorerParams: ExplorerParams): Promise { +async function configureHosted(): Promise { const win = (window as unknown) as HostedExplorerChildFrame; if (win.hostedConfig.authType === AuthType.EncryptedToken) { - return configureHostedWithEncryptedToken(win.hostedConfig, explorerParams); + return configureHostedWithEncryptedToken(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.ResourceToken) { - return configureHostedWithResourceToken(win.hostedConfig, explorerParams); + return configureHostedWithResourceToken(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.ConnectionString) { - return configureHostedWithConnectionString(win.hostedConfig, explorerParams); + return configureHostedWithConnectionString(win.hostedConfig); } else if (win.hostedConfig.authType === AuthType.AAD) { - return configureHostedWithAAD(win.hostedConfig, explorerParams); + return configureHostedWithAAD(win.hostedConfig); } throw new Error(`Unknown hosted config: ${win.hostedConfig}`); } -async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParams): Promise { +async function configureHostedWithAAD(config: AAD): Promise { // TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken updateUserContext({ authType: AuthType.AAD, @@ -88,21 +91,42 @@ async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParam const accountResourceId = account.id; const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; - const keys = await listKeys(subscriptionId, resourceGroup, account.name); + let aadToken; + let keys: DatabaseAccountListKeysResult = {}; + if (account.properties?.documentEndpoint) { + const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default"); + const msalInstance = getMsalInstance(); + const cachedAccount = msalInstance.getAllAccounts()?.[0]; + msalInstance.setActiveAccount(cachedAccount); + const aadTokenResponse = await msalInstance.acquireTokenSilent({ + forceRefresh: true, + scopes: [hrefEndpoint], + }); + aadToken = aadTokenResponse.accessToken; + } + try { + if (!account.properties.disableLocalAuth) { + keys = await listKeys(subscriptionId, resourceGroup, account.name); + } + } catch (e) { + if (userContext.features.enableAadDataPlane) { + console.warn(e); + } else { + throw new Error(`List keys failed: ${e.message}`); + } + } updateUserContext({ subscriptionId, resourceGroup, + aadToken, databaseAccount: config.databaseAccount, masterKey: keys.primaryMasterKey, }); - const explorer = new Explorer(explorerParams); - explorer.configure({ - databaseAccount: account, - }); + const explorer = new Explorer(); return explorer; } -function configureHostedWithConnectionString(config: ConnectionString, explorerParams: ExplorerParams): Explorer { +function configureHostedWithConnectionString(config: ConnectionString): Explorer { const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const databaseAccount = { id: "", @@ -120,14 +144,11 @@ function configureHostedWithConnectionString(config: ConnectionString, explorerP databaseAccount, masterKey: config.masterKey, }); - const explorer = new Explorer(explorerParams); - explorer.configure({ - databaseAccount, - }); + const explorer = new Explorer(); return explorer; } -function configureHostedWithResourceToken(config: ResourceToken, explorerParams: ExplorerParams): Explorer { +function configureHostedWithResourceToken(config: ResourceToken): Explorer { const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken); const databaseAccount = { id: "", @@ -142,47 +163,44 @@ function configureHostedWithResourceToken(config: ResourceToken, explorerParams: authType: AuthType.ResourceToken, resourceToken: parsedResourceToken.resourceToken, endpoint: parsedResourceToken.accountEndpoint, + parsedResourceToken: { + databaseId: parsedResourceToken.databaseId, + collectionId: parsedResourceToken.collectionId, + partitionKey: parsedResourceToken.partitionKey, + }, }); - const explorer = new Explorer(explorerParams); - explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); - explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); - if (parsedResourceToken.partitionKey) { - explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); - } - explorer.configure({ databaseAccount }); + const explorer = new Explorer(); return explorer; } -function configureHostedWithEncryptedToken(config: EncryptedToken, explorerParams: ExplorerParams): Explorer { +function configureHostedWithEncryptedToken(config: EncryptedToken): Explorer { + const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); updateUserContext({ authType: AuthType.EncryptedToken, accessToken: encodeURIComponent(config.encryptedToken), - }); - const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); - const explorer = new Explorer(explorerParams); - explorer.configure({ databaseAccount: { id: "", + location: "", + type: "", name: config.encryptedTokenMetadata.accountName, kind: getDatabaseAccountKindFromExperience(apiExperience), properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata), - tags: {}, }, }); + const explorer = new Explorer(); return explorer; } -function configureEmulator(explorerParams: ExplorerParams): Explorer { +function configureEmulator(): Explorer { updateUserContext({ databaseAccount: emulatorAccount, authType: AuthType.MasterKey, }); - const explorer = new Explorer(explorerParams); - explorer.isAccountReady(true); + const explorer = new Explorer(); return explorer; } -async function configurePortal(explorerParams: ExplorerParams): Promise { +async function configurePortal(): Promise { updateUserContext({ authType: AuthType.AAD, }); @@ -198,8 +216,12 @@ async function configurePortal(explorerParams: ExplorerParams): Promise void; setError: (error: string) => void; } diff --git a/src/hooks/useNotificationConsole.ts b/src/hooks/useNotificationConsole.ts new file mode 100644 index 000000000..2593e9bf0 --- /dev/null +++ b/src/hooks/useNotificationConsole.ts @@ -0,0 +1,25 @@ +import create, { UseStore } from "zustand"; +import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData"; + +export interface NotificationConsoleState { + isExpanded: boolean; + inProgressConsoleDataIdToBeDeleted: string; + consoleData: ConsoleData | undefined; + expandConsole: () => void; + // TODO Remove this method. Add a `closeConsole` method instead + setIsExpanded: (isExpanded: boolean) => void; + // TODO These two methods badly need a refactor. Not very react friendly. + setNotificationConsoleData: (consoleData: ConsoleData) => void; + setInProgressConsoleDataIdToBeDeleted: (id: string) => void; +} + +export const useNotificationConsole: UseStore = create((set) => ({ + isExpanded: false, + consoleData: undefined, + inProgressConsoleDataIdToBeDeleted: "", + expandConsole: () => set((state) => ({ ...state, isExpanded: true })), + setIsExpanded: (isExpanded) => set((state) => ({ ...state, isExpanded })), + setNotificationConsoleData: (consoleData: ConsoleData) => set((state) => ({ ...state, consoleData })), + setInProgressConsoleDataIdToBeDeleted: (id: string) => + set((state) => ({ ...state, inProgressConsoleDataIdToBeDeleted: id })), +})); diff --git a/src/hooks/usePortalAccessToken.tsx b/src/hooks/usePortalAccessToken.tsx index da5cbe2f8..fdccc84a7 100644 --- a/src/hooks/usePortalAccessToken.tsx +++ b/src/hooks/usePortalAccessToken.tsx @@ -24,8 +24,8 @@ export async function fetchAccessData(portalToken: string): Promise(); +export function useTokenMetadata(token: string): AccessInputMetadata | undefined { + const [state, setState] = useState(); useEffect(() => { if (token) { diff --git a/src/hooks/useSidePanel.ts b/src/hooks/useSidePanel.ts index e5d93c84b..e1558b910 100644 --- a/src/hooks/useSidePanel.ts +++ b/src/hooks/useSidePanel.ts @@ -1,35 +1,18 @@ -import { useState } from "react"; +import create, { UseStore } from "zustand"; -export interface SidePanelHooks { - isPanelOpen: boolean; - panelContent: JSX.Element; - headerText: string; - openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void; +export interface SidePanelState { + isOpen: boolean; + panelWidth: string; + panelContent?: JSX.Element; + headerText?: string; + openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void; closeSidePanel: () => void; } -export const useSidePanel = (): SidePanelHooks => { - const [isPanelOpen, setIsPanelOpen] = useState(false); - const [panelContent, setPanelContent] = useState(); - const [headerText, setHeaderText] = useState(); - const [onCloseCallback, setOnCloseCallback] = useState<{ callback: () => void }>(); - - const openSidePanel = (headerText: string, panelContent: JSX.Element, onClose?: () => void): void => { - setHeaderText(headerText); - setPanelContent(panelContent); - setIsPanelOpen(true); - !!onClose && setOnCloseCallback({ callback: onClose }); - }; - - const closeSidePanel = (): void => { - setHeaderText(""); - setPanelContent(undefined); - setIsPanelOpen(false); - if (onCloseCallback) { - onCloseCallback.callback(); - setOnCloseCallback(undefined); - } - }; - - return { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel }; -}; +export const useSidePanel: UseStore = create((set) => ({ + isOpen: false, + panelWidth: "440px", + openSidePanel: (headerText, panelContent, panelWidth = "440px") => + set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })), + closeSidePanel: () => set((state) => ({ ...state, isOpen: false })), +})); diff --git a/src/hooks/useSubscriptions.tsx b/src/hooks/useSubscriptions.tsx index d7ebfcbe3..e06542240 100644 --- a/src/hooks/useSubscriptions.tsx +++ b/src/hooks/useSubscriptions.tsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { configContext } from "../ConfigContext"; import { Subscription } from "../Contracts/DataModels"; interface SubscriptionListResult { @@ -13,7 +14,7 @@ export async function fetchSubscriptions(accessToken: string): Promise = []; - let nextLink = `https://management.azure.com/subscriptions?api-version=2020-01-01`; + let nextLink = `${configContext.ARM_ENDPOINT}subscriptions?api-version=2020-01-01`; while (nextLink) { const response = await fetch(nextLink, { headers }); diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index cd597bbf5..50e1aa10a 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -1,18 +1,77 @@ -import { useState } from "react"; +import create, { UseStore } from "zustand"; +import * as ViewModels from "../Contracts/ViewModels"; import TabsBase from "../Explorer/Tabs/TabsBase"; -import { TabsManager } from "../Explorer/Tabs/TabsManager"; -import { useObservable } from "./useObservable"; -export type UseTabs = { - tabs: readonly TabsBase[]; +interface TabsState { + openedTabs: TabsBase[]; activeTab: TabsBase; - tabsManager: TabsManager; -}; - -export function useTabs(): UseTabs { - const [tabsManager] = useState(() => new TabsManager()); - const tabs = useObservable(tabsManager.openedTabs); - const activeTab = useObservable(tabsManager.activeTab); - - return { tabs, activeTab, tabsManager }; + activateTab: (tab: TabsBase) => void; + activateNewTab: (tab: TabsBase) => void; + updateTab: (tab: TabsBase) => void; + getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean) => TabsBase[]; + refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void; + closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void; + closeTab: (tab: TabsBase) => void; } + +export const useTabs: UseStore = create((set, get) => ({ + openedTabs: [], + activeTab: undefined, + activateTab: (tab: TabsBase): void => { + if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) { + set({ activeTab: tab }); + tab.onActivate(); + } + }, + activateNewTab: (tab: TabsBase): void => { + set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab })); + tab.onActivate(); + }, + updateTab: (tab: TabsBase) => { + if (get().activeTab?.tabId === tab.tabId) { + set({ activeTab: tab }); + } + + set((state) => ({ + openedTabs: state.openedTabs.map((openedTab) => { + if (openedTab.tabId === tab.tabId) { + return tab; + } + return openedTab; + }), + })); + }, + getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] => + get().openedTabs.filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab))), + refreshActiveTab: (comparator: (tab: TabsBase) => boolean): void => { + // ensures that the tab selects/highlights the right node based on resource tree expand/collapse state + const activeTab = get().activeTab; + activeTab && comparator(activeTab) && activeTab.onActivate(); + }, + closeTabsByComparator: (comparator: (tab: TabsBase) => boolean): void => + get() + .openedTabs.filter(comparator) + .forEach((tab) => tab.onCloseTabButtonClick()), + closeTab: (tab: TabsBase): void => { + let tabIndex: number; + const { activeTab, openedTabs } = get(); + const updatedTabs = openedTabs.filter((openedTab, index) => { + if (tab.tabId === openedTab.tabId) { + tabIndex = index; + return false; + } + return true; + }); + if (updatedTabs.length === 0) { + set({ activeTab: undefined }); + } + + if (tab.tabId === activeTab.tabId && tabIndex !== -1) { + const tabToTheRight = updatedTabs[tabIndex]; + const lastOpenTab = updatedTabs[updatedTabs.length - 1]; + set({ activeTab: tabToTheRight || lastOpenTab }); + } + + set({ openedTabs: updatedTabs }); + }, +})); diff --git a/src/i18n.ts b/src/i18n.ts index 98c122828..533baca85 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -8,8 +8,7 @@ i18n .init({ fallbackLng: "en", detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] }, - // temporarily setting debug to true to investigate loading issues in prod - debug: true, + debug: process.env.NODE_ENV === "development", keySeparator: ".", interpolation: { formatSeparator: ",", diff --git a/src/index.html b/src/index.html index e9672edd7..5332ae673 100644 --- a/src/index.html +++ b/src/index.html @@ -8,54 +8,7 @@ - -
-
- Azure Cosmos DB - - Create an Azure Cosmos DB account - - Azure Cosmos DB Emulator -
-
- - - - - - +
diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 45ee23ce7..af68a47dc 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,30 +1,29 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Cassandra keyspace and table CRUD", async () => { const keyspaceId = generateUniqueName("keyspace"); const tableId = generateUniqueName("table"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); await explorer.click('[data-test="New Table"]'); - await explorer.click('[data-test="addCollection-keyspaceId"]'); - await explorer.fill('[data-test="addCollection-keyspaceId"]', keyspaceId); - await explorer.click('[data-test="addCollection-tableId"]'); - await explorer.fill('[data-test="addCollection-tableId"]', tableId); - await explorer.click('[aria-label="Add Table"] [data-test="addCollection-createCollection"]'); - await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`); - await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); + await explorer.click('[aria-label="Keyspace id"]'); + await explorer.fill('[aria-label="Keyspace id"]', keyspaceId); + await explorer.click('[aria-label="addCollection-tableId"]'); + await explorer.fill('[aria-label="addCollection-tableId"]', tableId); + await explorer.click("#sidePanelOkButton"); + await explorer.click(`.nodeItem >> text=${keyspaceId}`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); - await explorer.click('[aria-label="Submit"]'); + await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Keyspace")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index d5ee55e1e..0b2bf5090 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,18 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Graph CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and graph await explorer.click('[data-test="New Graph"]'); @@ -20,13 +18,13 @@ test("Graph CRUD", async () => { await explorer.fill('[aria-label="Graph id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `.nodeItem >> text=${containerId}`); + await explorer.click(`.nodeItem >> text=${databaseId}`); + await explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and graph - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Graph")'); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Graph")'); await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId); - await explorer.click('[aria-label="Submit"]'); + await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 3b8c944d7..53d343882 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,18 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and collection await explorer.click('[data-test="New Collection"]'); @@ -20,10 +18,10 @@ test("Mongo CRUD", async () => { await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Shard key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `.nodeItem >> text=${containerId}`); + await explorer.click(`.nodeItem >> text=${databaseId}`); + await explorer.click(`.nodeItem >> text=${containerId}`); // Create indexing policy - await safeClick(explorer, ".nodeItem >> text=Setting"); + await explorer.click(".nodeItem >> text=Settings"); await explorer.click('button[role="tab"]:has-text("Indexing Policy")'); await explorer.click('[aria-label="Index Field Name 0"]'); await explorer.fill('[aria-label="Index Field Name 0"]', "foo"); @@ -34,10 +32,10 @@ test("Mongo CRUD", async () => { await explorer.click('[aria-label="Delete index Button"]'); await explorer.click('[data-test="Save"]'); // Delete database and collection - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); - await explorer.click('[aria-label="Submit"]'); + await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index e3949385e..5b3845a14 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,18 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and collection await explorer.click('[data-test="New Collection"]'); @@ -20,13 +18,13 @@ test("Mongo CRUD", async () => { await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Shard key"]', "pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `.nodeItem >> text=${containerId}`); + explorer.click(`.nodeItem >> text=${databaseId}`); + explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and collection - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); + explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); - await explorer.click('[aria-label="Submit"]'); + await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); diff --git a/test/notebooks/upload.spec.ts b/test/notebooks/upload.spec.ts index 36b4658bf..5f2483531 100644 --- a/test/notebooks/upload.spec.ts +++ b/test/notebooks/upload.spec.ts @@ -2,6 +2,7 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import fs from "fs"; import path from "path"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); const filename = "GettingStarted.ipynb"; @@ -11,10 +12,7 @@ fs.copyFileSync(path.join(__dirname, filename), path.join(__dirname, fileToUploa test("Notebooks", async () => { await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Upload and Delete Notebook await explorer.click('[data-test="My Notebooks"] [aria-label="More"]'); await explorer.click('button[role="menuitem"]:has-text("Upload File")'); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 5bec9cdef..4deb3e30e 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,29 +1,26 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("SQL CRUD", async () => { const databaseId = generateUniqueName("db"); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); - + const explorer = await waitForExplorer(); await explorer.click('[data-test="New Container"]'); await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="Container id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")'); + await explorer.click(`.nodeItem >> text=${databaseId}`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Container")'); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); - await explorer.click('[aria-label="Submit"]'); + await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index 33362c0e6..5dbfc9cfa 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,26 +1,25 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Tables CRUD", async () => { const tableId = generateUniqueName("table"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); + await page.waitForSelector('text="Querying databases"', { state: "detached" }); await explorer.click('[data-test="New Table"]'); await explorer.fill('[aria-label="Table id"]', tableId); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `[data-test="TablesDB"]`); - await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); + await explorer.click(`[data-test="TablesDB"]`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); - await explorer.click('[aria-label="Submit"]'); + await explorer.click('[aria-label="OK"]'); await expect(explorer).not.toHaveText(".dataResourceTree", tableId); }); diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index 397d46aa5..a83484ebb 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -3,7 +3,7 @@ import { ClientSecretCredential } from "@azure/identity"; import "../../less/hostedexplorer.less"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { updateUserContext } from "../../src/UserContext"; -import { get, listKeys } from "../../src/Utils/arm/generatedClients/2020-04-01/databaseAccounts"; +import { get, listKeys } from "../../src/Utils/arm/generatedClients/cosmos/databaseAccounts"; const resourceGroup = process.env.RESOURCE_GROUP || ""; const subscriptionId = process.env.SUBSCRIPTION_ID || ""; diff --git a/test/utils/safeClick.ts b/test/utils/safeClick.ts deleted file mode 100644 index d0c307bd0..000000000 --- a/test/utils/safeClick.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Frame } from "playwright"; - -export async function safeClick(page: Frame, selector: string): Promise { - // TODO: Remove. Playwright does this for you... mostly. - // But our knockout+react setup sometimes leaves dom nodes detached and even playwright can't recover. - // Resource tree is particually bad. - // Ideally this should only be added as a last resort - await page.waitForSelector(selector); - await page.waitForTimeout(5000); - await page.click(selector); -} diff --git a/test/utils/waitForExplorer.ts b/test/utils/waitForExplorer.ts new file mode 100644 index 000000000..30675a0cc --- /dev/null +++ b/test/utils/waitForExplorer.ts @@ -0,0 +1,9 @@ +import { Frame } from "playwright"; + +export const waitForExplorer = async (): Promise => { + await page.waitForSelector("iframe"); + await page.waitForTimeout(5000); + return page.frame({ + name: "explorer", + }); +}; diff --git a/tsconfig.json b/tsconfig.json index 29814265e..45097fb68 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,20 @@ "jest" ] }, + "typedocOptions": { + "entryPoints": [ + "./src/SelfServe/Documentation/Documentation.ts", + "./src/SelfServe/Documentation/SupportedFeatures.ts", + "./src/SelfServe/Decorators.tsx", + "./src/SelfServe/SelfServeTypes.ts", + "./src/SelfServe/SelfServeUtils.tsx", + "./src/SelfServe/SelfServeTelemetryProcessor.ts" + ], + "out": "docs", + "excludeInternal": true, + "includes": "./src/SelfServe/Documentation", + "disableSources": true + }, "include": [ "./src/**/*", "./utils/**/*" diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 99a0417ed..83bcdd350 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -12,6 +12,7 @@ "./src/AuthType.ts", "./src/Bindings/ReactBindingHandler.ts", "./src/Common/ArrayHashMap.ts", + "./src/Common/CollapsedResourceTree.tsx", "./src/Common/Constants.ts", "./src/Common/DeleteFeedback.ts", "./src/Common/DocumentUtility.ts", @@ -45,30 +46,37 @@ "./src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts", "./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts", "./src/Explorer/LazyMonaco.ts", + "./src/Explorer/Menus/NotificationConsole/ConsoleData.tsx", "./src/Explorer/Notebook/FileSystemUtil.ts", "./src/Explorer/Notebook/NTeractUtil.ts", + "./src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts", "./src/Explorer/Notebook/NotebookComponent/actions.ts", "./src/Explorer/Notebook/NotebookComponent/loadTransform.ts", "./src/Explorer/Notebook/NotebookComponent/reducers.ts", "./src/Explorer/Notebook/NotebookComponent/types.ts", - "./src/Explorer/Notebook/NotebookContentClient.ts", "./src/Explorer/Notebook/NotebookContentItem.ts", "./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx", "./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx", "./src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx", + "./src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/HoverableCell.tsx", "./src/Explorer/Notebook/NotebookUtil.ts", + "./src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx", + "./src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts", "./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.tsx", - "./src/Explorer/Panes/PaneComponents.ts", + "./src/Explorer/Panes/PanelContainerComponent.test.tsx", + "./src/Explorer/Panes/PanelContainerComponent.tsx", "./src/Explorer/Panes/PanelFooterComponent.tsx", "./src/Explorer/Panes/PanelInfoErrorComponent.tsx", "./src/Explorer/Panes/PanelLoadingScreen.tsx", + "./src/Explorer/Panes/PanelStyles.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts", "./src/Explorer/Tables/Constants.ts", + "./src/Explorer/Tables/CqlUtilities.test.ts", "./src/Explorer/Tables/CqlUtilities.ts", "./src/Explorer/Tables/DataTable/CacheBase.ts", "./src/Explorer/Tables/Entities.ts", @@ -77,22 +85,24 @@ "./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/GitHub/GitHubConnector.ts", "./src/HostedExplorerChildFrame.ts", - "./src/Index.ts", - "./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts", "./src/Platform/Hosted/Authorization.ts", "./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.tsx", "./src/Platform/Hosted/Components/SignInButton.tsx", + "./src/Platform/Hosted/HostedUtils.test.ts", + "./src/Platform/Hosted/HostedUtils.ts", "./src/Platform/Hosted/extractFeatures.test.ts", "./src/Platform/Hosted/extractFeatures.ts", "./src/ReactDevTools.ts", - "./src/ResourceProvider/IResourceProviderClient.ts", + "./src/SelfServe/Example/SelfServeExample.types.ts", "./src/SelfServe/SelfServeStyles.tsx", "./src/SelfServe/SqlX/SqlxTypes.ts", "./src/Shared/Constants.ts", "./src/Shared/DefaultExperienceUtility.ts", "./src/Shared/ExplorerSettings.ts", + "./src/Shared/LocalStorageUtility.ts", "./src/Shared/PriceEstimateCalculator.ts", + "./src/Shared/SessionStorageUtility.ts", "./src/Shared/StorageUtility.test.ts", "./src/Shared/StorageUtility.ts", "./src/Shared/StringUtility.test.ts", @@ -104,39 +114,56 @@ "./src/Utils/Base64Utils.test.ts", "./src/Utils/Base64Utils.ts", "./src/Utils/BlobUtils.ts", + "./src/Utils/CapabilityUtils.ts", + "./src/Utils/CloudUtils.ts", "./src/Utils/GitHubUtils.test.ts", "./src/Utils/GitHubUtils.ts", "./src/Utils/MessageValidation.test.ts", "./src/Utils/MessageValidation.ts", + "./src/Utils/NotificationConsoleUtils.ts", "./src/Utils/PricingUtils.ts", + "./src/Utils/StringUtils.test.ts", "./src/Utils/StringUtils.ts", "./src/Utils/StyleUtils.ts", "./src/Utils/WindowUtils.test.ts", "./src/Utils/WindowUtils.ts", + "./src/hooks/useConfig.ts", "./src/hooks/useDirectories.tsx", "./src/hooks/useFullScreenURLs.tsx", "./src/hooks/useGraphPhoto.tsx", + "./src/hooks/useNotebookSnapshotStore.ts", + "./src/hooks/usePortalAccessToken.tsx", + "./src/hooks/useNotificationConsole.ts", "./src/hooks/useObservable.ts", + "./src/hooks/useSidePanel.ts", "./src/i18n.ts", "./src/quickstart.ts", "./src/setupTests.ts", - "./src/userContext.test.ts" + "./src/userContext.test.ts", + "src/Common/EntityValue.tsx", + "./src/Platform/Hosted/Components/SwitchAccount.tsx", + "./src/Platform/Hosted/Components/SwitchSubscription.tsx" ], "include": [ "src/CellOutputViewer/transforms/**/*", "src/Common/Tooltip/**/*", "src/Controls/**/*", "src/Definitions/**/*", + "src/Explorer/Controls/Accordion/**/*", "src/Explorer/Controls/ErrorDisplayComponent/**/*", "src/Explorer/Controls/Header/**/*", "src/Explorer/Controls/RadioSwitchComponent/**/*", "src/Explorer/Controls/ResizeSensorReactComponent/**/*", "src/Explorer/Graph/GraphExplorerComponent/__mocks__/**/*", + "src/Explorer/Menus/NavBar/**/*", "src/Explorer/Notebook/NotebookComponent/__mocks__/**/*", + "src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/**/*", + "src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/**/*", "src/Explorer/Panes/RightPaneForm/**/*", "src/Libs/**/*", "src/Localization/**/*", "src/Platform/Emulator/**/*", + "src/SelfServe/Documentation/**/*", "src/Shared/Telemetry/**/*", "src/Terminal/**/*", "src/Utils/arm/**/*" diff --git a/utils/armClientGenerator/generator.ts b/utils/armClientGenerator/generator.ts index 7a1d70c0b..dd3da3db0 100644 --- a/utils/armClientGenerator/generator.ts +++ b/utils/armClientGenerator/generator.ts @@ -15,12 +15,14 @@ But it does work well enough to generate a fully typed tree-shakeable client for Results of this file should be checked into the repo. */ +// CHANGE THESE VALUES TO GENERATE NEW CLIENTS +const version = "2021-04-15"; +const resourceName = "cosmosNotebooks"; +const schemaURL = `https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/${version}/notebook.json`; +const outputDir = path.join(__dirname, `../../src/Utils/arm/generatedClients/${resourceName}/${version}`); + // Array of strings to use for eventual output const outputTypes: string[] = [""]; -const version = "2020-04-01"; -const schemaURL = `https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/${version}/cosmos-db.json`; - -const outputDir = path.join(__dirname, `../../src/Utils/arm/generatedClients/${version}`); mkdirp.sync(outputDir); // Buckets for grouping operations based on their name @@ -33,7 +35,7 @@ const clients: { [key: string]: Client } = {}; // Mapping for OpenAPI types to TypeScript types const propertyMap: { [key: string]: string } = { - integer: "number" + integer: "number", }; // Converts a Open API reference: "#/definitions/Foo" to a type name: Foo @@ -84,6 +86,7 @@ function responseType(operation: Operation, namespace: string) { return refToType(operation.responses[responseCode].schema.$ref, namespace); }) .filter((value, index, array) => array.indexOf(value) === index) + .filter((value) => value !== "unknown") .join(" | "); } return "unknown"; @@ -98,6 +101,7 @@ interface Property { items?: { $ref: string; }; + enum?: string[]; allOf?: { $ref: string; }[]; @@ -129,6 +133,13 @@ const propertyToType = (property: Property, prop: string, required: boolean) => /* ${property.description || "undocumented"} */ ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${type} `); + } else if (property.enum) { + outputTypes.push(` + /* ${property.description || "undocumented"} */ + ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.enum + .map((v: string) => `"${v}"`) + .join(" | ")} + `); } else { if (property.type === undefined) { console.log(`UHANDLED TYPE: ${prop}. Falling back to unknown`); @@ -162,9 +173,6 @@ async function main() { } else { outputTypes.push(`export interface ${definition} {`); } - if (definition === "SqlDatabaseGetProperties") { - console.log(schema.definitions[definition]); - } for (const prop in schema.definitions[definition].properties) { const property = schema.definitions[definition].properties[prop]; propertyToType(property, prop, schema.definitions[definition].required?.includes(prop)); @@ -225,9 +233,9 @@ async function main() { // Write all grouped fetch functions to objects for (const clientName in clients) { const outputClient: string[] = [""]; - outputClient.push(`import { armRequest } from "../../request"\n`); + outputClient.push(`import { armRequest } from "../../../request"\n`); outputClient.push(`import * as Types from "./types"\n`); - outputClient.push(`import { configContext } from "../../../../ConfigContext";\n`); + outputClient.push(`import { configContext } from "../../../../../ConfigContext";\n`); outputClient.push(`const apiVersion = "${version}"\n\n`); for (const path of clients[clientName].paths) { for (const method in schema.paths[path]) { @@ -240,7 +248,7 @@ async function main() { /* ${operation.description || "undocumented"} */ export async function ${sanitize(camelize(methodName))} ( ${parametersFromPath(path) - .map(p => `${p}: string`) + .map((p) => `${p}: string`) .join(",\n")} ${bodyParam(bodyParameter, "Types")} ) : Promise<${responseType(operation, "Types")}> { @@ -268,13 +276,15 @@ function sanitize(name: string) { function writeOutputFile(outputPath: string, components: string[]) { components.unshift(`/* AUTOGENERATED FILE - Do not manually edit Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: ${schemaURL} */\n\n`); writeFileSync(path.join(outputDir, outputPath), components.join("")); } -main().catch(e => { +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/web.config b/web.config index 51421bfe9..5bf762418 100644 --- a/web.config +++ b/web.config @@ -42,6 +42,13 @@ + + + + + + + @@ -63,6 +70,13 @@ + + + + + + + diff --git a/webpack.config.js b/webpack.config.js index 12c980224..054b06984 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -198,7 +198,7 @@ module.exports = function (_env = {}, argv = {}) { mode: mode, entry: { main: "./src/Main.tsx", - index: "./src/Index.ts", + index: "./src/Index.tsx", quickstart: "./src/quickstart.ts", hostedExplorer: "./src/HostedExplorer.tsx", testExplorer: "./test/testExplorer/TestExplorer.ts",