Compare commits

..

8 Commits

Author SHA1 Message Date
Steve Faulkner
f0dc70faa8 Add runs on 2021-05-21 17:06:39 -05:00
Steve Faulkner
e2f4a857fa Add runs on 2021-05-21 16:40:33 -05:00
Steve Faulkner
3d9fb156cd Add runs on 2021-05-21 16:31:13 -05:00
Steve Faulkner
ab8e7e9ff1 Add runs on 2021-05-21 16:30:33 -05:00
Steve Faulkner
03e6e9980e Add runs on 2021-05-21 16:29:39 -05:00
Steve Faulkner
de7e6032a1 More 2021-05-21 16:28:56 -05:00
Steve Faulkner
cdf0fac3a4 Yaml :/ 2021-05-21 16:27:06 -05:00
Steve Faulkner
579353375d Enforce all GitHub status checks pass 2021-05-21 16:25:14 -05:00
409 changed files with 31010 additions and 18533 deletions

View File

@@ -1 +1,16 @@
PORTAL_RUNNER_USERNAME=
PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_CONNECTION_STRING=
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html

View File

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

View File

@@ -3,8 +3,8 @@ module.exports = {
browser: true, browser: true,
es6: true, es6: true,
}, },
plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks", "jsx-a11y"], plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jsx-a11y/recommended"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: { globals: {
Atomics: "readonly", Atomics: "readonly",
SharedArrayBuffer: "readonly", SharedArrayBuffer: "readonly",

View File

@@ -92,11 +92,11 @@ jobs:
name: dist name: dist
path: dist/ path: dist/
- name: Upload build to preview blob storage - 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 || github.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}}" --account-key="${PREVIEW_STORAGE_KEY}"
env: env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
- name: Upload preview config to blob storage - 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 || github.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}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
env: env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
endtoendemulator: endtoendemulator:
@@ -143,7 +143,7 @@ jobs:
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts - ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off - ./test/notebooks/upload.spec.ts
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:
@@ -214,3 +214,10 @@ jobs:
name: packages name: packages
with: with:
path: "*.nupkg" path: "*.nupkg"
allcheckspass:
name: "All Checks Pass"
runs-on: ubuntu-latest
steps:
- uses: "WyriHaximus/github-action-wait-for-status@v1.3.1"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -7,7 +7,7 @@ on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
# Once every hour # Once every hour
- cron: "0 15 * * *" - cron: "0 * * * *"
# A workflow run is made up of one or more jobs that can run sequentially or in parallel # A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:

View File

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

View File

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 842 B

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -1,8 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" <svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"> xmlns:xlink="http://www.w3.org/1999/xlink">
<path <path
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z" d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
transform="scale(0.5)" fill="#000" stroke="#000"> transform="scale(0.5)"
fill="#000"
stroke="#CCC"
>
</path> </path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 503 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -37,8 +37,8 @@ module.exports = {
global: { global: {
branches: 25, branches: 25,
functions: 25, functions: 25,
lines: 29.5, lines: 30,
statements: 29.5, statements: 30,
}, },
}, },

View File

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

View File

@@ -724,24 +724,45 @@ execute-sproc-params-pane {
.results-container, .results-container,
.errors-container { .errors-container {
padding: @MediumSpace 0px 0px @MediumSpace;
height: 100%; height: 100%;
.flex-display(); .flex-display();
.flex-direction(); .flex-direction();
overflow: hidden; overflow: hidden;
.enterInputParameters { .toggles {
padding: @LargeSpace @MediumSpace; height: @ToggleHeight;
width: @ToggleWidth;
margin-left: @MediumSpace;
&:focus {
.focus();
}
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
} }
div[role="tabpanel"] { .enterInputParameters {
height: 100%; padding: @LargeSpace @MediumSpace;
padding-bottom: 50px;
} }
} }
.errors-container { .errors-container {
padding-left: (2 * @MediumSpace); padding-left: (2 * @MediumSpace);
padding: @MediumSpace 0px 0px @MediumSpace;
.errors-header { .errors-header {
font-weight: 700; font-weight: 700;
font-size: @DefaultFontSize; font-size: @DefaultFontSize;
@@ -2357,8 +2378,6 @@ a:link {
height: 100%; height: 100%;
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
min-height: 300px;
overflow-y: scroll;
} }
.tabs { .tabs {
@@ -2834,8 +2853,6 @@ a:link {
#explorerNotificationConsole { #explorerNotificationConsole {
z-index: 1000; z-index: 1000;
overflow-y: auto;
overflow-x: clip;
} }
.uniqueIndexesContainer { .uniqueIndexesContainer {
@@ -3068,17 +3085,3 @@ settings-pane {
padding-left: @SmallSpace; padding-left: @SmallSpace;
} }
} }
.hiddenMain {
display: none;
height: 0px;
}
.spinner {
width: 100%;
position: absolute;
z-index: 1;
background: white;
height: 100%;
}
.moreOption {
color: #337ab7;
}

View File

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

View File

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

View File

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

4893
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,4 +1,3 @@
{ {
"PROXY_PATH": "/proxy", "PROXY_PATH": "/proxy"
"msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
} }

View File

@@ -62,17 +62,6 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
}) })
.catch(() => res.sendStatus(500)); .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, () => { app.listen(port, () => {
console.log(`Example app listening on port: ${port}`); console.log(`Example app listening on port: ${port}`);

View File

@@ -1,53 +0,0 @@
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import { userContext } from "../UserContext";
import { NormalizedEventKey } from "./Constants";
export interface CollapsedResourceTreeProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
}
export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: CollapsedResourceTreeProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (focusButton.current) {
focusButton.current.focus();
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return (
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
<div className="main-nav nav">
<ul className="nav">
<li className="resourceTreeCollapse" id="collapseToggleLeftPaneButton" aria-label="Expand Tree">
<span
className="leftarrowCollapsed"
role="button"
tabIndex={0}
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
ref={focusButton}
>
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span className="collectionCollapsed">
<span>{userContext.apiType} API</span>
</span>
</li>
</ul>
</div>
</div>
);
};

View File

@@ -94,9 +94,7 @@ export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly SchemaAnalyzer = "schemaanalyzer";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
public static readonly Phoenix = "phoenix";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -160,6 +158,16 @@ export class DocumentsGridMetrics {
public static DocumentEditorMaxWidthRatio: number = 0.4; 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 { export class Areas {
public static ResourceTree: string = "Resource Tree"; public static ResourceTree: string = "Resource Tree";
public static ContextualPane: string = "Contextual Pane"; public static ContextualPane: string = "Contextual Pane";
@@ -338,12 +346,6 @@ export enum ConflictOperationType {
Delete = "delete", Delete = "delete",
} }
export enum ConnectionStatusType {
Connecting = "Connecting",
Connected = "Connected",
Failed = "Connection Failed",
}
export const EmulatorMasterKey = export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
@@ -357,11 +359,6 @@ export class Notebook {
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 120000;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -83,7 +83,7 @@ export function client(): Cosmos.CosmosClient {
if (_client) return _client; if (_client) return _client;
const options: Cosmos.CosmosClientOptions = { const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey, ...(!userContext.features.enableAadDataPlane && { key: userContext.masterKey }),
tokenProvider, tokenProvider,
connectionPolicy: { connectionPolicy: {
enableEndpointDiscovery: false, enableEndpointDiscovery: false,

View File

@@ -1,17 +0,0 @@
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();
}

View File

@@ -32,7 +32,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
<DatePicker <DatePicker
className="addEntityDatePicker" className="addEntityDatePicker"
placeholder={entityValuePlaceholder} placeholder={entityValuePlaceholder}
value={entityValue ? new Date(entityValue) : new Date()} value={entityValue && new Date(entityValue)}
ariaLabel={entityValuePlaceholder} ariaLabel={entityValuePlaceholder}
onSelectDate={onSelectDate} onSelectDate={onSelectDate}
disabled={isEntityValueDisable} disabled={isEntityValueDisable}
@@ -40,6 +40,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
<TextField <TextField
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
id="entityTimeId" id="entityTimeId"
autoFocus
type="time" type="time"
value={entityTimeValue} value={entityTimeValue}
onChange={onEntityTimeValueChange} onChange={onEntityTimeValueChange}
@@ -54,10 +55,11 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
className="addEntityTextField" className="addEntityTextField"
id="entityValueId" id="entityValueId"
autoFocus
disabled={isEntityValueDisable} disabled={isEntityValueDisable}
type={entityValueType} type={entityValueType}
placeholder={entityValuePlaceholder} placeholder={entityValuePlaceholder}
value={typeof entityValue === "string" ? entityValue : ""} value={typeof entityValue === "string" && entityValue}
onChange={onEntityValueChange} onChange={onEntityValueChange}
/> />
); );

View File

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

View File

@@ -3,16 +3,9 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
deleteDocument, jest.mock("../ResourceProvider/ResourceProviderClient.ts");
getEndpoint,
getFeatureEndpointOrDefault,
queryDocuments,
readDocument,
updateDocument,
} from "./MongoProxyClient";
const databaseId = "testDB"; const databaseId = "testDB";
@@ -254,31 +247,4 @@ describe("MongoProxyClient", () => {
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
}); });
}); });
describe("getFeatureEndpointOrDefault", () => {
beforeEach(() => {
resetConfigContext();
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
});
const params = new URLSearchParams({
"feature.mongoProxyEndpoint": "https://localhost:12901",
"feature.mongoProxyAPIs": "readDocument|createDocument",
});
const features = extractFeatures(params);
updateUserContext({
authType: AuthType.AAD,
features: features,
});
});
it("returns a local endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("readDocument");
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
});
it("returns a production endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
});
});
}); });

View File

@@ -6,7 +6,6 @@ import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
@@ -79,7 +78,7 @@ export function queryDocuments(
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault("resourcelist") || ""; const endpoint = getEndpoint() || "";
const headers = { const headers = {
...defaultHeaders, ...defaultHeaders,
@@ -112,7 +111,7 @@ export function queryDocuments(
headers: response.headers, headers: response.headers,
}; };
} }
await errorHandling(response, "querying documents", params); errorHandling(response, "querying documents", params);
return undefined; return undefined;
}); });
} }
@@ -142,8 +141,7 @@ export function readDocument(
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault("readDocument"); const endpoint = getEndpoint();
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
method: "GET", method: "GET",
@@ -155,11 +153,11 @@ export function readDocument(
), ),
}, },
}) })
.then(async (response) => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return await errorHandling(response, "reading document", params); return errorHandling(response, "reading document", params);
}); });
} }
@@ -183,7 +181,7 @@ export function createDocument(
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
}; };
const endpoint = getFeatureEndpointOrDefault("createDocument"); const endpoint = getEndpoint();
return window return window
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, { .fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
@@ -194,11 +192,11 @@ export function createDocument(
...authHeaders(), ...authHeaders(),
}, },
}) })
.then(async (response) => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return await errorHandling(response, "creating document", params); return errorHandling(response, "creating document", params);
}); });
} }
@@ -227,7 +225,7 @@ export function updateDocument(
? documentId.partitionKeyProperty ? documentId.partitionKeyProperty
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault("updateDocument"); const endpoint = getEndpoint();
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -240,11 +238,11 @@ export function updateDocument(
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
}, },
}) })
.then(async (response) => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return await errorHandling(response, "updating document", params); return errorHandling(response, "updating document", params);
}); });
} }
@@ -268,7 +266,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
? documentId.partitionKeyProperty ? documentId.partitionKeyProperty
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault("deleteDocument"); const endpoint = getEndpoint();
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -280,11 +278,11 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
}, },
}) })
.then(async (response) => { .then((response) => {
if (response.ok) { if (response.ok) {
return undefined; return undefined;
} }
return await errorHandling(response, "deleting document", params); return errorHandling(response, "deleting document", params);
}); });
} }
@@ -311,7 +309,7 @@ export function createMongoCollectionWithProxy(
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(), autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
}; };
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy"); const endpoint = getEndpoint();
return window return window
.fetch( .fetch(
@@ -327,23 +325,16 @@ export function createMongoCollectionWithProxy(
}, },
} }
) )
.then(async (response) => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return await errorHandling(response, "creating collection", mongoParams); return errorHandling(response, "creating collection", mongoParams);
}); });
} }
export function getFeatureEndpointOrDefault(feature: string): string { export function getEndpoint(): string {
return hasFlag(userContext.features.mongoProxyAPIs, feature) let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
? getEndpoint(userContext.features.mongoProxyEndpoint)
: getEndpoint();
}
export function getEndpoint(customEndpoint?: string): string {
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
url += "/api/mongo/explorer";
if (userContext.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo"); url = url.replace("api/mongo", "api/guest/mongo");

View File

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

View File

@@ -1,85 +0,0 @@
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } 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";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
container: Explorer;
}
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
container,
}: ResourceTreeContainerProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (isLeftPaneExpanded) {
if (focusButton.current) {
focusButton.current.focus();
}
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */}
<div id="mainslide" className="flexContainer">
{/* Collections Window Title/Command Bar - Start */}
<div className="collectiontitle">
<div className="coltitle">
<span className="titlepadcol">{userContext.apiType} API</span>
<div className="float-right">
<span
className="padimgcolrefresh"
data-test="refreshTree"
role="button"
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0}
aria-label="Refresh tree"
title="Refresh tree"
>
<img className="refreshcol" src={refreshImg} alt="Refresh Tree" />
</span>
<span
className="padimgcolrefresh1"
id="expandToggleLeftPaneButton"
role="button"
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
tabIndex={0}
aria-label="Collapse Tree"
title="Collapse Tree"
ref={focusButton}
>
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span>
</div>
</div>
</div>
{userContext.authType === AuthType.ResourceToken ? (
<ResourceTokenTree />
) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : (
<ResourceTree container={container} />
)}
</div>
{/* Collections Window - End */}
</div>
);
};

View File

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

View File

@@ -96,6 +96,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<TextField <TextField
label={entityPropertyLabel && entityPropertyLabel} label={entityPropertyLabel && entityPropertyLabel}
id="entityPropertyId" id="entityPropertyId"
autoFocus
disabled={isPropertyTypeDisable} disabled={isPropertyTypeDisable}
placeholder={entityPropertyPlaceHolder} placeholder={entityPropertyPlaceHolder}
value={entityProperty} value={entityProperty}

View File

@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
return ( return (
<span> <span>
<TooltipHost content={children}> <TooltipHost content={children}>
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} /> <Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
</TooltipHost> </TooltipHost>
</span> </span>
); );

View File

@@ -66,15 +66,9 @@ export const Upload: FunctionComponent<UploadProps> = ({
onChange={onUpload} onChange={onUpload}
role="button" role="button"
/> />
<span <a href="#" id="fileImportLinkNotebook" onClick={onImportLinkClick} onKeyPress={onImportLinkKeyPress}>
id="fileImportLinkNotebook"
role="button"
tabIndex={0}
onClick={onImportLinkClick}
onKeyPress={onImportLinkKeyPress}
>
<Image className="fileImportImg" src={FolderIcon} alt={title} title={title} /> <Image className="fileImportImg" src={FolderIcon} alt={title} title={title} />
</span> </a>
</Stack> </Stack>
</div> </div>
); );

View File

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

View File

@@ -4,16 +4,20 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils"; import {
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; createUpdateCassandraTable,
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; getCassandraTable,
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import {
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@@ -55,16 +59,6 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
}; };
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
if (!params.createNewDatabase) {
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
if (!isValid) {
const collectionName = getCollectionName().toLocaleLowerCase();
throw new Error(
`Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists`
);
}
}
const { apiType } = userContext; const { apiType } = userContext;
switch (apiType) { switch (apiType) {
case "SQL": case "SQL":
@@ -83,6 +77,23 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams
}; };
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.SqlContainerResource = { const resource: ARMTypes.SqlContainerResource = {
id: params.collectionId, id: params.collectionId,
@@ -120,6 +131,23 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }]; 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 options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.MongoDBCollectionResource = { const resource: ARMTypes.MongoDBCollectionResource = {
id: params.collectionId, id: params.collectionId,
@@ -161,6 +189,23 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
}; };
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getCassandraTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.CassandraTableResource = { const resource: ARMTypes.CassandraTableResource = {
id: params.collectionId, id: params.collectionId,
@@ -188,6 +233,23 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams):
}; };
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getGremlinGraph(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.GremlinGraphResource = { const resource: ARMTypes.GremlinGraphResource = {
id: params.collectionId, id: params.collectionId,
@@ -222,6 +284,22 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise<D
}; };
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.TableResource = { const resource: ARMTypes.TableResource = {
id: params.collectionId, id: params.collectionId,

View File

@@ -2,13 +2,20 @@ import { DatabaseResponse } from "@azure/cosmos";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getDatabaseName } from "../../Utils/APITypeUtils"; import {
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; createUpdateCassandraKeyspace,
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; getCassandraKeyspace,
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import {
createUpdateGremlinDatabase,
getGremlinDatabase,
} from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import {
createUpdateMongoDBDatabase,
getMongoDBDatabase,
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { import {
CassandraKeyspaceCreateUpdateParameters, CassandraKeyspaceCreateUpdateParameters,
CreateUpdateOptions, CreateUpdateOptions,
@@ -41,11 +48,6 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
} }
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
const databaseName = getDatabaseName().toLocaleLowerCase();
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
}
const { apiType } = userContext; const { apiType } = userContext;
switch (apiType) { switch (apiType) {
@@ -63,6 +65,22 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P
} }
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = { const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: { properties: {
@@ -83,6 +101,22 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
} }
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getMongoDBDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: MongoDBDatabaseCreateUpdateParameters = { const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
properties: { properties: {
@@ -103,6 +137,22 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
} }
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: CassandraKeyspaceCreateUpdateParameters = { const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
properties: { properties: {
@@ -123,6 +173,22 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
} }
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getGremlinDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: GremlinDatabaseCreateUpdateParameters = { const rpPayload: GremlinDatabaseCreateUpdateParameters = {
properties: { properties: {

View File

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

View File

@@ -1,5 +1,3 @@
import { ConnectionStatusType } from "../Common/Constants";
export interface DatabaseAccount { export interface DatabaseAccount {
id: string; id: string;
name: string; name: string;
@@ -11,7 +9,6 @@ export interface DatabaseAccount {
export interface DatabaseAccountExtendedProperties { export interface DatabaseAccountExtendedProperties {
documentEndpoint?: string; documentEndpoint?: string;
disableLocalAuth?: boolean;
tableEndpoint?: string; tableEndpoint?: string;
gremlinEndpoint?: string; gremlinEndpoint?: string;
cassandraEndpoint?: string; cassandraEndpoint?: string;
@@ -25,7 +22,6 @@ export interface DatabaseAccountExtendedProperties {
enableAnalyticalStorage?: boolean; enableAnalyticalStorage?: boolean;
isVirtualNetworkFilterEnabled?: boolean; isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[]; ipRules?: IpRule[];
privateEndpointConnections?: unknown[];
} }
export interface DatabaseAccountResponseLocation { export interface DatabaseAccountResponseLocation {
@@ -395,6 +391,16 @@ export interface GeospatialConfig {
type: string; type: string;
} }
export interface GatewayDatabaseAccount {
MediaLink: string;
DatabasesLink: string;
MaxMediaStorageUsageInMB: number;
CurrentMediaStorageUsageInMB: number;
EnableMultipleWriteLocations?: boolean;
WritableLocations: RegionEndpoint[];
ReadableLocations: RegionEndpoint[];
}
export interface RegionEndpoint { export interface RegionEndpoint {
name: string; name: string;
documentAccountEndpoint: string; documentAccountEndpoint: string;
@@ -415,6 +421,13 @@ export interface AccountKeys {
secondaryReadonlyMasterKey: string; secondaryReadonlyMasterKey: string;
} }
export interface AfecFeature {
id: string;
name: string;
properties: { state: string };
type: string;
}
export interface OperationStatus { export interface OperationStatus {
status: string; status: string;
id?: string; id?: string;
@@ -494,12 +507,92 @@ export interface MongoParameters extends RpParameters {
analyticalStorageTtl?: number; 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 { export interface MemoryUsageInfo {
freeKB: number; freeKB: number;
totalKB: number; totalKB: number;
} }
export interface ContainerConnectionInfo {
status: ConnectionStatusType;
//need to add ram and rom info
}

View File

@@ -5,8 +5,9 @@ import {
TriggerDefinition, TriggerDefinition,
UserDefinedFunctionDefinition, UserDefinedFunctionDefinition,
} from "@azure/cosmos"; } from "@azure/cosmos";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData"; import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient"; import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import ConflictId from "../Explorer/Tree/ConflictId"; import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
@@ -14,7 +15,6 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger"; import Trigger from "../Explorer/Tree/Trigger";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { SelfServeType } from "../SelfServe/SelfServeUtils"; import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { CollectionCreationDefaults } from "../UserContext";
import { SqlTriggerResource } from "../Utils/arm/generatedClients/cosmos/types"; import { SqlTriggerResource } from "../Utils/arm/generatedClients/cosmos/types";
import * as DataModels from "./DataModels"; import * as DataModels from "./DataModels";
import { SubscriptionType } from "./SubscriptionType"; import { SubscriptionType } from "./SubscriptionType";
@@ -89,6 +89,7 @@ export interface Database extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>; selectedSubnodeKind: ko.Observable<CollectionTabKind>;
selectDatabase(): void;
expandDatabase(): Promise<void>; expandDatabase(): Promise<void>;
collapseDatabase(): void; collapseDatabase(): void;
@@ -274,6 +275,8 @@ export interface TabOptions {
tabKind: CollectionTabKind; tabKind: CollectionTabKind;
title: string; title: string;
tabPath: string; tabPath: string;
hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>; isTabsContentExpanded?: ko.Observable<boolean>;
onLoadStartKey?: number; onLoadStartKey?: number;
@@ -284,7 +287,6 @@ export interface TabOptions {
rid?: string; rid?: string;
node?: TreeNode; node?: TreeNode;
theme?: string; theme?: string;
index?: number;
} }
export interface DocumentsTabOptions extends TabOptions { export interface DocumentsTabOptions extends TabOptions {
@@ -409,6 +411,25 @@ export interface SelfServeFrameInputs {
flights?: readonly string[]; 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 { export class MonacoEditorSettings {
public readonly language: string; public readonly language: string;
public readonly readOnly: boolean; public readonly readOnly: boolean;

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
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);
});
});

View File

@@ -1,8 +1,12 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
ko.components.register("editor", new EditorComponent()); ko.components.register("editor", new EditorComponent());
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);

View File

@@ -0,0 +1,172 @@
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();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
});
}
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
label: "New Stored Procedure",
});
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
},
label: "New UDF",
});
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
},
label: "New Trigger",
});
}
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () => container.openDeleteCollectionConfirmationPane(),
label: 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",
},
];
}
}

View File

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

View File

@@ -8,9 +8,7 @@ import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg"; import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
export interface AccordionComponentProps { export interface AccordionComponentProps {}
children: React.ReactNode;
}
export class AccordionComponent extends React.Component<AccordionComponentProps> { export class AccordionComponent extends React.Component<AccordionComponentProps> {
public render(): JSX.Element { public render(): JSX.Element {
@@ -55,13 +53,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="accordionItemContainer"> <div className="accordionItemContainer">
<div <div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
className="accordionItemHeader"
onClick={this.onHeaderClick}
onKeyPress={this.onHeaderKeyPress}
role="button"
tabIndex={0}
>
{this.renderCollapseExpandIcon()} {this.renderCollapseExpandIcon()}
{this.props.title} {this.props.title}
</div> </div>
@@ -80,11 +72,13 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
className="expandCollapseIcon" className="expandCollapseIcon"
src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon} src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon}
alt="Hide" alt="Hide"
tabIndex={0}
role="button"
/> />
); );
} }
private onHeaderClick = (): void => { private onHeaderClick = (_event: React.MouseEvent<HTMLDivElement>): void => {
this.setState({ isExpanded: !this.state.isExpanded }); this.setState({ isExpanded: !this.state.isExpanded });
}; };

View File

@@ -0,0 +1,142 @@
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<HTMLElement> | React.KeyboardEvent<HTMLElement>,
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<ArcadiaMenuPickerProps, ArcadiaMenuPickerStates> {
constructor(props: ArcadiaMenuPickerProps) {
super(props);
this.state = {
selectedSparkPool: props.selectedSparkPool,
};
}
private _onSparkPoolClicked = (
e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
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<HTMLElement> | React.KeyboardEvent<HTMLElement>,
item: IContextualMenuItem
): boolean | void => {
this.props.onCreateNewWorkspaceClicked();
};
private _onCreateNewSparkPoolClicked = (
e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
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 (
<DefaultButton
text={this.state.selectedSparkPool || this.props.selectText || "Select a Spark pool"}
persistMenu={true}
className="arcadia-menu-picker"
menuProps={{
items: workspaceMenuItems,
}}
styles={dropdownStyle}
/>
);
}
}

View File

@@ -1,6 +1,5 @@
import { Icon, Label, Stack } from "@fluentui/react"; import { Icon, Label, Stack } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
export interface CollapsibleSectionProps { export interface CollapsibleSectionProps {
@@ -31,13 +30,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
} }
} }
private onKeyPress = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
this.toggleCollapsed();
event.stopPropagation();
}
};
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<> <>
@@ -47,11 +39,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
verticalAlign="center" verticalAlign="center"
tokens={accordionStackTokens} tokens={accordionStackTokens}
onClick={this.toggleCollapsed} onClick={this.toggleCollapsed}
onKeyPress={this.onKeyPress}
tabIndex={0}
aria-label="Advanced"
role="button"
aria-expanded={this.state.isExpanded}
> >
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} /> <Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
<Label>{this.props.title}</Label> <Label>{this.props.title}</Label>

View File

@@ -3,14 +3,9 @@
exports[`CollapsibleSectionComponent renders 1`] = ` exports[`CollapsibleSectionComponent renders 1`] = `
<Fragment> <Fragment>
<Stack <Stack
aria-expanded={true}
aria-label="Advanced"
className="collapsibleSection" className="collapsibleSection"
horizontal={true} horizontal={true}
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
tokens={ tokens={
Object { Object {
"childrenGap": 10, "childrenGap": 10,

View File

@@ -1,12 +1,15 @@
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. * React component for Command button component.
*/ */
import * as React from "react"; import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import { ArcadiaMenuPickerProps } from "../Arcadia/ArcadiaMenuPicker";
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 * Options for this component
@@ -111,6 +114,15 @@ export interface CommandButtonComponentProps {
* Aria-label for the button * Aria-label for the button
*/ */
ariaLabel: string; 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<CommandButtonComponentProps> { export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
@@ -121,7 +133,8 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
if (!this.dropdownElt || !this.expandButtonElt) { if (!this.dropdownElt || !this.expandButtonElt) {
return; return;
} }
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
const dropdownElt = $(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
} }
private onKeyPress(event: React.KeyboardEvent): boolean { private onKeyPress(event: React.KeyboardEvent): boolean {
@@ -188,13 +201,12 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
ref={(ref: HTMLElement) => { ref={(ref: HTMLElement) => {
this.expandButtonElt = ref; this.expandButtonElt = ref;
}} }}
role="button"
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)} onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
> >
<div className="commandDropdownLauncher"> <div className="commandDropdownLauncher">
<span className="partialSplitter" /> <span className="partialSplitter" />
<span className="expandDropdown"> <span className="expandDropdown">
<img src={CollapseChevronDownIcon} alt="Collapse down icon" /> <img src={CollapseChevronDownIcon} />
</span> </span>
</div> </div>
<div <div

View File

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

View File

@@ -0,0 +1,105 @@
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(<DefaultDirectoryDropdownComponent {...props} />);
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(<DefaultDirectoryDropdownComponent {...props} />);
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(<DefaultDirectoryDropdownComponent {...props} />);
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(<DefaultDirectoryDropdownComponent {...props} />);
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(<DefaultDirectoryDropdownComponent {...props} />);
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();
});
});

View File

@@ -0,0 +1,71 @@
/**
* 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<Tenant>;
defaultDirectoryId: string;
onDefaultDirectoryChange: (newDirectory: Tenant) => void;
}
export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDirectoryDropdownProps> {
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<IDropdownOption> = this.props.directories.map(
(dirc): IDropdownOption => {
return {
key: dirc.tenantId,
text: `${dirc.displayName}(${dirc.tenantId})`,
};
}
);
const dropDownOptions: Array<IDropdownOption> = [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 <Dropdown {...dropDownProps} />;
}
private _onDropdownChange = (e: React.FormEvent<HTMLDivElement>, 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);
};
}

View File

@@ -0,0 +1,36 @@
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<number>;
constructor(
private _dropdownProps: ko.Observable<DefaultDirectoryDropdownProps>,
private _listProps: ko.Observable<DirectoryListProps>
) {
this._dropdownProps.subscribe(() => this.forceRender());
this._listProps.subscribe(() => this.forceRender());
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return (
<div>
<div className="directoryDropdownContainer">
<DefaultDirectoryDropdownComponent {...this._dropdownProps()} />
</div>
<div className="directoryDivider" />
<div className="directoryListContainer">
<DirectoryListComponent {...this._listProps()} />
</div>
</div>
);
}
public forceRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -0,0 +1,78 @@
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(<DirectoryListComponent {...props} />);
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(<DirectoryListComponent {...props} />);
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(<DirectoryListComponent {...props} />);
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(<DirectoryListComponent {...props} />);
wrapper.find("button.directoryListButton").simulate("click");
expect(props.onNewDirectorySelected).toBeCalled();
expect(props.onNewDirectorySelected).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,126 @@
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<Tenant>;
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<DirectoryListProps, DirectoryListComponentState> {
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 (
<ScrollablePane data-is-scrollable="true">
<Sticky stickyPosition={StickyPositionType.Header}>
<TextField {...textFieldProps} />
</Sticky>
<List items={filteredItemsSelected} onRenderCell={this._onRenderCell} />
</ScrollablePane>
);
}
private _onFilterChanged = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, 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 (
<DefaultButton {...buttonProps}>
<div className="directoryListItem" data-is-focusable={true}>
<div className="directoryListItemName">{directory.displayName}</div>
<div className="directoryListItemId">{directory.tenantId}</div>
</div>
</DefaultButton>
);
};
private _onNewDirectoryClick = (e: React.MouseEvent<HTMLButtonElement>): 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);
};
}

View File

@@ -0,0 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test render renders with directories and default 1`] = `
<Dropdown
className="defaultDirectoryDropdown"
defaultSelectedKey="asdfghjklzxcvbnm9876543210"
label="Set your default directory"
onChange={[Function]}
options={
Array [
Object {
"key": "lastVisited",
"text": "Sign in to your last visited directory",
},
Object {
"key": "asdfghjklzxcvbnm9876543210",
"text": "Macrohard(asdfghjklzxcvbnm9876543210)",
},
Object {
"key": "",
"text": "()",
},
]
}
/>
`;
exports[`test render renders with directories and last visit default 1`] = `
<Dropdown
className="defaultDirectoryDropdown"
defaultSelectedKey="lastVisited"
label="Set your default directory"
onChange={[Function]}
options={
Array [
Object {
"key": "lastVisited",
"text": "Sign in to your last visited directory",
},
Object {
"key": "asdfghjklzxcvbnm9876543210",
"text": "Macrohard(asdfghjklzxcvbnm9876543210)",
},
Object {
"key": "",
"text": "()",
},
]
}
/>
`;
exports[`test render renders with directories but no default 1`] = `
<Dropdown
className="defaultDirectoryDropdown"
defaultSelectedKey="lastVisited"
label="Set your default directory"
onChange={[Function]}
options={
Array [
Object {
"key": "lastVisited",
"text": "Sign in to your last visited directory",
},
Object {
"key": "asdfghjklzxcvbnm9876543210",
"text": "Macrohard(asdfghjklzxcvbnm9876543210)",
},
Object {
"key": "",
"text": "()",
},
]
}
/>
`;
exports[`test render renders with no directories 1`] = `
<Dropdown
className="defaultDirectoryDropdown"
defaultSelectedKey="lastVisited"
label="Set your default directory"
onChange={[Function]}
options={
Array [
Object {
"key": "lastVisited",
"text": "Sign in to your last visited directory",
},
]
}
/>
`;

View File

@@ -0,0 +1,64 @@
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<DynamicListItem> = ko.observableArray<DynamicListItem>();
function buildListOptions(
items: ko.ObservableArray<DynamicListItem>,
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);
});
});
});

View File

@@ -0,0 +1,59 @@
@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;
}
}
}
}

View File

@@ -0,0 +1,117 @@
/**
* Dynamic list:
*
* Creates a list of dynamic inputs that can be populated and deleted.
*
* How to use in your markup:
* <dynamic-list params="{ listItems: anObservableArrayOfDynamicListItem, placeholder: 'Text to display in placeholder', ariaLabel: 'Text for aria-label', buttonText: 'Add item' }">
* </dynamic-list>
*
*/
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<DynamicListItem>;
/**
* 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<string>;
}
export class DynamicListViewModel extends WaitsForTemplateViewModel {
public placeholder: string;
public ariaLabel: string;
public buttonText: string;
public newItem: ko.Observable<string>;
public isTemplateReady: ko.Observable<boolean>;
public listItems: ko.ObservableArray<DynamicListItem>;
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<DynamicListItem>();
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,
};

View File

@@ -0,0 +1,34 @@
<div class="dynamicList" data-bind="setTemplateReady: true">
<div class="dynamicListContainer" data-bind="foreach: listItems">
<div class="dynamicListItem">
<input
id="uniqueKeyItems"
type="text"
autocomplete="off"
data-bind="value: value, attr: {placeholder: $parent.placeholder, 'aria-label': $parent.ariaLabel}"
/>
<span
class="dynamicListItemDelete"
title="Remove item"
role="button"
aria-label="Remove item"
tabindex="0"
data-bind="click: $parent.removeItem, event: { keydown: $parent.onRemoveItemKeyPress }"
>
<img src="/delete.svg" alt="Remove item" />
</span>
</div>
</div>
<div class="dynamicListItemNew">
<span
class="dynamicListItemAdd"
id="addUniqueKeyBtn"
role="button"
aria-label="Add unique key"
tabindex="0"
data-bind="click: addItem, event: { keydown: onAddItemKeyPress }"
>
<img src="/Add-property.svg" data-bind="attr: {alt: buttonText}" /> <span data-bind="text: buttonText"></span>
</span>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,12 @@
* Wrapper around Notebook server terminal * Wrapper around Notebook server terminal
*/ */
import postRobot from "post-robot";
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { TerminalProps } from "../../../Terminal/TerminalProps";
import { userContext } from "../../../UserContext";
import * as StringUtils from "../../../Utils/StringUtils"; import * as StringUtils from "../../../Utils/StringUtils";
import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface NotebookTerminalComponentProps { export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
@@ -15,69 +15,79 @@ export interface NotebookTerminalComponentProps {
} }
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> { export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
private terminalWindow: Window;
constructor(props: NotebookTerminalComponentProps) { constructor(props: NotebookTerminalComponentProps) {
super(props); super(props);
} }
componentDidMount(): void {
this.sendPropsToTerminalFrame();
}
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="notebookTerminalContainer"> <div className="notebookTerminalContainer">
<iframe <iframe
title="Terminal to Notebook Server" title="Terminal to Notebook Server"
onLoad={(event) => this.handleFrameLoad(event)} src={NotebookTerminalComponent.createNotebookAppSrc(this.props.notebookServerInfo, this.getTerminalParams())}
src="terminal.html"
/> />
</div> </div>
); );
} }
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void { public getTerminalParams(): Map<string, string> {
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow; let params: Map<string, string> = new Map<string, string>();
this.sendPropsToTerminalFrame(); params.set(TerminalQueryParams.Terminal, "true");
}
sendPropsToTerminalFrame(): void { const terminalEndpoint: string = this.tryGetTerminalEndpoint();
if (!this.terminalWindow) { if (terminalEndpoint) {
return; params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
} }
const props: TerminalProps = { return params;
terminalEndpoint: this.tryGetTerminalEndpoint(),
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
authToken: this.props.notebookServerInfo?.authToken,
subscriptionId: userContext.subscriptionId,
apiType: userContext.apiType,
authType: userContext.authType,
databaseAccount: userContext.databaseAccount,
};
postRobot.send(this.terminalWindow, "props", props, {
domain: window.location.origin,
});
} }
public tryGetTerminalEndpoint(): string | undefined { public tryGetTerminalEndpoint(): string | null {
let terminalEndpoint: string | undefined; let terminalEndpoint: string | null;
const notebookServerEndpoint = this.props.notebookServerInfo?.notebookServerEndpoint; const notebookServerEndpoint: string = this.props.notebookServerInfo.notebookServerEndpoint;
if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) { if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) {
// mongoEndpoint is only available for Mongo 3.6 and higher, fallback to documentEndpoint otherwise let mongoShellEndpoint: string = this.props.databaseAccount.properties.mongoEndpoint;
terminalEndpoint = if (!mongoShellEndpoint) {
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint; // mongoEndpoint is only available for Mongo 3.6 and higher.
// Fallback to documentEndpoint otherwise.
mongoShellEndpoint = this.props.databaseAccount.properties.documentEndpoint;
}
terminalEndpoint = mongoShellEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) { } else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint; terminalEndpoint = this.props.databaseAccount.properties.cassandraEndpoint;
} }
if (terminalEndpoint) { if (terminalEndpoint) {
return new URL(terminalEndpoint).host; return new URL(terminalEndpoint).host;
} }
return null;
}
return undefined; public static createNotebookAppSrc(
serverInfo: DataModels.NotebookWorkspaceConnectionInfo,
params: Map<string, string>
): string {
if (!serverInfo.notebookServerEndpoint) {
handleError(
"Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.",
"NotebookTerminalComponent/createNotebookAppSrc"
);
return "";
}
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
if (serverInfo.authToken && serverInfo.authToken.length > 0) {
params.set(TerminalQueryParams.Token, serverInfo.authToken);
}
params.set(TerminalQueryParams.SubscriptionId, userContext.subscriptionId);
let result: string = "terminal.html?";
for (let key of params.keys()) {
result += `${key}=${encodeURIComponent(params.get(key))}&`;
}
return result;
} }
} }

View File

@@ -1,49 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotebookTerminalComponent renders cassandra shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders mongo 3.2 shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders mongo 3.6 shell 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;
exports[`NotebookTerminalComponent renders terminal 1`] = `
<div
className="notebookTerminalContainer"
>
<iframe
onLoad={[Function]}
src="terminal.html"
title="Terminal to Notebook Server"
/>
</div>
`;

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
@import "../../../../less/Common/Constants.less";
.radioSwitchComponent {
cursor: pointer;
display: flex;
&>span:nth-child(n+2) {
margin-left: 10px;
}
.caption {
color: @BaseDark;
padding-left: @SmallSpace;
vertical-align: top;
}
}

View File

@@ -0,0 +1,51 @@
/**
* Horizontal switch component
*/
import { Icon } from "@fluentui/react";
import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants";
import "./RadioSwitchComponent.less";
export interface Choice {
key: string;
onSelect: () => void;
label: string;
}
export interface RadioSwitchComponentProps {
choices: Choice[];
selectedKey: string;
onSelectionKeyChange?: (newValue: string) => void;
}
export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> {
public render(): JSX.Element {
return (
<div className="radioSwitchComponent">
{this.props.choices.map((choice: Choice) => (
<span
tabIndex={0}
key={choice.key}
onClick={() => this.onSelect(choice)}
onKeyPress={(event) => this.onKeyPress(event, choice)}
>
<Icon iconName={this.props.selectedKey === choice.key ? "RadioBtnOn" : "RadioBtnOff"} />
<span className="caption">{choice.label}</span>
</span>
))}
</div>
);
}
private onSelect(choice: Choice): void {
this.props.onSelectionKeyChange && this.props.onSelectionKeyChange(choice.key);
choice.onSelect();
}
private onKeyPress(event: React.KeyboardEvent<HTMLSpanElement>, choice: Choice): void {
if (event.key === NormalizedEventKey.Enter || event.key === NormalizedEventKey.Space) {
this.onSelect(choice);
}
}
}

View File

@@ -0,0 +1,45 @@
/**
* Generic abstract React component that senses its dimensions.
* It updates its state and re-renders if dimensions change.
*/
import * as React from "react";
import * as ResizeSensor from "css-element-queries/src/ResizeSensor";
export abstract class ResizeSensorComponent<P, S> extends React.Component<P, S> {
private isSensing: boolean = false;
private resizeSensor: any;
public constructor(props: P) {
super(props);
}
protected abstract onDimensionsChanged(width: number, height: number): void;
protected abstract getSensorTarget(): HTMLElement;
public componentDidUpdate(): void {
if (this.isSensing) {
return;
}
const bar = this.getSensorTarget();
if (bar.clientWidth > 0 || bar.clientHeight > 0) {
const oldPosition = bar.style.position;
// TODO Find a better way to use constructor
this.resizeSensor = new (ResizeSensor as any)(bar, () => {
this.onDimensionsChanged(bar.clientWidth, bar.clientHeight);
});
this.isSensing = true;
// ResizeSensor.js sets position to 'relative' which makes the dropdown menu appear clipped.
// Undoing doesn't seem to affect resize sensing functionality.
bar.style.position = oldPosition;
}
}
public componentWillUnmount(): void {
if (!!this.resizeSensor) {
this.resizeSensor.detach();
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,45 +1,45 @@
import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import { Urls, StyleConstants } from "../../../Common/Constants";
import {
getPriceCurrency,
getCurrencySign,
getAutoscalePricePerRu,
getMultimasterMultiplier,
computeRUUsagePriceHourly,
getPricePerRu,
estimatedCostDisclaimer,
} from "../../../Utils/PricingUtils";
import { import {
DetailsList,
DetailsListLayoutMode,
DetailsRow,
ICheckboxStyles,
IChoiceGroupStyles,
IColumn,
IDetailsColumnStyles,
IDetailsListStyles,
IDetailsRowProps,
IDetailsRowStyles,
IDropdownStyles,
IMessageBarStyles,
ISeparatorStyles,
IStackProps,
IStackStyles,
IStackTokens,
ITextFieldStyles, ITextFieldStyles,
ITextStyles, ICheckboxStyles,
IStackProps,
IStackTokens,
IChoiceGroupStyles,
Link, Link,
Text,
IMessageBarStyles,
ITextStyles,
IDetailsRowStyles,
IStackStyles,
IDetailsListStyles,
IDropdownStyles,
ISeparatorStyles,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
SelectionMode, Stack,
Spinner, Spinner,
SpinnerSize, SpinnerSize,
Stack, DetailsList,
Text, IColumn,
SelectionMode,
DetailsListLayoutMode,
IDetailsRowProps,
DetailsRow,
IDetailsColumnStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import * as React from "react"; import { isDirtyTypes, isDirty } from "./SettingsUtils";
import { StyleConstants, Urls } from "../../../Common/Constants";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import {
computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu,
getCurrencySign,
getMultimasterMultiplier,
getPriceCurrency,
getPricePerRu,
} from "../../../Utils/PricingUtils";
import { isDirty, isDirtyTypes } from "./SettingsUtils";
export interface EstimatedSpendingDisplayProps { export interface EstimatedSpendingDisplayProps {
costType: JSX.Element; costType: JSX.Element;
@@ -65,7 +65,7 @@ export interface PriceBreakdown {
currencySign: string; currencySign: string;
} }
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } }; export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: { label: {
@@ -223,15 +223,14 @@ export const getRuPriceBreakdown = (
multimasterEnabled: isMultimaster, multimasterEnabled: isMultimaster,
isAutoscale: isAutoscale, isAutoscale: isAutoscale,
}); });
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster); const basePricePerRu: number = isAutoscale
const pricePerRu: number = isAutoscale ? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId);
: getPricePerRu(serverId, multimasterMultiplier);
return { return {
hourlyPrice, hourlyPrice: hourlyPrice,
dailyPrice: hourlyPrice * 24, dailyPrice: hourlyPrice * 24,
monthlyPrice: hourlyPrice * hoursInAMonth, monthlyPrice: hourlyPrice * hoursInAMonth,
pricePerRu, pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
currency: getPriceCurrency(serverId), currency: getPriceCurrency(serverId),
currencySign: getCurrencySign(serverId), currencySign: getCurrencySign(serverId),
}; };
@@ -272,7 +271,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement"> <Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "} and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
<Link href={Urls.autoscaleMigration}>Learn more</Link> <a href={Urls.autoscaleMigration}>Learn more</a>
</Text> </Text>
); );

View File

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

View File

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

View File

@@ -122,7 +122,7 @@ export class IndexingPolicyComponent extends React.Component<
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
)} )}
<div className="settingsV2IndexingPolicyEditor" ref={this.indexingPolicyDiv}></div> <div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack> </Stack>
); );
} }

View File

@@ -8,7 +8,6 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

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

View File

@@ -7,7 +7,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
import * as SharedConstants from "../../../../Shared/Constants"; import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext"; import { userContext } from "../../../../UserContext";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils"; import Explorer from "../../../Explorer";
import { import {
getTextFieldStyles, getTextFieldStyles,
getThroughputApplyLongDelayMessage, getThroughputApplyLongDelayMessage,
@@ -23,6 +23,7 @@ import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents
export interface ScaleComponentProps { export interface ScaleComponentProps {
collection: ViewModels.Collection; collection: ViewModels.Collection;
database: ViewModels.Database; database: ViewModels.Database;
container: Explorer;
isFixedContainer: boolean; isFixedContainer: boolean;
onThroughputChange: (newThroughput: number) => void; onThroughputChange: (newThroughput: number) => void;
throughput: number; throughput: number;
@@ -108,7 +109,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
}; };
public canThroughputExceedMaximumValue = (): boolean => { public canThroughputExceedMaximumValue = (): boolean => {
return !this.props.isFixedContainer && configContext.platform === Platform.Portal && !isRunningOnNationalCloud(); return (
!this.props.isFixedContainer &&
configContext.platform === Platform.Portal &&
!this.props.container.isRunningOnNationalCloud()
);
}; };
public getInitialNotificationElement = (): JSX.Element => { public getInitialNotificationElement = (): JSX.Element => {
@@ -197,12 +202,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
} }
private getFreeTierInfoMessage(): JSX.Element { private getFreeTierInfoMessage(): JSX.Element {
const freeTierLimits = SharedConstants.FreeTierLimits;
return ( return (
<Text> <Text>
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in With free tier, you will get the first 400 RU/s and 5 GB of storage in this account for free. To keep your
this account for free. To keep your account free, keep the total RU/s across all resources in the account to{" "} account free, keep the total RU/s across all resources in the account to 400 RU/s.
{freeTierLimits.RU} RU/s.
<Link <Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts" href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank" target="_blank"

View File

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

View File

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

View File

@@ -155,9 +155,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
this.state = { this.state = {
spendAckChecked: this.props.spendAckChecked, spendAckChecked: this.props.spendAckChecked,
exceedFreeTierThroughput: exceedFreeTierThroughput:
this.props.isFreeTierAccount && this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400,
!this.props.isAutoPilotSelected &&
this.props.throughput > SharedConstants.FreeTierLimits.RU,
}; };
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep; this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
@@ -443,9 +441,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
if (this.overrideWithAutoPilotSettings()) { if (this.overrideWithAutoPilotSettings()) {
this.props.onMaxAutoPilotThroughputChange(newThroughput); this.props.onMaxAutoPilotThroughputChange(newThroughput);
} else { } else {
this.setState({ this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 });
exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > SharedConstants.FreeTierLimits.RU,
});
this.props.onThroughputChange(newThroughput); this.props.onThroughputChange(newThroughput);
} }
}; };
@@ -585,7 +581,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }} messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={messageBarStyles} styles={messageBarStyles}
> >
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`} {
"Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale."
}
</MessageBar> </MessageBar>
)} )}
{this.props.getThroughputWarningMessage() && ( {this.props.getThroughputWarningMessage() && (

View File

@@ -20,7 +20,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -40,7 +39,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -75,7 +73,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -83,11 +80,11 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
> >
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<StyledLinkBase <a
href="https://aka.ms/cosmos-autoscale-migration" href="https://aka.ms/cosmos-autoscale-migration"
> >
Learn more Learn more
</StyledLinkBase> </a>
</Text> </Text>
</StyledMessageBar> </StyledMessageBar>
<StyledChoiceGroup <StyledChoiceGroup
@@ -189,7 +186,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -464,7 +460,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -13,6 +13,7 @@ exports[`IndexingPolicyComponent renders 1`] = `
/> />
<div <div
className="settingsV2IndexingPolicyEditor" className="settingsV2IndexingPolicyEditor"
tabIndex={0}
/> />
</Stack> </Stack>
`; `;

View File

@@ -16,7 +16,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -136,7 +136,6 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -413,7 +412,6 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -954,7 +952,6 @@ exports[`SubSettingsComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -1231,7 +1228,6 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

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

View File

@@ -159,7 +159,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -167,17 +166,16 @@ exports[`SettingsUtils functions render 1`] = `
> >
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<StyledLinkBase <a
href="https://aka.ms/cosmos-autoscale-migration" href="https://aka.ms/cosmos-autoscale-migration"
> >
Learn more Learn more
</StyledLinkBase> </a>
</Text> </Text>
<Text <Text
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -197,7 +195,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -210,7 +207,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -223,7 +219,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -235,7 +230,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -255,7 +249,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -271,7 +264,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -286,7 +278,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -300,7 +291,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -312,7 +302,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -332,7 +321,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -375,7 +363,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -391,7 +378,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -408,7 +394,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -1,6 +1,7 @@
@import "../../../../less/Common/Constants"; @import "../../../../less/Common/Constants";
.tabComponentContainer { .tabComponentContainer {
overflow: hidden;
height: 100%; height: 100%;
.flex-display(); .flex-display();
.flex-direction(); .flex-direction();

View File

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

View File

@@ -6,7 +6,6 @@ import { userContext } from "../../../../UserContext";
import { import {
calculateEstimateNumber, calculateEstimateNumber,
computeRUUsagePriceHourly, computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu, getAutoscalePricePerRu,
getCurrencySign, getCurrencySign,
getMultimasterMultiplier, getMultimasterMultiplier,
@@ -43,9 +42,11 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId); const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier); const pricePerRu = isAutoscale
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
: getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>; const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
if (isAutoscale) { if (isAutoscale) {
return ( return (

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