Compare commits

...

24 Commits

Author SHA1 Message Date
Laurent Nguyen
a17d71d76e Merge branch 'master' into languy-resource-tree-to-react 2021-03-16 16:27:30 +01:00
Steve Faulkner
732d7ce8fa Fix upload worker error display (#547) 2021-03-15 22:47:49 -05:00
Laurent Nguyen
faf2e3b559 Remove comment and test code 2021-03-15 17:35:35 +01:00
Steve Faulkner
254c551999 Test Explorer Improvements (#541) 2021-03-14 22:53:16 -05:00
Jordi Bunster
f86883de6c MostRecentActivity changes (#463)
This changes the public API a bit, so that recording activity (the most common use) is less involved.
2021-03-15 03:10:48 +00:00
Laurent Nguyen
7e992c2b17 Fix build issues and reformat 2021-03-12 14:01:35 +01:00
Laurent Nguyen
21b92ed4f8 Merge branch 'master' into languy-resource-tree-to-react 2021-03-12 11:21:01 +01:00
Laurent Nguyen
e5755dff39 Fix unit tests 2021-03-12 10:36:27 +01:00
Laurent Nguyen
e48a6a10cb Fix notebook updates issues 2021-03-11 16:22:59 +01:00
Steve Faulkner
62550f8d6a Deprecate Explorer Properties (#535) 2021-03-10 23:02:55 -06:00
hardiknai-techm
184910ee6c Resolve Lint errors in NotebookConfigurationUtils.ts (#490) 2021-03-10 21:47:08 -06:00
Steve Faulkner
9253ab1876 Run Emulator test only on master (#536) 2021-03-10 18:52:32 -06:00
Steve Faulkner
920c95b614 Fix cleanup databases task 2021-03-10 16:03:50 -06:00
Srinath Narayanan
1d98c83be5 Created selfServe landing page (#444)
* Portal changes for DedicatedGateway

Changes to support creation and deletion of DedicatedGateway resource.

Tested locally with various scenarios.

* Portal changes for DedicatedGateway. CR feedback

* Stylecop changes

* created selfServe.html landing page

* Removing TODO comments

* exposed baselineValues

* added getOnSaveNotification

* disable UI when onSave is taking place

* minro edits

* made polling optional

* added optional polling

* added default

* Added portal notifications

* merged more changes

* minor edits

* added label for description

* Added correlationids and polling of refresh

* Added correlationids and polling of refresh

* minor edit

* added label tooltip

* removed ClassInfo decorator

* Added dynamic decription

* added info and warninf types for description

* more changes to promise retry

* promise retry changes

* added spinner on selfserve load

* compile errors fixed

* New changes

* added operationstatus link

* merged sqlxEdits

* undid sqlx changes

* added completed notification

* passed retryInterval in notif options

* more retry changes

* more changes

* added polling on landing on the page

* edits for error display

* added keys blade link

* added link generation

* added link to blade

* Modified info and description

* fixed format errors

* added selfserve contract to output files

* addressed PR comments

Co-authored-by: Balaji Sridharan <fnbalaji@microsoft.com>
Co-authored-by: fnbalaji <75445927+fnbalaji@users.noreply.github.com>
2021-03-10 13:55:05 -08:00
Sunil Kumar Yadav
b85a20cbea Fix Lint errors in StringUtility.ts (#479) 2021-03-09 19:33:29 -06:00
hardiknai-techm
d0f6923d24 Configure onsave auto format eslint in vscode (#478) 2021-03-09 19:31:38 -06:00
Srinath Narayanan
ecdc41ada9 Added more selfserve changes (#443)
* exposed baselineValues

* added getOnSaveNotification

* disable UI when onSave is taking place

* added optional polling

* Added portal notifications

* minor edits

* added label for description

* Added correlationids and polling of refresh

* added label tooltip

* removed ClassInfo decorator

* Added dynamic decription

* added info and warninf types for description

* promise retry changes

* compile errors fixed

* merged sqlxEdits

* undid sqlx changes

* added completed notification

* passed retryInterval in notif options

* added polling on landing on the page

* edits for error display

* added link generation

* addressed PR comments

* modified test

* fixed compilation error
2021-03-09 16:07:23 -08:00
Tanuj Mittal
c1b74266eb Gallery fixes (#514)
- Fix COC overlay height
- Make standalone gallery usable on mobile devices

Before:
![image](https://user-images.githubusercontent.com/693092/110415215-81cd0680-80b7-11eb-8000-bd0b8536607a.png)

After:
![image](https://user-images.githubusercontent.com/693092/110415236-898cab00-80b7-11eb-8266-94a5718113fe.png)
2021-03-09 18:42:54 +00:00
hardiknai-techm
ef6ecf0a5f Resolve Lint errors in NavBar component (#528) 2021-03-09 12:34:01 -06:00
Sunil Kumar Yadav
b241771e69 fixed eslint of IteratorUtility (#469) 2021-03-09 12:27:24 -06:00
victor-meng
9d63a346e4 Fix issues in the add collection pane (#501) 2021-03-09 10:18:40 -08:00
hardiknai-techm
2e7c7440d3 Resolve Lint errors in CommandBarUtil.test.tsx (#529) 2021-03-09 10:20:14 -06:00
Sunil Kumar Yadav
641dae30a1 fix-eslint-NTeractUtil.ts (#493) 2021-03-09 10:17:02 -06:00
Laurent Nguyen
4480a7250d Remove ResourceTreeAdapter 2021-03-08 14:20:27 +01:00
82 changed files with 2281 additions and 1891 deletions

View File

@@ -14,8 +14,6 @@ src/Common/DataAccessUtilityBase.ts
src/Common/EditableUtility.ts src/Common/EditableUtility.ts
src/Common/HashMap.test.ts src/Common/HashMap.test.ts
src/Common/HashMap.ts src/Common/HashMap.ts
src/Common/IteratorUtilities.test.ts
src/Common/IteratorUtilities.ts
src/Common/Logger.test.ts src/Common/Logger.test.ts
src/Common/MessageHandler.test.ts src/Common/MessageHandler.test.ts
src/Common/MessageHandler.ts src/Common/MessageHandler.ts
@@ -101,7 +99,6 @@ 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/FileSystemUtil.ts src/Explorer/Notebook/FileSystemUtil.ts
src/Explorer/Notebook/NTeractUtil.ts
src/Explorer/Notebook/NotebookClientV2.ts src/Explorer/Notebook/NotebookClientV2.ts
src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts
src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts
@@ -251,8 +248,6 @@ src/Shared/ExplorerSettings.ts
src/Shared/PriceEstimateCalculator.ts src/Shared/PriceEstimateCalculator.ts
src/Shared/StorageUtility.test.ts src/Shared/StorageUtility.test.ts
src/Shared/StorageUtility.ts src/Shared/StorageUtility.ts
src/Shared/StringUtility.test.ts
src/Shared/StringUtility.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
@@ -263,7 +258,6 @@ src/TokenProviders/PortalTokenProvider.ts
src/TokenProviders/TokenProviderFactory.ts src/TokenProviders/TokenProviderFactory.ts
src/Utils/DatabaseAccountUtils.test.ts src/Utils/DatabaseAccountUtils.test.ts
src/Utils/DatabaseAccountUtils.ts src/Utils/DatabaseAccountUtils.ts
src/Utils/NotebookConfigurationUtils.ts
src/Utils/PricingUtils.test.ts src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts src/Utils/QueryUtils.ts
@@ -316,15 +310,7 @@ src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.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/CommandBarComponentAdapter.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
src/Explorer/Menus/NavBar/ControlBarComponent.tsx
src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx
src/Explorer/Menus/NavBar/MeControlComponent.test.tsx
src/Explorer/Menus/NavBar/MeControlComponent.tsx
src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx

View File

@@ -15,10 +15,10 @@ jobs:
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: node utils/codeMetrics.js - run: node utils/codeMetrics.js
env: env:
@@ -28,10 +28,10 @@ jobs:
name: "Compile TypeScript" name: "Compile TypeScript"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run compile - run: npm run compile
- run: npm run compile:strict - run: npm run compile:strict
@@ -40,10 +40,10 @@ jobs:
name: "Check Format" name: "Check Format"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run format:check - run: npm run format:check
lint: lint:
@@ -51,10 +51,10 @@ jobs:
name: "Lint" name: "Lint"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run lint - run: npm run lint
unittest: unittest:
@@ -62,10 +62,10 @@ jobs:
name: "Unit Tests" name: "Unit Tests"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run test - run: npm run test
build: build:
@@ -74,10 +74,10 @@ jobs:
name: "Build" name: "Build"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm run build:contracts - run: npm run build:contracts
- name: Restore Build Cache - name: Restore Build Cache
@@ -94,14 +94,14 @@ jobs:
path: dist/ path: dist/
endtoendemulator: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Emulator Tests"
needs: [lint, format, compile, unittest] if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- uses: southpolesteve/cosmos-emulator-github-action@v1 - uses: southpolesteve/cosmos-emulator-github-action@v1
- name: End to End Tests - name: End to End Tests
run: | run: |
@@ -125,10 +125,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 12.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Accessibility Check - name: Accessibility Check
run: | run: |
# Ubuntu gets mad when webpack runs too many files watchers # Ubuntu gets mad when webpack runs too many files watchers
@@ -163,6 +163,7 @@ jobs:
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
strategy: strategy:
fail-fast: false
matrix: matrix:
test-file: test-file:
- ./test/cassandra/container.spec.ts - ./test/cassandra/container.spec.ts

43
.vscode/settings.json vendored
View File

@@ -1,21 +1,26 @@
// Place your settings in this file to overwrite default and user settings. // Place your settings in this file to overwrite default and user settings.
{ {
"files.exclude": { "files.exclude": {
".vs": true, ".vs": true,
".vscode/**": true, ".vscode/**": true,
"*.trx": true, "*.trx": true,
"**/.DS_Store": true, "**/.DS_Store": true,
"**/.git": true, "**/.git": true,
"**/.hg": true, "**/.hg": true,
"**/.svn": true, "**/.svn": true,
"built/**": true, "built/**": true,
"coverage/**": true, "coverage/**": true,
"libs/**": true, "libs/**": true,
"node_modules/**": true, "node_modules/**": true,
"package-lock.json": true, "package-lock.json": true,
"quickstart/**": true, "quickstart/**": true,
"test/out/**": true, "test/out/**": true,
"workers/libs/**": true "workers/libs/**": true
}, },
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
} "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
}
}

View File

@@ -7,6 +7,7 @@
.main { .main {
height: 100%; height: 100%;
} }
border-right: 1px solid @BaseMedium;
} }
.resourceTreeScroll { .resourceTreeScroll {

110
package-lock.json generated
View File

@@ -949,14 +949,20 @@
} }
}, },
"node_modules/@babel/plugin-syntax-class-properties": { "node_modules/@babel/plugin-syntax-class-properties": {
"version": "7.12.1", "version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
"integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.10.4" "@babel/helper-plugin-utils": "^7.12.13"
} }
}, },
"node_modules/@babel/plugin-syntax-class-properties/node_modules/@babel/helper-plugin-utils": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
"integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
"dev": true
},
"node_modules/@babel/plugin-syntax-decorators": { "node_modules/@babel/plugin-syntax-decorators": {
"version": "7.12.1", "version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz",
@@ -1951,9 +1957,9 @@
} }
}, },
"node_modules/@istanbuljs/schema": { "node_modules/@istanbuljs/schema": {
"version": "0.1.2", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -2127,9 +2133,9 @@
} }
}, },
"node_modules/@jest/globals/node_modules/@types/yargs": { "node_modules/@jest/globals/node_modules/@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@@ -4908,9 +4914,9 @@
} }
}, },
"node_modules/@types/graceful-fs": { "node_modules/@types/graceful-fs": {
"version": "4.1.4", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
"integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@@ -15439,9 +15445,9 @@
} }
}, },
"node_modules/jest/node_modules/@types/yargs": { "node_modules/jest/node_modules/@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@@ -15756,9 +15762,9 @@
} }
}, },
"node_modules/jest/node_modules/fsevents": { "node_modules/jest/node_modules/fsevents": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"os": [ "os": [
@@ -16751,9 +16757,9 @@
} }
}, },
"node_modules/jest/node_modules/string-width": { "node_modules/jest/node_modules/string-width": {
"version": "4.2.0", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@@ -22760,11 +22766,6 @@
"safer-buffer": "^2.0.2", "safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0" "tweetnacl": "~0.14.0"
}, },
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -25098,8 +25099,7 @@
"node_modules/webcrypto-liner/node_modules/core-js": { "node_modules/webcrypto-liner/node_modules/core-js": {
"version": "3.8.3", "version": "3.8.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz",
"integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==", "integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q=="
"hasInstallScript": true
}, },
"node_modules/webfontloader": { "node_modules/webfontloader": {
"version": "1.6.28", "version": "1.6.28",
@@ -27019,12 +27019,20 @@
} }
}, },
"@babel/plugin-syntax-class-properties": { "@babel/plugin-syntax-class-properties": {
"version": "7.12.1", "version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
"integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.10.4" "@babel/helper-plugin-utils": "^7.12.13"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
"integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
"dev": true
}
} }
}, },
"@babel/plugin-syntax-decorators": { "@babel/plugin-syntax-decorators": {
@@ -27993,9 +28001,9 @@
} }
}, },
"@istanbuljs/schema": { "@istanbuljs/schema": {
"version": "0.1.2", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true "dev": true
}, },
"@jest/console": { "@jest/console": {
@@ -28135,9 +28143,9 @@
} }
}, },
"@types/yargs": { "@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@@ -30752,9 +30760,9 @@
} }
}, },
"@types/graceful-fs": { "@types/graceful-fs": {
"version": "4.1.4", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
"integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@@ -39218,9 +39226,9 @@
} }
}, },
"@types/yargs": { "@types/yargs": {
"version": "15.0.12", "version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@@ -39473,9 +39481,9 @@
} }
}, },
"fsevents": { "fsevents": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
@@ -40261,9 +40269,9 @@
} }
}, },
"string-width": { "string-width": {
"version": "4.2.0", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true, "dev": true,
"requires": { "requires": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",

View File

@@ -392,6 +392,9 @@ 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 MyNotebooksTitle = "My Notebooks";
public static readonly GitHubReposTitle = "GitHub repos";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -1,10 +1,10 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import { getErrorMessage } from "./ErrorHandlingUtils"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { userContext } from "../UserContext"; import { getErrorMessage } from "./ErrorHandlingUtils";
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;

View File

@@ -1,8 +1,9 @@
import { ARMError } from "../Utils/arm/request";
import { HttpStatusCodes } from "./Constants";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import { userContext } from "../UserContext";
import { ARMError } from "../Utils/arm/request";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { HttpStatusCodes } from "./Constants";
import { logError } from "./Logger"; import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -44,7 +45,7 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
const replaceKnownError = (errorMessage: string): string => { const replaceKnownError = (errorMessage: string): string => {
if ( if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal && userContext.subscriptionType === SubscriptionType.Internal &&
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0 errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
) { ) {
return "Database throughput is not supported for internal subscriptions."; return "Database throughput is not supported for internal subscriptions.";

View File

@@ -1,6 +1,8 @@
import { QueryResults } from "../Contracts/ViewModels"; import { QueryResults } from "../Contracts/ViewModels";
interface QueryResponse { interface QueryResponse {
// [Todo] remove any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resources: any[]; resources: any[];
hasMoreResults: boolean; hasMoreResults: boolean;
activityId: string; activityId: string;
@@ -16,6 +18,7 @@ export interface MinimalQueryIterator {
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
return documentsIterator.fetchNext().then((response) => { return documentsIterator.fetchNext().then((response) => {
const documents = response.resources; const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
const itemCount = (documents && documents.length) || 0; const itemCount = (documents && documents.length) || 0;
return { return {

View File

@@ -9,10 +9,10 @@ export interface DatabaseAccount {
} }
export interface DatabaseAccountExtendedProperties { export interface DatabaseAccountExtendedProperties {
documentEndpoint: string; documentEndpoint?: string;
tableEndpoint: string; tableEndpoint?: string;
gremlinEndpoint: string; gremlinEndpoint?: string;
cassandraEndpoint: string; cassandraEndpoint?: string;
configurationOverrides?: ConfigurationOverrides; configurationOverrides?: ConfigurationOverrides;
capabilities?: Capability[]; capabilities?: Capability[];
enableMultipleWriteLocations?: boolean; enableMultipleWriteLocations?: boolean;

View File

@@ -0,0 +1,9 @@
/**
* Messaging types used with SelfServe Component <-> Portal communication
* and Hosted <-> SelfServe Component communication
*/
export enum SelfServeMessageTypes {
TelemetryInfo = "TelemetryInfo",
Notification = "Notification",
}

View File

@@ -393,7 +393,16 @@ export interface DataExplorerInputsFrame {
isAuthWithresourceToken?: boolean; isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
selfServeType?: SelfServeType; }
export interface SelfServeFrameInputs {
selfServeType: SelfServeType;
databaseAccount: any;
subscriptionId: string;
resourceGroup: string;
authorizationToken: string;
csmEndpoint: string;
flights?: readonly string[];
} }
export interface CollectionCreationDefaults { export interface CollectionCreationDefaults {

View File

@@ -11,7 +11,7 @@
.publicGalleryTabContainer { .publicGalleryTabContainer {
position: relative; position: relative;
height: 100vh; min-height: 100vh;
} }
.publicGalleryTabOverlayContent { .publicGalleryTabOverlayContent {

View File

@@ -388,7 +388,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createSearchBarHeader(content: JSX.Element): JSX.Element { private createSearchBarHeader(content: JSX.Element): JSX.Element {
return ( return (
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}> <Stack horizontal wrap tokens={{ childrenGap: 20, padding: 10 }}>
<Stack.Item grow> <Stack.Item grow>
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} /> <SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
</Stack.Item> </Stack.Item>

View File

@@ -36,6 +36,7 @@ exports[`GalleryViewerComponent renders 1`] = `
"padding": 10, "padding": 10,
} }
} }
wrap={true}
> >
<StackItem <StackItem
grow={true} grow={true}
@@ -121,6 +122,7 @@ exports[`GalleryViewerComponent renders 1`] = `
"padding": 10, "padding": 10,
} }
} }
wrap={true}
> >
<StackItem <StackItem
grow={true} grow={true}

View File

@@ -986,6 +986,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -1018,26 +1019,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -1060,14 +1041,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
@@ -2195,6 +2168,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -2227,26 +2201,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -2269,14 +2223,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
@@ -3417,6 +3363,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -3449,26 +3396,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -3491,14 +3418,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
@@ -4626,6 +4545,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -4658,26 +4578,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -4700,14 +4600,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes"; import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: SmartUiDescriptor = {
@@ -18,10 +18,12 @@ describe("SmartUiComponent", () => {
{ {
id: "description", id: "description",
input: { input: {
labelTKey: undefined,
dataFieldName: "description", dataFieldName: "description",
type: "string", type: "string",
description: { description: {
textTKey: "this is an example description text.", textTKey: "this is an example description text.",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Click here for more information.", textTKey: "Click here for more information.",

View File

@@ -6,12 +6,13 @@ import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { TextField } from "office-ui-fabric-react/lib/TextField"; import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Text } from "office-ui-fabric-react/lib/Text"; import { Text } from "office-ui-fabric-react/lib/Text";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
import { Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react"; import { Label, Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
import * as InputUtils from "./InputUtils"; import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less"; import "./SmartUiComponent.less";
import { import {
ChoiceItem, ChoiceItem,
Description, Description,
DescriptionType,
Info, Info,
InputType, InputType,
InputTypeValue, InputTypeValue,
@@ -19,6 +20,7 @@ import {
SmartUiInput, SmartUiInput,
} from "../../../SelfServe/SelfServeTypes"; } from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next"; import { TFunction } from "i18next";
import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent";
/** /**
* Generic UX renderer * Generic UX renderer
@@ -29,15 +31,14 @@ import { TFunction } from "i18next";
*/ */
interface BaseDisplay { interface BaseDisplay {
labelTKey: string;
dataFieldName: string; dataFieldName: string;
errorMessage?: string; errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
} }
interface BaseInput extends BaseDisplay { interface BaseInput extends BaseDisplay {
labelTKey: string;
placeholderTKey?: string; placeholderTKey?: string;
errorMessage?: string;
} }
/** /**
@@ -67,7 +68,8 @@ interface ChoiceInput extends BaseInput {
} }
interface DescriptionDisplay extends BaseDisplay { interface DescriptionDisplay extends BaseDisplay {
description: Description; description?: Description;
isDynamicDescription?: boolean;
} }
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay; type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
@@ -123,25 +125,27 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
private renderInfo(info: Info): JSX.Element { private renderInfo(info: Info): JSX.Element {
return ( return (
<MessageBar styles={{ root: { width: 400 } }}> info && (
{this.props.getTranslation(info.messageTKey)} <Text>
{info.link && ( {this.props.getTranslation(info.messageTKey)}{" "}
<Link href={info.link.href} target="_blank"> {info.link && (
{this.props.getTranslation(info.link.textTKey)} <Link href={info.link.href} target="_blank">
</Link> {this.props.getTranslation(info.link.textTKey)}
)} </Link>
</MessageBar> )}
</Text>
)
); );
} }
private renderTextInput(input: StringInput): JSX.Element { private renderTextInput(input: StringInput, labelId: string): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as string; const value = this.props.currentValues.get(input.dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<div className="stringInputContainer"> <div className="stringInputContainer">
<TextField <TextField
id={`${input.dataFieldName}-textField-input`} id={`${input.dataFieldName}-textField-input`}
label={this.props.getTranslation(input.labelTKey)} aria-labelledby={labelId}
type="text" type="text"
value={value || ""} value={value || ""}
placeholder={this.props.getTranslation(input.placeholderTKey)} placeholder={this.props.getTranslation(input.placeholderTKey)}
@@ -149,32 +153,35 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onChange={(_, newValue) => this.props.onInputChange(input, newValue)} onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },
subComponentStyles: {
label: {
root: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
},
},
}} }}
/> />
</div> </div>
); );
} }
private renderDescription(input: DescriptionDisplay): JSX.Element { private renderDescription(input: DescriptionDisplay, labelId: string): JSX.Element {
const description = input.description; const dataFieldName = input.dataFieldName;
return ( const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description);
<Text id={`${input.dataFieldName}-text-display`}> if (!description) {
{this.props.getTranslation(input.description.textTKey)}{" "} return this.renderError("Description is not provided.");
}
const descriptionElement = (
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
{this.props.getTranslation(description.textTKey)}{" "}
{description.link && ( {description.link && (
<Link target="_blank" href={input.description.link.href}> <Link target="_blank" href={description.link.href}>
{this.props.getTranslation(input.description.link.textTKey)} {this.props.getTranslation(description.link.textTKey)}
</Link> </Link>
)} )}
</Text> </Text>
); );
if (description.type === DescriptionType.Text) {
return descriptionElement;
}
const messageBarType =
description.type === DescriptionType.InfoMessageBar ? MessageBarType.info : MessageBarType.warning;
return <MessageBar messageBarType={messageBarType}>{descriptionElement}</MessageBar>;
} }
private clearError(dataFieldName: string): void { private clearError(dataFieldName: string): void {
@@ -220,13 +227,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined; return undefined;
}; };
private renderNumberInput(input: NumberInput): JSX.Element { private renderNumberInput(input: NumberInput, labelId: string): JSX.Element {
const { labelTKey, min, max, dataFieldName, step } = input; const { labelTKey, min, max, dataFieldName, step } = input;
const props = { const props = {
label: this.props.getTranslation(labelTKey),
min: min, min: min,
max: max, max: max,
ariaLabel: labelTKey, ariaLabel: this.props.getTranslation(labelTKey),
step: step, step: step,
}; };
@@ -243,13 +249,8 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)} onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)} onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
labelPosition={Position.top} labelPosition={Position.top}
aria-labelledby={labelId}
disabled={disabled} disabled={disabled}
styles={{
label: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
}}
/> />
{this.state.errors.has(dataFieldName) && ( {this.state.errors.has(dataFieldName) && (
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar> <MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
@@ -266,10 +267,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onChange={(newValue) => this.props.onInputChange(input, newValue)} onChange={(newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },
titleLabel: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
valueLabel: SmartUiComponent.labelStyle, valueLabel: SmartUiComponent.labelStyle,
}} }}
/> />
@@ -280,13 +277,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
} }
private renderBooleanInput(input: BooleanInput): JSX.Element { private renderBooleanInput(input: BooleanInput, labelId: string): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean; const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<Toggle <Toggle
id={`${input.dataFieldName}-toggle-input`} id={`${input.dataFieldName}-toggle-input`}
label={this.props.getTranslation(input.labelTKey)} aria-labelledby={labelId}
checked={value || false} checked={value || false}
onText={this.props.getTranslation(input.trueLabelTKey)} onText={this.props.getTranslation(input.trueLabelTKey)}
offText={this.props.getTranslation(input.falseLabelTKey)} offText={this.props.getTranslation(input.falseLabelTKey)}
@@ -297,8 +294,8 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
); );
} }
private renderChoiceInput(input: ChoiceInput): JSX.Element { private renderChoiceInput(input: ChoiceInput, labelId: string): JSX.Element {
const { labelTKey, defaultKey, dataFieldName, choices, placeholderTKey } = input; const { defaultKey, dataFieldName, choices, placeholderTKey } = input;
const value = this.props.currentValues.get(dataFieldName)?.value as string; const value = this.props.currentValues.get(dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
let selectedKey = value ? value : defaultKey; let selectedKey = value ? value : defaultKey;
@@ -308,7 +305,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Dropdown <Dropdown
id={`${input.dataFieldName}-dropdown-input`} id={`${input.dataFieldName}-dropdown-input`}
label={this.props.getTranslation(labelTKey)} aria-labelledby={labelId}
selectedKey={selectedKey} selectedKey={selectedKey}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())} onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={this.props.getTranslation(placeholderTKey)} placeholder={this.props.getTranslation(placeholderTKey)}
@@ -319,40 +316,53 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}))} }))}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },
label: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
dropdown: SmartUiComponent.labelStyle, dropdown: SmartUiComponent.labelStyle,
}} }}
/> />
); );
} }
private renderError(input: AnyDisplay): JSX.Element { private renderError(errorMessage: string): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>; return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>;
} }
private renderDisplay(input: AnyDisplay): JSX.Element { private renderDisplayWithInfoBubble(input: AnyDisplay, info: Info): JSX.Element {
if (input.errorMessage) { if (input.errorMessage) {
return this.renderError(input); return this.renderError(input.errorMessage);
} }
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden; const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
if (inputHidden) { if (inputHidden) {
return <></>; return <></>;
} }
const labelId = `${input.dataFieldName}-label`;
return (
<Stack>
{input.labelTKey && (
<Label id={labelId}>
<ToolTipLabelComponent
label={this.props.getTranslation(input.labelTKey)}
toolTipElement={this.renderInfo(info)}
/>
</Label>
)}
{this.renderDisplay(input, labelId)}
</Stack>
);
}
private renderDisplay(input: AnyDisplay, labelId: string): JSX.Element {
switch (input.type) { switch (input.type) {
case "string": case "string":
if ("description" in input) { if ("description" in input || "isDynamicDescription" in input) {
return this.renderDescription(input as DescriptionDisplay); return this.renderDescription(input as DescriptionDisplay, labelId);
} }
return this.renderTextInput(input as StringInput); return this.renderTextInput(input as StringInput, labelId);
case "number": case "number":
return this.renderNumberInput(input as NumberInput); return this.renderNumberInput(input as NumberInput, labelId);
case "boolean": case "boolean":
return this.renderBooleanInput(input as BooleanInput); return this.renderBooleanInput(input as BooleanInput, labelId);
case "object": case "object":
return this.renderChoiceInput(input as ChoiceInput); return this.renderChoiceInput(input as ChoiceInput, labelId);
default: default:
throw new Error(`Unknown input type: ${input.type}`); throw new Error(`Unknown input type: ${input.type}`);
} }
@@ -363,10 +373,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item> <Stack.Item>{node.input && this.renderDisplayWithInfoBubble(node.input, node.info as Info)}</Stack.Item>
{node.info && this.renderInfo(node.info as Info)}
{node.input && this.renderDisplay(node.input)}
</Stack.Item>
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)} {node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack> </Stack>
); );

View File

@@ -9,25 +9,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
} }
> >
<StackItem> <StackItem />
<StyledMessageBarBase
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div <div
key="description" key="description"
> >
@@ -40,18 +22,21 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Text <Stack>
id="description-text-display" <Text
> aria-labelledby="description-label"
this is an example description text. id="description-text-display"
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
> >
Click here for more information. this is an example description text.
</StyledLinkBase>
</Text> <StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -67,53 +52,53 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack <Stack>
styles={ <StyledLabelBase
Object { id="throughput-label"
"root": Object { >
"width": 400, <ToolTipLabelComponent
}, label="Throughput (input)"
} />
} </StyledLabelBase>
tokens={ <Stack
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
}
}
disabled={true}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label="Throughput (input)"
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
styles={ styles={
Object { Object {
"label": Object { "root": Object {
"color": "#393939", "width": 400,
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
}, },
} }
} }
/> tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
}
}
disabled={true}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label=""
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -130,37 +115,39 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<div <Stack>
id="throughput2-slider-input" <StyledLabelBase
> id="throughput2-label"
<StyledSliderBase >
ariaLabel="Throughput (Slider)" <ToolTipLabelComponent
disabled={true} label="Throughput (Slider)"
label="Throughput (Slider)" />
max={500} </StyledLabelBase>
min={400} <div
onChange={[Function]} id="throughput2-slider-input"
step={10} >
styles={ <StyledSliderBase
Object { ariaLabel="Throughput (Slider)"
"root": Object { disabled={true}
"width": 400, max={500}
}, min={400}
"titleLabel": Object { onChange={[Function]}
"color": "#393939", step={10}
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", styles={
"fontSize": 12, Object {
"fontWeight": 600, "root": Object {
}, "width": 400,
"valueLabel": Object { },
"color": "#393939", "valueLabel": Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "color": "#393939",
"fontSize": 12, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
}, "fontSize": 12,
},
}
} }
} />
/> </div>
</div> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -197,35 +184,34 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<div <Stack>
className="stringInputContainer" <StyledLabelBase
> id="containerId-label"
<StyledTextFieldBase >
disabled={true} <ToolTipLabelComponent
id="containerId-textField-input" label="Container id"
label="Container id" />
onChange={[Function]} </StyledLabelBase>
styles={ <div
Object { className="stringInputContainer"
"root": Object { >
"width": 400, <StyledTextFieldBase
}, aria-labelledby="containerId-label"
"subComponentStyles": Object { disabled={true}
"label": Object { id="containerId-textField-input"
"root": Object { onChange={[Function]}
"color": "#393939", styles={
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", Object {
"fontSize": 12, "root": Object {
"fontWeight": 600, "width": 400,
},
}, },
}, }
} }
} type="text"
type="text" value=""
value="" />
/> </div>
</div> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -241,22 +227,31 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<StyledToggleBase <Stack>
checked={false} <StyledLabelBase
disabled={true} id="analyticalStore-label"
id="analyticalStore-toggle-input" >
label="Analytical Store" <ToolTipLabelComponent
offText="Disabled" label="Analytical Store"
onChange={[Function]} />
onText="Enabled" </StyledLabelBase>
styles={ <StyledToggleBase
Object { aria-labelledby="analyticalStore-label"
"root": Object { checked={false}
"width": 400, disabled={true}
}, id="analyticalStore-toggle-input"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
} }
} />
/> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -272,47 +267,50 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<StyledWithResponsiveMode <Stack>
disabled={true} <StyledLabelBase
id="database-dropdown-input" id="database-label"
label="Database" >
onChange={[Function]} <ToolTipLabelComponent
options={ label="Database"
Array [ />
Object { </StyledLabelBase>
"key": "db1", <StyledWithResponsiveMode
"text": "Database 1", aria-labelledby="database-label"
}, disabled={true}
Object { id="database-dropdown-input"
"key": "db2", onChange={[Function]}
"text": "Database 2", options={
}, Array [
Object { Object {
"key": "db3", "key": "db1",
"text": "Database 3", "text": "Database 1",
}, },
] Object {
} "key": "db2",
selectedKey="db2" "text": "Database 2",
styles={ },
Object { Object {
"dropdown": Object { "key": "db3",
"color": "#393939", "text": "Database 3",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", },
"fontSize": 12, ]
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"root": Object {
"width": 400,
},
} }
} selectedKey="db2"
/> styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -328,25 +326,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
} }
> >
<StackItem> <StackItem />
<StyledMessageBarBase
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div <div
key="description" key="description"
> >
@@ -359,18 +339,21 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Text <Stack>
id="description-text-display" <Text
> aria-labelledby="description-label"
this is an example description text. id="description-text-display"
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
> >
Click here for more information. this is an example description text.
</StyledLinkBase>
</Text> <StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -386,53 +369,53 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Stack <Stack>
styles={ <StyledLabelBase
Object { id="throughput-label"
"root": Object { >
"width": 400, <ToolTipLabelComponent
}, label="Throughput (input)"
} />
} </StyledLabelBase>
tokens={ <Stack
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
}
}
disabled={false}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label="Throughput (input)"
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
styles={ styles={
Object { Object {
"label": Object { "root": Object {
"color": "#393939", "width": 400,
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
}, },
} }
} }
/> tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
}
}
disabled={false}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label=""
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -449,36 +432,38 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<div <Stack>
id="throughput2-slider-input" <StyledLabelBase
> id="throughput2-label"
<StyledSliderBase >
ariaLabel="Throughput (Slider)" <ToolTipLabelComponent
label="Throughput (Slider)" label="Throughput (Slider)"
max={500} />
min={400} </StyledLabelBase>
onChange={[Function]} <div
step={10} id="throughput2-slider-input"
styles={ >
Object { <StyledSliderBase
"root": Object { ariaLabel="Throughput (Slider)"
"width": 400, max={500}
}, min={400}
"titleLabel": Object { onChange={[Function]}
"color": "#393939", step={10}
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", styles={
"fontSize": 12, Object {
"fontWeight": 600, "root": Object {
}, "width": 400,
"valueLabel": Object { },
"color": "#393939", "valueLabel": Object {
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "color": "#393939",
"fontSize": 12, "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
}, "fontSize": 12,
},
}
} }
} />
/> </div>
</div> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -515,34 +500,33 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<div <Stack>
className="stringInputContainer" <StyledLabelBase
> id="containerId-label"
<StyledTextFieldBase >
id="containerId-textField-input" <ToolTipLabelComponent
label="Container id" label="Container id"
onChange={[Function]} />
styles={ </StyledLabelBase>
Object { <div
"root": Object { className="stringInputContainer"
"width": 400, >
}, <StyledTextFieldBase
"subComponentStyles": Object { aria-labelledby="containerId-label"
"label": Object { id="containerId-textField-input"
"root": Object { onChange={[Function]}
"color": "#393939", styles={
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", Object {
"fontSize": 12, "root": Object {
"fontWeight": 600, "width": 400,
},
}, },
}, }
} }
} type="text"
type="text" value=""
value="" />
/> </div>
</div> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -558,21 +542,30 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<StyledToggleBase <Stack>
checked={false} <StyledLabelBase
id="analyticalStore-toggle-input" id="analyticalStore-label"
label="Analytical Store" >
offText="Disabled" <ToolTipLabelComponent
onChange={[Function]} label="Analytical Store"
onText="Enabled" />
styles={ </StyledLabelBase>
Object { <StyledToggleBase
"root": Object { aria-labelledby="analyticalStore-label"
"width": 400, checked={false}
}, id="analyticalStore-toggle-input"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
} }
} />
/> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@@ -588,46 +581,49 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<StyledWithResponsiveMode <Stack>
id="database-dropdown-input" <StyledLabelBase
label="Database" id="database-label"
onChange={[Function]} >
options={ <ToolTipLabelComponent
Array [ label="Database"
Object { />
"key": "db1", </StyledLabelBase>
"text": "Database 1", <StyledWithResponsiveMode
}, aria-labelledby="database-label"
Object { id="database-dropdown-input"
"key": "db2", onChange={[Function]}
"text": "Database 2", options={
}, Array [
Object { Object {
"key": "db3", "key": "db1",
"text": "Database 3", "text": "Database 1",
}, },
] Object {
} "key": "db2",
selectedKey="db2" "text": "Database 2",
styles={ },
Object { Object {
"dropdown": Object { "key": "db3",
"color": "#393939", "text": "Database 3",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", },
"fontSize": 12, ]
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"root": Object {
"width": 400,
},
} }
} selectedKey="db2"
/> styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>

View File

@@ -129,7 +129,6 @@ export interface ThroughputInputParams {
throughputModeRadioName: string; throughputModeRadioName: string;
maxAutoPilotThroughputSet: ViewModels.Editable<number>; maxAutoPilotThroughputSet: ViewModels.Editable<number>;
autoPilotUsageCost: ko.Computed<string>; autoPilotUsageCost: ko.Computed<string>;
showAutoPilot?: ko.Observable<boolean>;
overrideWithAutoPilotSettings: ko.Observable<boolean>; overrideWithAutoPilotSettings: ko.Observable<boolean>;
overrideWithProvisionedThroughputSettings: ko.Observable<boolean>; overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
freeTierExceedThroughputTooltip?: ko.Observable<string>; freeTierExceedThroughputTooltip?: ko.Observable<string>;
@@ -158,7 +157,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
public infoBubbleText: string | ko.Observable<string>; public infoBubbleText: string | ko.Observable<string>;
public label: ko.Observable<string>; public label: ko.Observable<string>;
public isFixed: boolean; public isFixed: boolean;
public showAutoPilot: ko.Observable<boolean>;
public isAutoPilotSelected: ko.Observable<boolean>; public isAutoPilotSelected: ko.Observable<boolean>;
public throughputAutoPilotRadioId: string; public throughputAutoPilotRadioId: string;
public throughputProvisionedRadioId: string; public throughputProvisionedRadioId: string;
@@ -202,7 +200,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
this.isFixed = !!options.isFixed; this.isFixed = !!options.isFixed;
this.infoBubbleText = options.infoBubbleText || ko.observable<string>(); this.infoBubbleText = options.infoBubbleText || ko.observable<string>();
this.label = options.label || ko.observable<string>(); this.label = options.label || ko.observable<string>();
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false); this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
this.isAutoPilotSelected.subscribe((value) => { this.isAutoPilotSelected.subscribe((value) => {
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {

View File

@@ -17,7 +17,7 @@
</div> </div>
<!-- ko if: !isFixed --> <!-- ko if: !isFixed -->
<div data-bind="visible: showAutoPilot" class="throughputModeContainer"> <div class="throughputModeContainer">
<input <input
class="throughputModeRadio" class="throughputModeRadio"
aria-label="Autopilot mode" aria-label="Autopilot mode"

View File

@@ -1,93 +1,86 @@
import React from "react";
import * as ComponentRegisterer from "./ComponentRegisterer";
import * as Constants from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import * as path from "path"; import * as path from "path";
import * as SharedConstants from "../Shared/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import _ from "underscore";
import AddCollectionPane from "./Panes/AddCollectionPane";
import AddDatabasePane from "./Panes/AddDatabasePane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane";
import NewVertexPane from "./Panes/NewVertexPane";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import Q from "q"; import Q from "q";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import React from "react";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import _ from "underscore";
import TerminalTab from "./Tabs/TerminalTab";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import * as Constants from "../Common/Constants";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import { ExplorerMetrics } from "../Common/Constants"; import { ExplorerMetrics } from "../Common/Constants";
import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { readCollection } from "../Common/dataAccess/readCollection";
import { FileSystemUtil } from "./Notebook/FileSystemUtil"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import { IGalleryItem } from "../Juno/JunoClient"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { sendMessage, sendCachedDataMessage } from "../Common/MessageHandler"; import { sendCachedDataMessage, sendMessage } from "../Common/MessageHandler";
import { QueriesClient } from "../Common/QueriesClient";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem, IPinnedRepo } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler";
import { appInsights } from "../Shared/appInsights";
import * as SharedConstants from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { updateUserContext, userContext } from "../UserContext";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; import AddCollectionPane from "./Panes/AddCollectionPane";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { QueriesClient } from "../Common/QueriesClient"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import { RouteHandler } from "../RouteHandlers/RouteHandler"; import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane"; import { SettingsPane } from "./Panes/SettingsPane";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { SplashScreen } from "./SplashScreen/SplashScreen";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { TabsManager } from "./Tabs/TabsManager";
import { UploadFilePane } from "./Panes/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter"; import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { ReactAdapter } from "../Bindings/ReactBindingHandler"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import UserDefinedFunction from "./Tree/UserDefinedFunction"; import TabsBase from "./Tabs/TabsBase";
import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger"; import Trigger from "./Tree/Trigger";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import UserDefinedFunction from "./Tree/UserDefinedFunction";
import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { appInsights } from "../Shared/appInsights";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -101,6 +94,10 @@ export interface ExplorerParams {
closeSidePanel: () => void; closeSidePanel: () => void;
closeDialog: () => void; closeDialog: () => void;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
onRefreshNotebookList: () => void;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
getMyNotebooksContentRoot: () => NotebookContentItem;
} }
export default class Explorer { export default class Explorer {
@@ -118,20 +115,55 @@ export default class Explorer {
public hasWriteAccess: ko.Observable<boolean>; public hasWriteAccess: ko.Observable<boolean>;
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
/**
* @deprecated
* Use userContext.databaseAccount instead
* */
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>; public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
/**
* @deprecated
* Use userContext.subscriptionType instead
* */
public subscriptionType: ko.Observable<SubscriptionType>; public subscriptionType: ko.Observable<SubscriptionType>;
/**
* @deprecated
* Use userContext.apiType instead
* */
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "SQL"
* */
public isPreferredApiDocumentDB: ko.Computed<boolean>; public isPreferredApiDocumentDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra"
* */
public isPreferredApiCassandra: ko.Computed<boolean>; public isPreferredApiCassandra: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isPreferredApiMongoDB: ko.Computed<boolean>; public isPreferredApiMongoDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
* */
public isPreferredApiGraph: ko.Computed<boolean>; public isPreferredApiGraph: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
* */
public isPreferredApiTable: ko.Computed<boolean>; public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isEnableMongoCapabilityPresent: ko.Computed<boolean>; public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>; public isServerlessEnabled: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public selfServeType: ko.Observable<SelfServeType>;
public canSaveQueries: ko.Computed<boolean>; public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>; public features: ko.Observable<any>;
public serverId: ko.Observable<string>; public serverId: ko.Observable<string>;
@@ -157,9 +189,11 @@ export default class Explorer {
public selectedCollectionId: ko.Computed<string>; public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>; public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
/**
* @deprecated
* Use a local loading state and spinner instead. Using a global isRefreshing state causes problems.
* */
public isRefreshingExplorer: ko.Observable<boolean>; public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter;
private selfServeComponentAdapter: SelfServeComponentAdapter;
// Resource Token // Resource Token
public resourceTokenDatabaseId: ko.Observable<string>; public resourceTokenDatabaseId: ko.Observable<string>;
@@ -243,11 +277,10 @@ export default class Explorer {
// React adapters // React adapters
private commandBarComponentAdapter: CommandBarComponentAdapter; private commandBarComponentAdapter: CommandBarComponentAdapter;
private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(params?: ExplorerParams) { constructor(public params?: ExplorerParams) {
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded; this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
this.setNotificationConsoleData = params?.setNotificationConsoleData; this.setNotificationConsoleData = params?.setNotificationConsoleData;
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted; this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
@@ -287,7 +320,6 @@ export default class Explorer {
} }
}); });
this.isAccountReady = ko.observable<boolean>(false); this.isAccountReady = ko.observable<boolean>(false);
this.selfServeType = ko.observable<SelfServeType>(undefined);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
this.arcadiaToken.subscribe((token: string) => { this.arcadiaToken.subscribe((token: string) => {
@@ -439,6 +471,7 @@ export default class Explorer {
databaseAccount databaseAccount
); );
this.defaultExperience(defaultExperience); this.defaultExperience(defaultExperience);
// TODO. Remove this entirely
updateUserContext({ updateUserContext({
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience), defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience),
}); });
@@ -662,7 +695,6 @@ export default class Explorer {
}); });
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.selfServeComponentAdapter = new SelfServeComponentAdapter(this);
this.loadQueryPane = new LoadQueryPane({ this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane", id: "loadquerypane",
@@ -745,99 +777,90 @@ export default class Explorer {
$(document.body).click(() => $(".commandDropdownContainer").hide()); $(document.body).click(() => $(".commandDropdownContainer").hide());
}); });
// TODO move this to API customization class switch (userContext.apiType) {
this.defaultExperience.subscribe((defaultExperience) => { case "SQL":
const defaultExperienceNormalizedString = ( this.addCollectionText("New Container");
defaultExperience || Constants.DefaultAccountExperience.Default this.addDatabaseText("New Database");
).toLowerCase(); this.collectionTitle("SQL API");
this.collectionTreeNodeAltText("Container");
switch (defaultExperienceNormalizedString) { this.deleteCollectionText("Delete Container");
case Constants.DefaultAccountExperience.DocumentDB.toLowerCase(): this.deleteDatabaseText("Delete Database");
this.addCollectionText("New Container"); this.addCollectionPane.title("Add Container");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionIdTitle("Container id");
this.collectionTitle("SQL API"); this.addCollectionPane.collectionWithThroughputInSharedTitle(
this.collectionTreeNodeAltText("Container"); "Provision dedicated throughput for this container"
this.deleteCollectionText("Delete Container"); );
this.deleteDatabaseText("Delete Database"); this.deleteCollectionConfirmationPane.title("Delete Container");
this.addCollectionPane.title("Add Container"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
this.addCollectionPane.collectionIdTitle("Container id"); this.refreshTreeTitle("Refresh containers");
this.addCollectionPane.collectionWithThroughputInSharedTitle( break;
"Provision dedicated throughput for this container" case "Mongo":
); this.addCollectionText("New Collection");
this.deleteCollectionConfirmationPane.title("Delete Container"); this.addDatabaseText("New Database");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); this.collectionTitle("Collections");
this.refreshTreeTitle("Refresh containers"); this.collectionTreeNodeAltText("Collection");
break; this.deleteCollectionText("Delete Collection");
case Constants.DefaultAccountExperience.MongoDB.toLowerCase(): this.deleteDatabaseText("Delete Database");
case Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase(): this.addCollectionPane.title("Add Collection");
this.addCollectionText("New Collection"); this.addCollectionPane.collectionIdTitle("Collection id");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionWithThroughputInSharedTitle(
this.collectionTitle("Collections"); "Provision dedicated throughput for this collection"
this.collectionTreeNodeAltText("Collection"); );
this.deleteCollectionText("Delete Collection"); this.refreshTreeTitle("Refresh collections");
this.deleteDatabaseText("Delete Database"); break;
this.addCollectionPane.title("Add Collection"); case "Gremlin":
this.addCollectionPane.collectionIdTitle("Collection id"); this.addCollectionText("New Graph");
this.addCollectionPane.collectionWithThroughputInSharedTitle( this.addDatabaseText("New Database");
"Provision dedicated throughput for this collection" this.deleteCollectionText("Delete Graph");
); this.deleteDatabaseText("Delete Database");
this.refreshTreeTitle("Refresh collections"); this.collectionTitle("Gremlin API");
break; this.collectionTreeNodeAltText("Graph");
case Constants.DefaultAccountExperience.Graph.toLowerCase(): this.addCollectionPane.title("Add Graph");
this.addCollectionText("New Graph"); this.addCollectionPane.collectionIdTitle("Graph id");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.deleteCollectionText("Delete Graph"); this.deleteCollectionConfirmationPane.title("Delete Graph");
this.deleteDatabaseText("Delete Database"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id");
this.collectionTitle("Gremlin API"); this.refreshTreeTitle("Refresh graphs");
this.collectionTreeNodeAltText("Graph"); break;
this.addCollectionPane.title("Add Graph"); case "Tables":
this.addCollectionPane.collectionIdTitle("Graph id"); this.addCollectionText("New Table");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); this.addDatabaseText("New Database");
this.deleteCollectionConfirmationPane.title("Delete Graph"); this.deleteCollectionText("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id"); this.deleteDatabaseText("Delete Database");
this.refreshTreeTitle("Refresh graphs"); this.collectionTitle("Azure Table API");
break; this.collectionTreeNodeAltText("Table");
case Constants.DefaultAccountExperience.Table.toLowerCase(): this.addCollectionPane.title("Add Table");
this.addCollectionText("New Table"); this.addCollectionPane.collectionIdTitle("Table id");
this.addDatabaseText("New Database"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.deleteCollectionText("Delete Table"); this.refreshTreeTitle("Refresh tables");
this.deleteDatabaseText("Delete Database"); this.addTableEntityPane.title("Add Table Entity");
this.collectionTitle("Azure Table API"); this.editTableEntityPane.title("Edit Table Entity");
this.collectionTreeNodeAltText("Table"); this.deleteCollectionConfirmationPane.title("Delete Table");
this.addCollectionPane.title("Add Table"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.addCollectionPane.collectionIdTitle("Table id"); this.tableDataClient = new TablesAPIDataClient();
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); break;
this.refreshTreeTitle("Refresh tables"); case "Cassandra":
this.addTableEntityPane.title("Add Table Entity"); this.addCollectionText("New Table");
this.editTableEntityPane.title("Edit Table Entity"); this.addDatabaseText("New Keyspace");
this.deleteCollectionConfirmationPane.title("Delete Table"); this.deleteCollectionText("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); this.deleteDatabaseText("Delete Keyspace");
this.tableDataClient = new TablesAPIDataClient(); this.collectionTitle("Cassandra API");
break; this.collectionTreeNodeAltText("Table");
case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): this.addCollectionPane.title("Add Table");
this.addCollectionText("New Table"); this.addCollectionPane.collectionIdTitle("Table id");
this.addDatabaseText("New Keyspace"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.deleteCollectionText("Delete Table"); this.refreshTreeTitle("Refresh tables");
this.deleteDatabaseText("Delete Keyspace"); this.addTableEntityPane.title("Add Table Row");
this.collectionTitle("Cassandra API"); this.editTableEntityPane.title("Edit Table Row");
this.collectionTreeNodeAltText("Table"); this.deleteCollectionConfirmationPane.title("Delete Table");
this.addCollectionPane.title("Add Table"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.addCollectionPane.collectionIdTitle("Table id"); this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.refreshTreeTitle("Refresh tables"); this.tableDataClient = new CassandraAPIDataClient();
this.addTableEntityPane.title("Add Table Row"); break;
this.editTableEntityPane.title("Edit Table Row"); }
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.tableDataClient = new CassandraAPIDataClient();
break;
}
});
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
this._initSettings(); this._initSettings();
@@ -857,7 +880,6 @@ export default class Explorer {
this.notebookManager.initialize({ this.notebookManager.initialize({
container: this, container: this,
notebookBasePath: this.notebookBasePath, notebookBasePath: this.notebookBasePath,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(), refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(), refreshNotebookList: () => this.refreshNotebookList(),
}); });
@@ -872,7 +894,6 @@ export default class Explorer {
this.isSparkEnabled = ko.observable(false); this.isSparkEnabled = ko.observable(false);
this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons());
this.resourceTree = new ResourceTreeAdapter(this);
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({ this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
notebookServerEndpoint: undefined, notebookServerEndpoint: undefined,
@@ -1407,20 +1428,6 @@ export default class Explorer {
return false; return false;
} }
public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void {
const selfServeFeature = inputs.features[Constants.Features.selfServeType];
if (selfServeFeature) {
// self serve type received from query string
const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType];
this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid);
} else if (inputs.selfServeType) {
// self serve type received from portal
this.selfServeType(inputs.selfServeType);
} else {
this.selfServeType(SelfServeType.none);
}
}
public configure(inputs: ViewModels.DataExplorerInputsFrame): void { public configure(inputs: ViewModels.DataExplorerInputsFrame): void {
if (inputs != null) { if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage. // In development mode, save the iframe message from the portal in session storage.
@@ -1429,8 +1436,6 @@ export default class Explorer {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
} }
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount || null; const databaseAccount = inputs.databaseAccount || null;
if (inputs.defaultCollectionThroughput) { if (inputs.defaultCollectionThroughput) {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput; this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
@@ -1446,22 +1451,6 @@ export default class Explorer {
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription ?? false); this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription ?? false);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken ?? false); this.isAuthWithResourceToken(inputs.isAuthWithresourceToken ?? false);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
this.setSelfServeType(inputs);
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId,
});
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
{ {
@@ -1718,7 +1707,7 @@ export default class Explorer {
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
promise promise
.then(() => this.resourceTree.triggerRender()) .then(() => this.params.onRefreshNotebookList())
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason));
return promise; return promise;
} }
@@ -1726,7 +1715,7 @@ export default class Explorer {
public async importAndOpen(path: string): Promise<boolean> { public async importAndOpen(path: string): Promise<boolean> {
const name = NotebookUtil.getName(path); const name = NotebookUtil.getName(path);
const item = NotebookUtil.createNotebookContentItem(name, path, "file"); const item = NotebookUtil.createNotebookContentItem(name, path, "file");
const parent = this.resourceTree.myNotebooksContentRoot; const parent = this.params.getMyNotebooksContentRoot();
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
const existingItem = _.find(parent.children, (node) => node.name === name); const existingItem = _.find(parent.children, (node) => node.name === name);
@@ -1743,7 +1732,8 @@ export default class Explorer {
} }
public async importAndOpenContent(name: string, content: string): Promise<boolean> { public async importAndOpenContent(name: string, content: string): Promise<boolean> {
const parent = this.resourceTree.myNotebooksContentRoot; // const parent = this.params.getMyNotebooksContentRoot();
const parent = this.params.getMyNotebooksContentRoot();
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
@@ -1928,7 +1918,6 @@ export default class Explorer {
return newNotebookFile; return newNotebookFile;
}); });
result.then(() => this.resourceTree.triggerRender());
return result; return result;
} }
@@ -1949,7 +1938,6 @@ export default class Explorer {
defaultInput: "", defaultInput: "",
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input), onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input),
}); });
result.then(() => this.resourceTree.triggerRender());
return result; return result;
} }
@@ -2105,12 +2093,14 @@ export default class Explorer {
return false; return false;
} }
}; };
private refreshNotebookList = async (): Promise<void> => { private refreshNotebookList = async (): Promise<void> => {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
return; return;
} }
await this.resourceTree.initialize(); this.params?.onRefreshNotebookList();
this.notebookManager?.refreshPinnedRepos(); this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) { if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
@@ -2173,7 +2163,7 @@ export default class Explorer {
throw new Error(error); throw new Error(error);
} }
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.params.getMyNotebooksContentRoot();
const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( const notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
@@ -2197,7 +2187,7 @@ export default class Explorer {
); );
return this.openNotebook(newFile); return this.openNotebook(newFile);
}) })
.then(() => this.resourceTree.triggerRender()) .then(() => this.params.onRefreshNotebookList())
.catch((error: any) => { .catch((error: any) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage);
@@ -2215,7 +2205,7 @@ export default class Explorer {
} }
public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.params.getMyNotebooksContentRoot();
this.uploadFilePane.openWithOptions({ this.uploadFilePane.openWithOptions({
paneTitle: "Upload file to notebook server", paneTitle: "Upload file to notebook server",
@@ -2246,7 +2236,7 @@ export default class Explorer {
}); });
} }
public refreshContentItem(item: NotebookContentItem): Promise<void> { public refreshContentItem(item: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled"; const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem"); handleError(error, "Explorer/refreshContentItem");

View File

@@ -1,5 +1,4 @@
import * as CommandBarUtil from "./CommandBarUtil"; import * as CommandBarUtil from "./CommandBarUtil";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar"; import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@@ -26,7 +25,7 @@ describe("CommandBarUtil tests", () => {
const converteds = CommandBarUtil.convertButton([btn], backgroundColor); const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
expect(converteds.length).toBe(1); expect(converteds.length).toBe(1);
const converted = converteds[0]; const converted = converteds[0];
expect(!converted.split); expect(converted.split).toBe(undefined);
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc); expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt); expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
expect(converted.text).toEqual(btn.commandButtonLabel); expect(converted.text).toEqual(btn.commandButtonLabel);
@@ -50,7 +49,7 @@ describe("CommandBarUtil tests", () => {
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor"); const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
expect(converteds.length).toBe(1); expect(converteds.length).toBe(1);
const converted = converteds[0]; const converted = converteds[0];
expect(converted.split); expect(converted.split).toBe(true);
expect(converted.subMenuProps.items.length).toBe(btn.children.length); expect(converted.subMenuProps.items.length).toBe(btn.children.length);
for (let i = 0; i < converted.subMenuProps.items.length; i++) { for (let i = 0; i < converted.subMenuProps.items.length; i++) {
expect(converted.subMenuProps.items[i].text).toEqual(btn.children[i].commandButtonLabel); expect(converted.subMenuProps.items[i].text).toEqual(btn.children[i].commandButtonLabel);
@@ -64,7 +63,6 @@ describe("CommandBarUtil tests", () => {
} }
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor"); const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
const keys = converteds.map((btn: ICommandBarItemProps) => btn.key);
const uniqueKeys = converteds const uniqueKeys = converteds
.map((btn: ICommandBarItemProps) => btn.key) .map((btn: ICommandBarItemProps) => btn.key)
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index); .filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
@@ -75,7 +73,7 @@ describe("CommandBarUtil tests", () => {
const btn = createButton(); const btn = createButton();
const backgroundColor = "backgroundColor"; const backgroundColor = "backgroundColor";
btn.commandButtonLabel = null; btn.commandButtonLabel = undefined;
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0]; let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
expect(converted.text).toEqual(btn.tooltipText); expect(converted.text).toEqual(btn.tooltipText);

View File

@@ -17,7 +17,7 @@ export class ControlBarComponent extends React.Component<ControlBarComponentProp
return commandButtonOptions.map( return commandButtonOptions.map(
(btn: CommandButtonComponentProps, index: number): JSX.Element => { (btn: CommandButtonComponentProps, index: number): JSX.Element => {
// Remove label // Remove label
btn.commandButtonLabel = null; btn.commandButtonLabel = undefined;
return CommandButtonComponent.renderButton(btn, `${index}`); return CommandButtonComponent.renderButton(btn, `${index}`);
} }
); );

View File

@@ -0,0 +1,86 @@
import { observable } from "knockout";
import { mostRecentActivity } from "./MostRecentActivity";
describe("MostRecentActivity", () => {
const accountId = "some account";
beforeEach(() => mostRecentActivity.clear(accountId));
it("Has no items at first", () => {
expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
});
it("Can record collections being opened", () => {
const collectionId = "some collection";
const databaseId = "some database";
const collection = {
id: observable(collectionId),
databaseId,
};
mostRecentActivity.collectionWasOpened(accountId, collection);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([
expect.objectContaining({
collectionId,
databaseId,
}),
]);
});
it("Can record notebooks being opened", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([expect.objectContaining(notebook)]);
});
it("Filters out duplicates", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
const sameNotebook = { name, path };
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook);
const activity = mostRecentActivity.getItems(accountId);
expect(activity.length).toEqual(1);
expect(activity).toEqual([expect.objectContaining(notebook)]);
});
it("Allows for multiple accounts", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
const anotherNotebook = { name: "Another " + name, path };
const anotherAccountId = "Another " + accountId;
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook);
expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]);
expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]);
});
it("Can store multiple distinct elements, in FIFO order", () => {
const name = "some notebook";
const path = "some path";
const first = { name, path };
const second = { name: "Another " + name, path };
const third = { name, path: "Another " + path };
mostRecentActivity.notebookWasItemOpened(accountId, first);
mostRecentActivity.notebookWasItemOpened(accountId, second);
mostRecentActivity.notebookWasItemOpened(accountId, third);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([third, second, first].map(expect.objectContaining));
});
});

View File

@@ -1,4 +1,6 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility"; import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
export enum Type { export enum Type {
OpenCollection, OpenCollection,
@@ -6,21 +8,18 @@ export enum Type {
} }
export interface OpenNotebookItem { export interface OpenNotebookItem {
type: Type.OpenNotebook;
name: string; name: string;
path: string; path: string;
} }
export interface OpenCollectionItem { export interface OpenCollectionItem {
type: Type.OpenCollection;
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
} }
export interface Item { type Item = OpenNotebookItem | OpenCollectionItem;
type: Type;
title: string;
description: string;
data: OpenNotebookItem | OpenCollectionItem;
}
// Update schemaVersion if you are going to change this interface // Update schemaVersion if you are going to change this interface
interface StoredData { interface StoredData {
@@ -32,7 +31,7 @@ interface StoredData {
* Stores most recent activity * Stores most recent activity
*/ */
class MostRecentActivity { class MostRecentActivity {
private static readonly schemaVersion: string = "1"; private static readonly schemaVersion: string = "2";
private static itemsMaxNumber: number = 5; private static itemsMaxNumber: number = 5;
private storedData: StoredData; private storedData: StoredData;
constructor() { constructor() {
@@ -92,7 +91,7 @@ class MostRecentActivity {
LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData)); LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData));
} }
public addItem(accountId: string, newItem: Item): void { private addItem(accountId: string, newItem: Item): void {
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable. // When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
// if (!accountId) { // if (!accountId) {
// return; // return;
@@ -111,6 +110,23 @@ class MostRecentActivity {
return this.storedData.itemsMap[accountId] || []; return this.storedData.itemsMap[accountId] || [];
} }
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
const collectionId = id();
this.addItem(accountId, {
type: Type.OpenCollection,
databaseId,
collectionId,
});
}
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
this.addItem(accountId, {
type: Type.OpenNotebook,
name,
path,
});
}
public clear(accountId: string): void { public clear(accountId: string): void {
delete this.storedData.itemsMap[accountId]; delete this.storedData.itemsMap[accountId];
this.saveToLocalStorage(); this.saveToLocalStorage();
@@ -128,11 +144,7 @@ class MostRecentActivity {
let index = -1; let index = -1;
for (let i = 0; i < itemsArray.length; i++) { for (let i = 0; i < itemsArray.length; i++) {
const currentItem = itemsArray[i]; const currentItem = itemsArray[i];
if ( if (JSON.stringify(currentItem) === JSON.stringify(item)) {
currentItem.title === item.title &&
currentItem.description === item.description &&
JSON.stringify(currentItem.data) === JSON.stringify(item.data)
) {
index = i; index = i;
break; break;
} }

View File

@@ -3,20 +3,18 @@ import { NotebookContentRecordProps, selectors } from "@nteract/core";
/** /**
* A bunch of utilities to interact with nteract * A bunch of utilities to interact with nteract
*/ */
export default class NTeractUtil { export function getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined {
public static getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined { if (!content) {
if (!content) {
return undefined;
}
const cellFocusedId = selectors.notebook.cellFocused(content.model);
if (cellFocusedId) {
const cell = selectors.notebook.cellById(content.model, { id: cellFocusedId });
if (cell) {
return cell.cell_type;
}
}
return undefined; return undefined;
} }
const cellFocusedId = selectors.notebook.cellFocused(content.model);
if (cellFocusedId) {
const cell = selectors.notebook.cellById(content.model, { id: cellFocusedId });
if (cell) {
return cell.cell_type;
}
}
return undefined;
} }

View File

@@ -29,7 +29,7 @@ import "@nteract/styles/global-variables.css";
import "react-table/react-table.css"; import "react-table/react-table.css";
import * as CdbActions from "./actions"; import * as CdbActions from "./actions";
import NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
export interface NotebookComponentBootstrapperOptions { export interface NotebookComponentBootstrapperOptions {
notebookClient: NotebookClientV2; notebookClient: NotebookClientV2;

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { AppState, ContentRef, selectors } from "@nteract/core"; import { AppState, ContentRef, selectors } from "@nteract/core";
import { connect } from "react-redux"; import { connect } from "react-redux";
import NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps { interface VirtualCommandBarComponentProps {
kernelSpecName: string; kernelSpecName: string;

View File

@@ -18,11 +18,13 @@ export class NotebookContentClient {
/** /**
* This updates the item and points all the children's parent to this item * This updates the item and points all the children's parent to this item
* @param item * @param item
* @return updated item
*/ */
public updateItemChildren(item: NotebookContentItem): Promise<void> { public updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
return this.fetchNotebookFiles(item.path).then((subItems) => { return this.fetchNotebookFiles(item.path).then((subItems) => {
item.children = subItems; item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item)); subItems.forEach((subItem) => (subItem.parent = item));
return item;
}); });
} }

View File

@@ -18,7 +18,6 @@ import { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { MemoryUsageInfo } from "../../Contracts/DataModels"; import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable"; import { ImmutableNotebook } from "@nteract/commutable";
@@ -30,7 +29,6 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
notebookBasePath: ko.Observable<string>; notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void; refreshCommandBarButtons: () => void;
refreshNotebookList: () => void; refreshNotebookList: () => void;
} }
@@ -107,8 +105,8 @@ export default class NotebookManager {
}); });
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos); // TODO Move this out of NotebookManager?
this.params.resourceTree.triggerRender(); this.params.container.params.initializeGitHubRepos(pinnedRepos);
}); });
this.refreshPinnedRepos(); this.refreshPinnedRepos();
} }

View File

@@ -214,7 +214,6 @@
maxAutoPilotThroughputSet: sharedAutoPilotThroughput, maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >
@@ -435,7 +434,6 @@
maxAutoPilotThroughputSet: autoPilotThroughput, maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFixedStorageSelected(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >

View File

@@ -749,12 +749,16 @@ export default class AddCollectionPane extends ContextualPaneBase {
return undefined; return undefined;
} }
if (this.isAutoPilotSelected()) { // return undefined if autopilot is selected for the new database/collection
return undefined; if (this.databaseCreateNew()) {
} // database is shared and autopilot is sleected for the database
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) {
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) { return undefined;
return undefined; }
// database is not shared and autopilot is selected for the collection
if (!this.databaseCreateNewShared() && this.isAutoPilotSelected()) {
return undefined;
}
} }
return this._getThroughput(); return this._getThroughput();

View File

@@ -149,7 +149,6 @@
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet, maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >

View File

@@ -166,7 +166,6 @@
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
costsVisible: costsVisible, costsVisible: costsVisible,
showAutoPilot: !isFreeTierAccount()
}" }"
> >
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>

View File

@@ -8,10 +8,9 @@ import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRight
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
import { IDropdownOption } from "office-ui-fabric-react"; import { IDropdownOption } from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants"; import { HttpStatusCodes, Notebook } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location { interface Location {
@@ -151,7 +150,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
name: ResourceTreeAdapter.MyNotebooksTitle, name: Notebook.MyNotebooksTitle,
path: this.container.getNotebookBasePath(), path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -159,7 +158,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
case "GitHub": case "GitHub":
parent = { parent = {
name: ResourceTreeAdapter.GitHubReposTitle, name: Notebook.GitHubReposTitle,
path: GitHubUtils.toContentUri( path: GitHubUtils.toContentUri(
this.selectedLocation.owner, this.selectedLocation.owner,
this.selectedLocation.repo, this.selectedLocation.repo,

View File

@@ -1,7 +1,6 @@
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as React from "react"; import * as React from "react";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { import {
Stack, Stack,
Label, Label,
@@ -13,6 +12,7 @@ import {
IRenderFunction, IRenderFunction,
ISelectableOption, ISelectableOption,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { Notebook } from "../../Common/Constants";
interface Location { interface Location {
type: "MyNotebooks" | "GitHub"; type: "MyNotebooks" | "GitHub";
@@ -70,8 +70,8 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
options.push({ options.push({
key: "MyNotebooks-Item", key: "MyNotebooks-Item",
text: ResourceTreeAdapter.MyNotebooksTitle, text: Notebook.MyNotebooksTitle,
title: ResourceTreeAdapter.MyNotebooksTitle, title: Notebook.MyNotebooksTitle,
data: { data: {
type: "MyNotebooks", type: "MyNotebooks",
} as Location, } as Location,
@@ -86,7 +86,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
options.push({ options.push({
key: "GitHub-Header", key: "GitHub-Header",
text: ResourceTreeAdapter.GitHubReposTitle, text: Notebook.GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header, itemType: SelectableOptionMenuItemType.Header,
}); });

View File

@@ -5,7 +5,7 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { StringUtility } from "../../Shared/StringUtility"; import * as StringUtility from "../../Shared/StringUtility";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
export class SettingsPane extends ContextualPaneBase { export class SettingsPane extends ContextualPaneBase {

View File

@@ -217,42 +217,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return heroes; return heroes;
} }
private getItemIcon(item: MostRecentActivity.Item): string {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection:
return CollectionIcon;
case MostRecentActivity.Type.OpenNotebook:
return NotebookIcon;
default:
return null;
}
}
private onItemClicked(item: MostRecentActivity.Item) {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection: {
const openCollectionitem = item.data as MostRecentActivity.OpenCollectionItem;
const collection = this.container.findCollection(
openCollectionitem.databaseId,
openCollectionitem.collectionId
);
if (collection) {
collection.openTab();
}
break;
}
case MostRecentActivity.Type.OpenNotebook: {
const openNotebookItem = item.data as MostRecentActivity.OpenNotebookItem;
const notebookItem = this.container.createNotebookContentItemFile(openNotebookItem.name, openNotebookItem.path);
notebookItem && this.container.openNotebook(notebookItem);
break;
}
default:
console.error("Unknown item type", item);
break;
}
}
private createCommonTaskItems(): SplashScreenItem[] { private createCommonTaskItems(): SplashScreenItem[] {
const items: SplashScreenItem[] = []; const items: SplashScreenItem[] = [];
@@ -333,23 +297,45 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return items; return items;
} }
private static getInfo(item: MostRecentActivity.Item): string { private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
if (item.type === MostRecentActivity.Type.OpenNotebook) { return {
const data = item.data as MostRecentActivity.OpenNotebookItem; iconSrc: NotebookIcon,
return data.path; title: collectionId,
} else { description: "Data",
return undefined; onClick: () => {
} const collection = this.container.findCollection(databaseId, collectionId);
collection && collection.openTab();
},
};
}
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
return {
info: path,
iconSrc: CollectionIcon,
title: name,
description: "Notebook",
onClick: () => {
const notebookItem = this.container.createNotebookContentItemFile(name, path);
notebookItem && this.container.openNotebook(notebookItem);
},
};
} }
private createRecentItems(): SplashScreenItem[] { private createRecentItems(): SplashScreenItem[] {
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((item) => ({ return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
iconSrc: this.getItemIcon(item), switch (activity.type) {
title: item.title, default: {
description: item.description, const unknownActivity: never = activity;
info: SplashScreen.getInfo(item), throw new Error(`Unknown activity: ${unknownActivity}`);
onClick: () => this.onItemClicked(item), }
})); case MostRecentActivity.Type.OpenNotebook:
return this.decorateOpenNotebookActivity(activity);
case MostRecentActivity.Type.OpenCollection:
return this.decorateOpenCollectionActivity(activity);
}
});
} }
private createTipsItems(): SplashScreenItem[] { private createTipsItems(): SplashScreenItem[] {

View File

@@ -53,7 +53,6 @@
throughputAutoPilotRadioId: throughputAutoPilotRadioId, throughputAutoPilotRadioId: throughputAutoPilotRadioId,
throughputProvisionedRadioId: throughputProvisionedRadioId, throughputProvisionedRadioId: throughputProvisionedRadioId,
throughputModeRadioName: throughputModeRadioName, throughputModeRadioName: throughputModeRadioName,
showAutoPilot: userCanChangeProvisioningTypes,
isAutoPilotSelected: isAutoPilotSelected, isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: autoPilotThroughput, maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,

View File

@@ -1,23 +1,23 @@
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import DiscardIcon from "../../../images/discard.svg";
import editable from "../../Common/EditableUtility";
import Q from "q"; import Q from "q";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import TabsBase from "./TabsBase"; import * as Constants from "../../Common/Constants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../Explorer";
import { updateOffer } from "../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../Common/dataAccess/updateOffer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import editable from "../../Common/EditableUtility";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as SharedConstants from "../../Shared/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import TabsBase from "./TabsBase";
const updateThroughputBeyondLimitWarningMessage: string = ` const updateThroughputBeyondLimitWarningMessage: string = `
You are about to request an increase in throughput beyond the pre-allocated capacity. You are about to request an increase in throughput beyond the pre-allocated capacity.
@@ -73,7 +73,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
public shouldShowStatusBar: ko.Computed<boolean>; public shouldShowStatusBar: ko.Computed<boolean>;
public throughputTitle: ko.PureComputed<string>; public throughputTitle: ko.PureComputed<string>;
public throughputAriaLabel: ko.PureComputed<string>; public throughputAriaLabel: ko.PureComputed<string>;
public userCanChangeProvisioningTypes: ko.Observable<boolean>;
public autoPilotUsageCost: ko.PureComputed<string>; public autoPilotUsageCost: ko.PureComputed<string>;
public warningMessage: ko.Computed<string>; public warningMessage: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
@@ -106,7 +105,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this._wasAutopilotOriginallySet = ko.observable(false); this._wasAutopilotOriginallySet = ko.observable(false);
this.isAutoPilotSelected = editable.observable(false); this.isAutoPilotSelected = editable.observable(false);
this.autoPilotThroughput = editable.observable<number>(); this.autoPilotThroughput = editable.observable<number>();
this.userCanChangeProvisioningTypes = ko.observable(true);
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput; const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput) { if (autoscaleMaxThroughput) {
@@ -118,9 +116,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
} }
this._hasProvisioningTypeChanged = ko.pureComputed<boolean>(() => { this._hasProvisioningTypeChanged = ko.pureComputed<boolean>(() => {
if (!this.userCanChangeProvisioningTypes()) {
return false;
}
if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) { if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) {
return true; return true;
} }
@@ -136,7 +131,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
}); });
this.requestUnitsUsageCost = ko.pureComputed(() => { this.requestUnitsUsageCost = ko.pureComputed(() => {
const account = this.container.databaseAccount(); const account = userContext.databaseAccount;
if (!account) { if (!account) {
return ""; return "";
} }
@@ -362,7 +357,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this.isTemplateReady = ko.observable<boolean>(false); this.isTemplateReady = ko.observable<boolean>(false);
this.isFreeTierAccount = ko.computed<boolean>(() => { this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container?.databaseAccount(); const databaseAccount = userContext.databaseAccount;
return databaseAccount?.properties?.enableFreeTier; return databaseAccount?.properties?.enableFreeTier;
}); });
@@ -448,7 +443,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput)); this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput); this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
this.throughput.setBaseline(offer.manualThroughput); this.throughput.setBaseline(offer.manualThroughput);
this.userCanChangeProvisioningTypes(true);
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@@ -24,7 +24,7 @@ import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBa
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";

View File

@@ -1,12 +1,11 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory"; import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
@@ -18,8 +17,6 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg";
import { ArrayHashMap } from "../../Common/ArrayHashMap"; import { ArrayHashMap } from "../../Common/ArrayHashMap";
import { NotebookUtil } from "../Notebook/NotebookUtil"; import { NotebookUtil } from "../Notebook/NotebookUtil";
import _ from "underscore";
import { IPinnedRepo } from "../../Juno/JunoClient";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
@@ -34,31 +31,39 @@ import Trigger from "./Trigger";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { DataTitle, NotebooksTitle, PseudoDirPath } from "../../hooks/useNotebooks";
export class ResourceTreeAdapter implements ReactAdapter { export interface ResourceTreeProps {
public static readonly MyNotebooksTitle = "My Notebooks"; // TODO remove eventually
public static readonly GitHubReposTitle = "GitHub repos"; explorer: Explorer;
private static readonly DataTitle = "DATA"; lastRefreshedTime: number;
private static readonly NotebooksTitle = "NOTEBOOKS";
private static readonly PseudoDirPath = "PsuedoDir";
public parameters: ko.Observable<number>; galleryContentRoot: NotebookContentItem;
myNotebooksContentRoot: NotebookContentItem;
public galleryContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem;
public myNotebooksContentRoot: NotebookContentItem; }
public gitHubNotebooksContentRoot: NotebookContentItem;
export class ResourceTree extends React.Component<ResourceTreeProps> {
private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs
private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs
private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids
public constructor(private container: Explorer) { private readonly container: Explorer;
this.parameters = ko.observable(Date.now());
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender()); constructor(props: ResourceTreeProps) {
this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender()); super(props);
this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender()); this.state = {
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
};
this.container = props.explorer;
this.container.selectedNode.subscribe(() => this.triggerRender());
this.container.tabsManager.activeTab.subscribe(() => this.triggerRender());
this.container.isNotebookEnabled.subscribe(() => this.triggerRender());
this.koSubsDatabaseIdMap = new ArrayHashMap(); this.koSubsDatabaseIdMap = new ArrayHashMap();
this.koSubsCollectionIdMap = new ArrayHashMap(); this.koSubsCollectionIdMap = new ArrayHashMap();
@@ -73,34 +78,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
}); });
this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database)); this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender();
} }
private traceMyNotebookTreeInfo() { render(): JSX.Element {
const myNotebooksTree = this.myNotebooksContentRoot;
if (myNotebooksTree.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
myNotebooksTree.children.forEach((treeNode) => {
switch ((treeNode as NotebookContentItem).type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
}
public renderComponent(): JSX.Element {
const dataRootNode = this.buildDataTree(); const dataRootNode = this.buildDataTree();
const notebooksRootNode = this.buildNotebooksTrees(); const notebooksRootNode = this.buildNotebooksTrees();
@@ -108,15 +88,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
return ( return (
<> <>
<AccordionComponent> <AccordionComponent>
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}> <AccordionItemComponent title={DataTitle} isExpanded={!this.props.gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} /> <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}> <AccordionItemComponent title={NotebooksTitle}>
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} /> <TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
{this.galleryContentRoot && this.buildGalleryCallout()} {this.props.galleryContentRoot && this.buildGalleryCallout()}
</> </>
); );
} else { } else {
@@ -124,71 +104,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
} }
public async initialize(): Promise<void[]> {
const refreshTasks: Promise<void>[] = [];
this.galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
this.myNotebooksContentRoot = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
// Only if notebook server is available we can refresh
if (this.container.notebookServerInfo().notebookServerEndpoint) {
refreshTasks.push(
this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => {
this.triggerRender();
this.traceMyNotebookTreeInfo();
})
);
}
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
this.gitHubNotebooksContentRoot = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory,
};
} else {
this.gitHubNotebooksContentRoot = undefined;
}
return Promise.all(refreshTasks);
}
public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void {
if (this.gitHubNotebooksContentRoot) {
this.gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory,
children: [],
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
});
});
this.gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
this.triggerRender();
}
}
private buildDataTree(): TreeNode { private buildDataTree(): TreeNode {
const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => { const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => {
const databaseNode: TreeNode = { const databaseNode: TreeNode = {
@@ -264,15 +179,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
onClick: () => { onClick: () => {
collection.openTab(); collection.openTab();
// push to most recent // push to most recent
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
type: MostRecentActivity.Type.OpenCollection,
title: collection.id(),
description: "Data",
data: {
databaseId: collection.databaseId,
collectionId: collection.id(),
},
});
}, },
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [ this.isDataNodeSelected(collection.databaseId, collection.id(), [
@@ -296,7 +203,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
children.push(schemaNode); children.push(schemaNode);
} }
if (ResourceTreeAdapter.showScriptNodes(this.container)) { if (ResourceTree.showScriptNodes(this.container)) {
children.push(this.buildStoredProcedureNode(collection)); children.push(this.buildStoredProcedureNode(collection));
children.push(this.buildUserDefinedFunctionsNode(collection)); children.push(this.buildUserDefinedFunctionsNode(collection));
children.push(this.buildTriggerNode(collection)); children.push(this.buildTriggerNode(collection));
@@ -337,7 +244,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
); );
}, },
onExpanded: () => { onExpanded: () => {
if (ResourceTreeAdapter.showScriptNodes(this.container)) { if (ResourceTree.showScriptNodes(this.container)) {
collection.loadStoredProcedures(); collection.loadStoredProcedures();
collection.loadUserDefinedFunctions(); collection.loadUserDefinedFunctions();
collection.loadTriggers(); collection.loadTriggers();
@@ -416,7 +323,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
public buildSchemaNode(collection: ViewModels.Collection): TreeNode { public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
if (collection.analyticalStorageTtl() == undefined) { if (collection.analyticalStorageTtl() === undefined) {
return undefined; return undefined;
} }
@@ -437,12 +344,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] { private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const schema: any = {}; const schema: any = {};
//unflatten //unflatten
fields.forEach((field: DataModels.IDataField, fieldIndex: number) => { fields.forEach((field: DataModels.IDataField) => {
const path: string[] = field.path.split("."); const path: string[] = field.path.split(".");
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = {}; let current: any = {};
path.forEach((name: string, pathIndex: number) => { path.forEach((name: string, pathIndex: number) => {
if (pathIndex === 0) { if (pathIndex === 0) {
@@ -467,9 +376,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
}); });
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const traverse = (obj: any): TreeNode[] => { const traverse = (obj: any): TreeNode[] => {
const children: TreeNode[] = []; const children: TreeNode[] = [];
// eslint-disable-next-line no-null/no-null
if (obj !== null && !Array.isArray(obj) && typeof obj === "object") { if (obj !== null && !Array.isArray(obj) && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => { Object.entries(obj).forEach(([key, value]) => {
children.push({ label: key, children: traverse(value) }); children.push({ label: key, children: traverse(value) });
@@ -485,21 +396,21 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
private buildNotebooksTrees(): TreeNode { private buildNotebooksTrees(): TreeNode {
let notebooksTree: TreeNode = { const notebooksTree: TreeNode = {
label: undefined, label: undefined,
isExpanded: true, isExpanded: true,
children: [], children: [],
}; };
if (this.galleryContentRoot) { if (this.props.galleryContentRoot) {
notebooksTree.children.push(this.buildGalleryNotebooksTree()); notebooksTree.children.push(this.buildGalleryNotebooksTree());
} }
if (this.myNotebooksContentRoot) { if (this.props.myNotebooksContentRoot) {
notebooksTree.children.push(this.buildMyNotebooksTree()); notebooksTree.children.push(this.buildMyNotebooksTree());
} }
if (this.gitHubNotebooksContentRoot) { if (this.props.gitHubNotebooksContentRoot) {
// collapse all other notebook nodes // collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(this.buildGitHubNotebooksTree()); notebooksTree.children.push(this.buildGitHubNotebooksTree());
@@ -569,11 +480,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildMyNotebooksTree(): TreeNode { private buildMyNotebooksTree(): TreeNode {
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.myNotebooksContentRoot, this.props.myNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
this.pushItemToMostRecent(item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
}, },
@@ -590,11 +501,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildGitHubNotebooksTree(): TreeNode { private buildGitHubNotebooksTree(): TreeNode {
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.gitHubNotebooksContentRoot, this.props.gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
this.pushItemToMostRecent(item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
}, },
@@ -624,18 +535,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
return gitHubNotebooksTree; return gitHubNotebooksTree;
} }
private pushItemToMostRecent(item: NotebookContentItem) {
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
type: MostRecentActivity.Type.OpenNotebook,
title: item.name,
description: "Notebook",
data: {
name: item.name,
path: item.path,
},
});
}
private buildChildNodes( private buildChildNodes(
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void,
@@ -674,6 +573,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
@@ -687,7 +587,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item), onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()),
}, },
{ {
label: "Delete", label: "Delete",
@@ -776,7 +676,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
{ {
label: "New Directory", label: "New Directory",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item), onClick: () => this.container.onCreateDirectory(item).then(() => this.triggerRender()),
}, },
{ {
label: "New Notebook", label: "New Notebook",
@@ -829,20 +729,19 @@ export class ResourceTreeAdapter implements ReactAdapter {
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: contextMenu:
createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath createDirectoryContextMenu && item.path !== PseudoDirPath ? this.createDirectoryContextMenu(item) : undefined,
? this.createDirectoryContextMenu(item)
: undefined,
data: item, data: item,
children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu),
}; };
} }
public triggerRender() { private triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now())); this.setState({});
} }
/** /**

View File

@@ -1,6 +1,6 @@
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import * as ko from "knockout"; import * as ko from "knockout";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; import { ResourceTree } from "./ResourceTree";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
@@ -26,22 +26,40 @@ describe("ResourceTreeAdapter", () => {
it("it should not select if no selected node", () => { it("it should not select if no selected node", () => {
const explorer = mockContainer(); const explorer = mockContainer();
explorer.selectedNode(undefined); explorer.selectedNode(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
it("it should not select incorrect subnodekinds", () => { it("it should not select incorrect subnodekinds", () => {
const resourceTreeAdapter = new ResourceTreeAdapter(mockContainer()); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); explorer: mockContainer(),
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
it("it should not select if no active tab", () => { it("it should not select if no active tab", () => {
const explorer = mockContainer(); const explorer = mockContainer();
explorer.tabsManager.activeTab(undefined); explorer.tabsManager.activeTab(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
@@ -54,8 +72,14 @@ describe("ResourceTreeAdapter", () => {
id: ko.observable<string>("dbid"), id: ko.observable<string>("dbid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode); } as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", undefined, [ explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", undefined, [
ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Documents,
]); ]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
@@ -74,8 +98,14 @@ describe("ResourceTreeAdapter", () => {
id: ko.observable<string>("collid"), id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode); } as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
let isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
let isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
subNodeKind = ViewModels.CollectionTabKind.Graph; subNodeKind = ViewModels.CollectionTabKind.Graph;
@@ -89,7 +119,7 @@ describe("ResourceTreeAdapter", () => {
id: ko.observable<string>("collid"), id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode); } as unknown) as ViewModels.TreeNode);
isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
}); });
@@ -105,8 +135,14 @@ describe("ResourceTreeAdapter", () => {
explorer.tabsManager.activeTab({ explorer.tabsManager.activeTab({
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
} as TabsBase); } as TabsBase);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [ explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", "collid", [
ViewModels.CollectionTabKind.Settings, ViewModels.CollectionTabKind.Settings,
]); ]);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();

View File

@@ -2,7 +2,7 @@ import * as ko from "knockout";
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 React from "react"; import React from "react";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; import { ResourceTree } from "./ResourceTree";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -237,7 +237,13 @@ const createMockCollection = (): ViewModels.Collection => {
describe("Resource tree for schema", () => { describe("Resource tree for schema", () => {
const mockContainer: Explorer = createMockContainer(); const mockContainer: Explorer = createMockContainer();
const resourceTree = new ResourceTreeAdapter(mockContainer); const resourceTree = new ResourceTree({
explorer: mockContainer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
it("should render", () => { it("should render", () => {
const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection()); const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection());

View File

@@ -1,5 +1,5 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
@@ -44,15 +44,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
onClick: () => { onClick: () => {
collection.onDocumentDBDocumentsClick(); collection.onDocumentDBDocumentsClick();
// push to most recent // push to most recent
MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, { mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
type: MostRecentActivity.Type.OpenCollection,
title: collection.id(),
description: "Data",
data: {
databaseId: collection.databaseId,
collectionId: collection.id(),
},
});
}, },
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents), this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents),

View File

@@ -36,7 +36,7 @@ const onInit = async () => {
<header> <header>
<GalleryHeaderComponent /> <GalleryHeaderComponent />
</header> </header>
<div style={{ marginLeft: 138, marginRight: 138 }}> <div style={{ margin: "auto", width: "85%" }}>
<div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}> <div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}>
<Text block> <Text block>
Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best

View File

@@ -9,9 +9,11 @@
"North Central US": "North Central US", "North Central US": "North Central US",
"West US": "West US", "West US": "West US",
"East US 2": "East US 2", "East US 2": "East US 2",
"ClassInfo": "This is a self serve class", "Current Region": "Current Region",
"RegionDropdownInfo": "More regions can be added in the future.", "RegionDropdownInfo": "More regions can be added in the future.",
"ValidationError": "Regions and AccountName should not be empty.", "RegionsAndAccountNameValidationError": "Regions and account name should not be empty.",
"DbThroughputValidationError": "Please update throughput for database.",
"DescriptionLabel": "Description",
"DescriptionText": "This class sets collection and database throughput.", "DescriptionText": "This class sets collection and database throughput.",
"DecriptionLinkText": "Click here for more information", "DecriptionLinkText": "Click here for more information",
"Regions": "Regions", "Regions": "Regions",
@@ -22,10 +24,17 @@
"Account Name": "Account Name", "Account Name": "Account Name",
"AccountNamePlaceHolder": "Enter the account name", "AccountNamePlaceHolder": "Enter the account name",
"Collection Throughput": "Collection Throughput", "Collection Throughput": "Collection Throughput",
"Enable DB level throughput": "Enable DB level throughput", "Enable DB level throughput": "Enable Database Level Throughput",
"Database Throughput": "Database Throughput", "Database Throughput": "Database Throughput",
"RefreshMessage": "Self Serve Example successfully refreshing", "UpdateInProgressMessage": "Data is being updated",
"SubmissionMessage": "Submitted successfully" "UpdateCompletedMessageTitle":"Update succeeded",
"UpdateCompletedMessageText": "Data updation completed.",
"SubmissionMessageSuccessTitle": "Update started",
"SubmissionMessageForNewRegionText": "Data update started. Region changed.",
"SubmissionMessageForSameRegionText": "Data update started. Region not changed.",
"SubmissionMessageErrorTitle": "Data update failed",
"SubmissionMessageErrorText": "Data update failed because of errors.",
"OnSaveFailureMessage": "Data save operation not currently permitted."
}, },
"SqlX": { "SqlX": {
} }

View File

@@ -1,74 +1,71 @@
// CSS Dependencies // CSS Dependencies
import "abort-controller/polyfill";
import "babel-polyfill";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import "../less/documentDB.less"; import "es6-object-assign/auto";
import "../less/tree.less"; import "es6-symbol/implement";
import "../less/forms.less"; import "object.entries/auto";
import "../less/menus.less"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import "../less/infobox.less"; import "promise-polyfill/src/polyfill";
import "../less/messagebox.less"; import "promise.prototype.finally/auto";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; import React, { useState } from "react";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import ReactDOM from "react-dom";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "url-polyfill/url-polyfill.min";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "whatwg-fetch";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Panes/PanelComponent.less";
import "../less/TableStyles/queryBuilder.less";
import "../externals/jquery.dataTables.min.css";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/resourceTree.less";
import "../externals/jquery.typeahead.min.css";
import "../externals/jquery-ui.min.css"; import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js";
import "../externals/jquery-ui.structure.min.css"; import "../externals/jquery-ui.structure.min.css";
import "../externals/jquery-ui.theme.min.css"; import "../externals/jquery-ui.theme.min.css";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less"; import "../externals/jquery.dataTables.min.css";
import "./Explorer/Panes/GraphNewVertexPane.less"; import "../externals/jquery.typeahead.min.css";
import "./Explorer/Tabs/QueryTab.less"; import "../externals/jquery.typeahead.min.js";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
// Image Dependencies // Image Dependencies
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico"; import "../images/favicon.ico";
import "./Shared/appInsights";
import "babel-polyfill";
import "es6-symbol/implement";
import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "./Libs/jquery";
import "bootstrap/dist/js/npm";
import "../externals/jquery.typeahead.min.js";
import "../externals/jquery-ui.min.js";
import "promise-polyfill/src/polyfill";
import "abort-controller/polyfill";
import "whatwg-fetch";
import "es6-object-assign/auto";
import "promise.prototype.finally/auto";
import "object.entries/auto";
import "./Libs/is-integer-polyfill";
import "url-polyfill/url-polyfill.min";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { ExplorerParams } from "./Explorer/Explorer";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import refreshImg from "../images/refresh-cosmos.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import refreshImg from "../images/refresh-cosmos.svg";
import { useConfig } from "./hooks/useConfig"; import "../less/documentDB.less";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import "../less/forms.less";
import { useSidePanel } from "./hooks/useSidePanel"; import "../less/infobox.less";
import "../less/menus.less";
import "../less/messagebox.less";
import "../less/resourceTree.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less";
import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import Explorer, { ExplorerParams } from "./Explorer/Explorer";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import "./Explorer/Panes/GraphNewVertexPane.less";
import "./Explorer/Panes/PanelComponent.less";
import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent"; import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent";
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog"; import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Tabs/QueryTab.less";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useNotebooks } from "./hooks/useNotebooks";
import { useSidePanel } from "./hooks/useSidePanel";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import "./Libs/is-integer-polyfill";
import "./Libs/jquery";
import "./Shared/appInsights";
initializeIcons(); initializeIcons();
@@ -91,6 +88,18 @@ const App: React.FunctionComponent = () => {
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel(); const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
// TODO Figure out a better pattern: this is because we don't have container, yet
const context: { container: Explorer } = { container: undefined };
const {
lastRefreshTime,
galleryContentRoot,
myNotebooksContentRoot,
gitHubNotebooksContentRoot,
refreshList,
initializeGitHubRepos,
getMyNotebooksContentRoot,
} = useNotebooks(context);
const explorerParams: ExplorerParams = { const explorerParams: ExplorerParams = {
setIsNotificationConsoleExpanded, setIsNotificationConsoleExpanded,
setNotificationConsoleData, setNotificationConsoleData,
@@ -99,23 +108,22 @@ const App: React.FunctionComponent = () => {
closeSidePanel, closeSidePanel,
openDialog, openDialog,
closeDialog, closeDialog,
onRefreshNotebookList: refreshList,
initializeGitHubRepos,
getMyNotebooksContentRoot,
}; };
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams); const explorer = useKnockoutExplorer(config?.platform, explorerParams);
context.container = explorer;
if (!explorer) {
return <LoadingExplorer />;
}
return ( return (
<div className="flexContainer"> <div className="flexContainer">
<div <div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
id="divSelfServe" {/* Main Command Bar - Start */}
className="flexContainer"
data-bind="visible: selfServeType() && selfServeType() !== 'none', react: selfServeComponentAdapter"
></div>
<div
id="divExplorer"
data-bind="if: selfServeType() === 'none'"
className="flexContainer hideOverflows"
style={{ display: "none" }}
>
<div data-bind="react: commandBarComponentAdapter" /> <div data-bind="react: commandBarComponentAdapter" />
{/* Collections Tree and Tabs - Begin */} {/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs"> <div className="resourceTreeAndTabs">
@@ -167,7 +175,15 @@ const App: React.FunctionComponent = () => {
style={{ overflowY: "auto" }} style={{ overflowY: "auto" }}
data-bind="if: isAuthWithResourceToken(), react:resourceTreeForResourceToken" data-bind="if: isAuthWithResourceToken(), react:resourceTreeForResourceToken"
/> />
<div style={{ overflowY: "auto" }} data-bind="if: !isAuthWithResourceToken(), react:resourceTree" /> <div style={{ overflowY: "auto" }} data-bind="if: !isAuthWithResourceToken()">
<ResourceTree
explorer={explorer}
lastRefreshedTime={lastRefreshTime}
galleryContentRoot={galleryContentRoot}
myNotebooksContentRoot={myNotebooksContentRoot}
gitHubNotebooksContentRoot={gitHubNotebooksContentRoot}
/>
</div>
</div> </div>
{/* Collections Window - End */} {/* Collections Window - End */}
</div> </div>
@@ -245,25 +261,6 @@ const App: React.FunctionComponent = () => {
/> />
</div> </div>
</div> </div>
{/* Global loader - Start */}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
<div className="splashLoaderContentContainer">
<div data-bind="visible: selfServeType() === undefined, react: selfServeLoadingComponentAdapter"></div>
<div data-bind="if: selfServeType() === 'none'" style={{ display: "none" }}>
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
</div>
{/* Global loader - End */}
<PanelContainerComponent <PanelContainerComponent
isOpen={isPanelOpen} isOpen={isPanelOpen}
panelContent={panelContent} panelContent={panelContent}
@@ -307,3 +304,21 @@ const App: React.FunctionComponent = () => {
}; };
ReactDOM.render(<App />, document.body); ReactDOM.render(<App />, document.body);
function LoadingExplorer(): JSX.Element {
return (
<div className="splashLoaderContainer">
<div className="splashLoaderContentContainer">
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
Connecting...
</p>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput } from "./SelfServeTypes"; import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput, RefreshParams } from "./SelfServeTypes";
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils"; import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
type ValueOf<T> = T[keyof T]; type ValueOf<T> = T[keyof T];
@@ -33,7 +33,9 @@ export interface ChoiceInputOptions extends InputOptionsBase {
} }
export interface DescriptionDisplayOptions { export interface DescriptionDisplayOptions {
labelTKey?: string;
description?: (() => Promise<Description>) | Description; description?: (() => Promise<Description>) | Description;
isDynamicDescription?: boolean;
} }
type InputOptions = type InputOptions =
@@ -56,7 +58,7 @@ const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is Choic
}; };
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => { const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
return "description" in inputOptions; return "description" in inputOptions || "isDynamicDescription" in inputOptions;
}; };
const addToMap = (...decorators: Decorator[]): PropertyDecorator => { const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
@@ -80,7 +82,11 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
}; };
export const OnChange = ( export const OnChange = (
onChange: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput> onChange: (
newValue: InputType,
currentState: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Map<string, SmartUiInput>
): PropertyDecorator => { ): PropertyDecorator => {
return addToMap({ name: "onChange", value: onChange }); return addToMap({ name: "onChange", value: onChange });
}; };
@@ -111,7 +117,11 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
{ name: "choices", value: inputOptions.choices } { name: "choices", value: inputOptions.choices }
); );
} else if (isDescriptionDisplayOptions(inputOptions)) { } else if (isDescriptionDisplayOptions(inputOptions)) {
return addToMap({ name: "description", value: inputOptions.description }); return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey },
{ name: "description", value: inputOptions.description },
{ name: "isDynamicDescription", value: inputOptions.isDynamicDescription }
);
} else { } else {
return addToMap( return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey }, { name: "labelTKey", value: inputOptions.labelTKey },
@@ -126,8 +136,8 @@ export const IsDisplayable = (): ClassDecorator => {
}; };
}; };
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => { export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => {
return (target) => { return (target) => {
addPropertyToMap(target.prototype, "root", target.name, "info", info); addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams);
}; };
}; };

View File

@@ -64,13 +64,20 @@ export const initialize = async (): Promise<InitializeResponse> => {
}; };
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => { export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
const refreshCountString = SessionStorageUtility.getEntry("refreshCount");
const refreshCount = refreshCountString ? parseInt(refreshCountString) : 0;
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
const databaseAccountName = userContext.databaseAccount.name; const databaseAccountName = userContext.databaseAccount.name;
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName); const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded"; const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
const progressToBeSent = refreshCount % 5 === 0 ? isUpdateInProgress : true;
SessionStorageUtility.setEntry("refreshCount", (refreshCount + 1).toString());
return { return {
isUpdateInProgress: isUpdateInProgress, isUpdateInProgress: progressToBeSent,
notificationMessage: "RefreshMessage", updateInProgressMessageTKey: "UpdateInProgressMessage",
}; };
}; };

View File

@@ -1,13 +1,14 @@
import { PropertyInfo, OnChange, Values, IsDisplayable, ClassInfo } from "../Decorators"; import { PropertyInfo, OnChange, Values, IsDisplayable, RefreshOptions } from "../Decorators";
import { import {
ChoiceItem, ChoiceItem,
Description,
DescriptionType,
Info, Info,
InputType, InputType,
NumberUiType, NumberUiType,
OnSaveResult,
RefreshResult, RefreshResult,
SelfServeBaseClass, SelfServeBaseClass,
SelfServeNotification,
SelfServeNotificationType,
SmartUiInput, SmartUiInput,
} from "../SelfServeTypes"; } from "../SelfServeTypes";
import { import {
@@ -27,16 +28,19 @@ const regionDropdownItems: ChoiceItem[] = [
{ label: "East US 2", key: Regions.EastUS2 }, { label: "East US 2", key: Regions.EastUS2 },
]; ];
const selfServeExampleInfo: Info = {
messageTKey: "ClassInfo",
};
const regionDropdownInfo: Info = { const regionDropdownInfo: Info = {
messageTKey: "RegionDropdownInfo", messageTKey: "RegionDropdownInfo",
}; };
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => { const onRegionsChange = (newValue: InputType, currentState: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentState.set("regions", { value: newValue }); currentState.set("regions", { value: newValue });
const currentRegionText = `current region selected is ${newValue}`;
currentState.set("currentRegionText", {
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
hidden: false,
});
const currentEnableLogging = currentState.get("enableLogging"); const currentEnableLogging = currentState.get("enableLogging");
if (newValue === Regions.NorthCentralUS) { if (newValue === Regions.NorthCentralUS) {
currentState.set("enableLogging", { value: false, disabled: true }); currentState.set("enableLogging", { value: false, disabled: true });
@@ -47,8 +51,8 @@ const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: Inpu
}; };
const onEnableDbLevelThroughputChange = ( const onEnableDbLevelThroughputChange = (
currentState: Map<string, SmartUiInput>, newValue: InputType,
newValue: InputType currentState: Map<string, SmartUiInput>
): Map<string, SmartUiInput> => { ): Map<string, SmartUiInput> => {
currentState.set("enableDbLevelThroughput", { value: newValue }); currentState.set("enableDbLevelThroughput", { value: newValue });
const currentDbThroughput = currentState.get("dbThroughput"); const currentDbThroughput = currentState.get("dbThroughput");
@@ -57,9 +61,15 @@ const onEnableDbLevelThroughputChange = (
return currentState; return currentState;
}; };
const validate = (currentvalues: Map<string, SmartUiInput>): void => { const validate = (
currentvalues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): void => {
if (currentvalues.get("dbThroughput") === baselineValues.get("dbThroughput")) {
throw new Error("DbThroughputValidationError");
}
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) { if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
throw new Error("ValidationError"); throw new Error("RegionsAndAccountNameValidationError");
} }
}; };
@@ -86,12 +96,12 @@ const validate = (currentvalues: Map<string, SmartUiInput>): void => {
*/ */
@IsDisplayable() @IsDisplayable()
/* /*
@ClassInfo() @RefreshOptions()
- optional - role: Passes the refresh options to be used by the self serve model.
- input: Info | () => Promise<Info> - inputs:
- role: Display an Info bar as the first element of the UI. retryIntervalInMs - The time interval between refresh attempts when an update in ongoing.
*/ */
@ClassInfo(selfServeExampleInfo) @RefreshOptions({ retryIntervalInMs: 2000 })
export default class SelfServeExample extends SelfServeBaseClass { export default class SelfServeExample extends SelfServeBaseClass {
/* /*
onRefresh() onRefresh()
@@ -109,18 +119,21 @@ export default class SelfServeExample extends SelfServeBaseClass {
/* /*
onSave() onSave()
- input: (currentValues: Map<string, InputType>) => Promise<void> - input: (currentValues: Map<string, InputType>, baselineValues: ReadonlyMap<string, SmartUiInput>) => Promise<string>
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API - role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
calls here using the data from the different inputs passed as a Map to this callback function. calls here using the data from the different inputs passed as a Map to this callback function.
In this example, the onSave callback simply sets the value for keys corresponding to the field name In this example, the onSave callback simply sets the value for keys corresponding to the field name
in the SessionStorage. in the SessionStorage. It uses the currentValues and baselineValues maps to perform custom validations
- returns: SelfServeNotification - as well.
message: The message to be displayed in the message bar after the onSave is completed
type: The type of message bar to be used (info, warning, error) - returns: The initialize, success and failure messages to be displayed in the Portal Notification blade after the operation is completed.
*/ */
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => { public onSave = async (
validate(currentValues); currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): Promise<OnSaveResult> => {
validate(currentValues, baselineValues);
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions]; const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
const enableLogging = currentValues.get("enableLogging")?.value as boolean; const enableLogging = currentValues.get("enableLogging")?.value as boolean;
const accountName = currentValues.get("accountName")?.value as string; const accountName = currentValues.get("accountName")?.value as string;
@@ -128,8 +141,48 @@ export default class SelfServeExample extends SelfServeBaseClass {
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean; const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
let dbThroughput = currentValues.get("dbThroughput")?.value as number; let dbThroughput = currentValues.get("dbThroughput")?.value as number;
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined; dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput); try {
return { message: "SubmissionMessage", type: SelfServeNotificationType.info }; await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
if (currentValues.get("regions") === baselineValues.get("regions")) {
return {
operationStatusUrl: undefined,
portalNotification: {
initialize: {
titleTKey: "SubmissionMessageSuccessTitle",
messageTKey: "SubmissionMessageForSameRegionText",
},
success: {
titleTKey: "UpdateCompletedMessageTitle",
messageTKey: "UpdateCompletedMessageText",
},
failure: {
titleTKey: "SubmissionMessageErrorTitle",
messageTKey: "SubmissionMessageErrorText",
},
},
};
} else {
return {
operationStatusUrl: undefined,
portalNotification: {
initialize: {
titleTKey: "SubmissionMessageSuccessTitle",
messageTKey: "SubmissionMessageForNewRegionText",
},
success: {
titleTKey: "UpdateCompletedMessageTitle",
messageTKey: "UpdateCompletedMessageText",
},
failure: {
titleTKey: "SubmissionMessageErrorTitle",
messageTKey: "SubmissionMessageErrorText",
},
},
};
}
} catch (error) {
throw new Error("OnSaveFailureMessage");
}
}; };
/* /*
@@ -150,6 +203,11 @@ export default class SelfServeExample extends SelfServeBaseClass {
public initialize = async (): Promise<Map<string, SmartUiInput>> => { public initialize = async (): Promise<Map<string, SmartUiInput>> => {
const initializeResponse = await initialize(); const initializeResponse = await initialize();
const defaults = new Map<string, SmartUiInput>(); const defaults = new Map<string, SmartUiInput>();
const currentRegionText = `current region selected is ${initializeResponse.regions}`;
defaults.set("currentRegionText", {
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
hidden: false,
});
defaults.set("regions", { value: initializeResponse.regions }); defaults.set("regions", { value: initializeResponse.regions });
defaults.set("enableLogging", { value: initializeResponse.enableLogging }); defaults.set("enableLogging", { value: initializeResponse.enableLogging });
const accountName = initializeResponse.accountName; const accountName = initializeResponse.accountName;
@@ -172,15 +230,24 @@ export default class SelfServeExample extends SelfServeBaseClass {
e) Text (with optional hyperlink) for descriptions e) Text (with optional hyperlink) for descriptions
*/ */
@Values({ @Values({
labelTKey: "DescriptionLabel",
description: { description: {
textTKey: "DescriptionText", textTKey: "DescriptionText",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://aka.ms/cosmos-create-account-portal",
textTKey: "DecriptionLinkText", textTKey: "DecriptionLinkText",
}, },
}, },
}) })
description: string; description: string;
@Values({
labelTKey: "Current Region",
isDynamicDescription: true,
})
currentRegionText: string;
/* /*
@PropertyInfo() @PropertyInfo()
- optional - optional
@@ -192,8 +259,8 @@ export default class SelfServeExample extends SelfServeBaseClass {
/* /*
@OnChange() @OnChange()
- optional - optional
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType> - input: (currentValues: Map<string, InputType>, newValue: InputType, baselineValues: ReadonlyMap<string, SmartUiInput>) => Map<string, InputType>
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property, - role: Takes a Map of current values, the newValue for this property and a ReadonlyMap of baselineValues as inputs. This is called when a property,
say prop1, changes its value in the UI. This can be used to say prop1, changes its value in the UI. This can be used to
a) Change the value (and reflect it in the UI) for prop2 based on prop1. a) Change the value (and reflect it in the UI) for prop2 based on prop1.
b) Change the visibility for prop2 in the UI, based on prop1 b) Change the visibility for prop2 in the UI, based on prop1

View File

@@ -0,0 +1,16 @@
.selfServeComponentContainer {
text-transform: none;
line-height: 1.28581;
letter-spacing: 0;
font-size: 14px;
font-weight: 400;
color: #182026;
height: 100%;
min-height: 100vh;
width: 100%;
background-color: #FFFFFF;
}
body {
margin: 0;
}

View File

@@ -0,0 +1,92 @@
import * as React from "react";
import ReactDOM from "react-dom";
import { sendMessage } from "../Common/MessageHandler";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
import { SelfServeFrameInputs } from "../Contracts/ViewModels";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { configContext, updateConfigContext } from "../ConfigContext";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { updateUserContext } from "../UserContext";
import "./SelfServe.less";
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
initializeIcons();
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
switch (selfServeType) {
case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
return new SelfServeExample.default().toSelfServeDescriptor();
}
case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
return new SqlX.default().toSelfServeDescriptor();
}
default:
return undefined;
}
};
const renderComponent = (selfServeDescriptor: SelfServeDescriptor): JSX.Element => {
if (!selfServeDescriptor) {
return <h1>Invalid self serve type!</h1>;
}
return <SelfServeComponent descriptor={selfServeDescriptor} />;
};
const renderSpinner = (): JSX.Element => {
return <Spinner size={SpinnerSize.large}></Spinner>;
};
const handleMessage = async (event: MessageEvent): Promise<void> => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (event.data["signature"] !== "pcIframe") {
return;
}
if (typeof event.data !== "object") {
return;
}
const inputs = event.data.data.inputs as SelfServeFrameInputs;
if (!inputs) {
return;
}
const urlSearchParams = new URLSearchParams(window.location.search);
const selfServeTypeText = inputs.selfServeType || urlSearchParams.get("selfServeType");
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType];
if (
!inputs.subscriptionId ||
!inputs.resourceGroup ||
!inputs.databaseAccount ||
!inputs.authorizationToken ||
!inputs.csmEndpoint ||
!selfServeType
) {
return;
}
updateConfigContext({
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken: inputs.authorizationToken,
databaseAccount: inputs.databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
});
const descriptor = await getDescriptor(selfServeType);
ReactDOM.render(renderComponent(descriptor), document.getElementById("selfServeContent"));
};
ReactDOM.render(renderSpinner(), document.getElementById("selfServeContent"));
window.addEventListener("message", handleMessage, false);
sendMessage("ready");

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent"; import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
import { NumberUiType, SelfServeDescriptor, SelfServeNotificationType, SmartUiInput } from "./SelfServeTypes"; import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes";
describe("SelfServeComponent", () => { describe("SelfServeComponent", () => {
const defaultValues = new Map<string, SmartUiInput>([ const defaultValues = new Map<string, SmartUiInput>([
@@ -17,13 +17,20 @@ describe("SelfServeComponent", () => {
const initializeMock = jest.fn(async () => new Map(defaultValues)); const initializeMock = jest.fn(async () => new Map(defaultValues));
const onSaveMock = jest.fn(async () => { const onSaveMock = jest.fn(async () => {
return { message: "submitted successfully", type: SelfServeNotificationType.info }; return {
operationStatusUrl: undefined,
} as OnSaveResult;
}); });
const refreshResult = {
isUpdateInProgress: false,
updateInProgressMessageTKey: "refresh performed successfully",
};
const onRefreshMock = jest.fn(async () => { const onRefreshMock = jest.fn(async () => {
return { isUpdateInProgress: false, notificationMessage: "refresh performed successfully" }; return { ...refreshResult };
}); });
const onRefreshIsUpdatingMock = jest.fn(async () => { const onRefreshIsUpdatingMock = jest.fn(async () => {
return { isUpdateInProgress: true, notificationMessage: "refresh performed successfully" }; return { ...refreshResult, isUpdateInProgress: true };
}); });
const exampleData: SelfServeDescriptor = { const exampleData: SelfServeDescriptor = {
@@ -136,16 +143,15 @@ describe("SelfServeComponent", () => {
wrapper.update(); wrapper.update();
state = wrapper.state() as SelfServeComponentState; state = wrapper.state() as SelfServeComponentState;
isEqual(state.baselineValues, updatedValues); isEqual(state.baselineValues, updatedValues);
selfServeComponent.resetBaselineValues(); selfServeComponent.updateBaselineValues();
state = wrapper.state() as SelfServeComponentState; state = wrapper.state() as SelfServeComponentState;
isEqual(state.baselineValues, defaultValues); isEqual(state.baselineValues, defaultValues);
isEqual(state.currentValues, state.baselineValues); isEqual(state.currentValues, state.baselineValues);
// clicking refresh calls onRefresh. If component is not updating, it calls initialize() as well // clicking refresh calls onRefresh.
selfServeComponent.onRefreshClicked(); selfServeComponent.onRefreshClicked();
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
expect(onRefreshMock).toHaveBeenCalledTimes(2); expect(onRefreshMock).toHaveBeenCalledTimes(2);
expect(initializeMock).toHaveBeenCalledTimes(2);
selfServeComponent.onSaveButtonClick(); selfServeComponent.onSaveButtonClick();
expect(onSaveMock).toHaveBeenCalledTimes(1); expect(onSaveMock).toHaveBeenCalledTimes(1);

View File

@@ -15,20 +15,45 @@ import {
InputType, InputType,
RefreshResult, RefreshResult,
SelfServeDescriptor, SelfServeDescriptor,
SelfServeNotification,
SmartUiInput, SmartUiInput,
DescriptionDisplay, DescriptionDisplay,
StringInput, StringInput,
NumberInput, NumberInput,
BooleanInput, BooleanInput,
ChoiceInput, ChoiceInput,
SelfServeNotificationType,
} from "./SelfServeTypes"; } from "./SelfServeTypes";
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { getMessageBarType } from "./SelfServeUtils";
import { Translation } from "react-i18next"; import { Translation } from "react-i18next";
import { TFunction } from "i18next"; import { TFunction } from "i18next";
import "../i18n"; import "../i18n";
import { sendMessage } from "../Common/MessageHandler";
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
import promiseRetry, { AbortError } from "p-retry";
interface SelfServeNotification {
message: string;
type: MessageBarType;
isCancellable: boolean;
}
interface PortalNotificationContent {
retryIntervalInMs: number;
operationStatusUrl: string;
portalNotification?: {
initialize: {
title: string;
message: string;
};
success: {
title: string;
message: string;
};
failure: {
title: string;
message: string;
};
};
}
export interface SelfServeComponentProps { export interface SelfServeComponentProps {
descriptor: SelfServeDescriptor; descriptor: SelfServeDescriptor;
@@ -39,17 +64,26 @@ export interface SelfServeComponentState {
currentValues: Map<string, SmartUiInput>; currentValues: Map<string, SmartUiInput>;
baselineValues: Map<string, SmartUiInput>; baselineValues: Map<string, SmartUiInput>;
isInitializing: boolean; isInitializing: boolean;
isSaving: boolean;
hasErrors: boolean; hasErrors: boolean;
compileErrorMessage: string; compileErrorMessage: string;
notification: SelfServeNotification;
refreshResult: RefreshResult; refreshResult: RefreshResult;
notification: SelfServeNotification;
} }
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> { export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
private static readonly defaultRetryIntervalInMs = 30000;
private smartUiGeneratorClassName: string; private smartUiGeneratorClassName: string;
private retryIntervalInMs: number;
private retryOptions: promiseRetry.Options;
private translationFunction: TFunction;
componentDidMount(): void { componentDidMount(): void {
this.performRefresh(); this.performRefresh().then(() => {
if (this.state.refreshResult?.isUpdateInProgress) {
promiseRetry(() => this.pollRefresh(), this.retryOptions);
}
});
this.initializeSmartUiComponent(); this.initializeSmartUiComponent();
} }
@@ -60,12 +94,18 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
currentValues: new Map(), currentValues: new Map(),
baselineValues: new Map(), baselineValues: new Map(),
isInitializing: true, isInitializing: true,
isSaving: false,
hasErrors: false, hasErrors: false,
compileErrorMessage: undefined, compileErrorMessage: undefined,
notification: undefined,
refreshResult: undefined, refreshResult: undefined,
notification: undefined,
}; };
this.smartUiGeneratorClassName = this.props.descriptor.root.id; this.smartUiGeneratorClassName = this.props.descriptor.root.id;
this.retryIntervalInMs = this.props.descriptor.refreshParams?.retryIntervalInMs;
if (!this.retryIntervalInMs) {
this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs;
}
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
} }
private onError = (hasErrors: boolean): void => { private onError = (hasErrors: boolean): void => {
@@ -109,7 +149,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
this.setState({ currentValues, baselineValues }); this.setState({ currentValues, baselineValues });
}; };
public resetBaselineValues = (): void => { public updateBaselineValues = (): void => {
const currentValues = this.state.currentValues; const currentValues = this.state.currentValues;
let baselineValues = this.state.baselineValues; let baselineValues = this.state.baselineValues;
for (const key of currentValues.keys()) { for (const key of currentValues.keys()) {
@@ -204,7 +244,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
private onInputChange = (input: AnyDisplay, newValue: InputType) => { private onInputChange = (input: AnyDisplay, newValue: InputType) => {
if (input.onChange) { if (input.onChange) {
const newValues = input.onChange(this.state.currentValues, newValue); const newValues = input.onChange(
newValue,
this.state.currentValues,
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
);
this.setState({ currentValues: newValues }); this.setState({ currentValues: newValues });
} else { } else {
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
@@ -215,42 +259,60 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
} }
}; };
public performSave = async (): Promise<void> => {
this.setState({ isSaving: true, notification: undefined });
try {
const onSaveResult = await this.props.descriptor.onSave(
this.state.currentValues,
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
);
if (onSaveResult.portalNotification) {
const requestInitializedPortalNotification = onSaveResult.portalNotification.initialize;
const requestSucceededPortalNotification = onSaveResult.portalNotification.success;
const requestFailedPortalNotification = onSaveResult.portalNotification.failure;
this.sendNotificationMessage({
retryIntervalInMs: this.retryIntervalInMs,
operationStatusUrl: onSaveResult.operationStatusUrl,
portalNotification: {
initialize: {
title: this.getTranslation(requestInitializedPortalNotification.titleTKey),
message: this.getTranslation(requestInitializedPortalNotification.messageTKey),
},
success: {
title: this.getTranslation(requestSucceededPortalNotification.titleTKey),
message: this.getTranslation(requestSucceededPortalNotification.messageTKey),
},
failure: {
title: this.getTranslation(requestFailedPortalNotification.titleTKey),
message: this.getTranslation(requestFailedPortalNotification.messageTKey),
},
},
});
}
promiseRetry(() => this.pollRefresh(), this.retryOptions);
} catch (error) {
this.setState({
notification: {
type: MessageBarType.error,
isCancellable: true,
message: this.getTranslation(error.message),
},
});
throw error;
} finally {
this.setState({ isSaving: false });
}
await this.onRefreshClicked();
this.updateBaselineValues();
};
public onSaveButtonClick = (): void => { public onSaveButtonClick = (): void => {
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues); this.performSave();
onSavePromise.catch((error) => {
this.setState({
notification: {
message: `${error.message}`,
type: SelfServeNotificationType.error,
},
});
});
onSavePromise.then((notification: SelfServeNotification) => {
this.setState({
notification: {
message: notification.message,
type: notification.type,
},
});
this.resetBaselineValues();
this.onRefreshClicked();
});
}; };
public isDiscardButtonDisabled = (): boolean => { public isDiscardButtonDisabled = (): boolean => {
for (const key of this.state.currentValues.keys()) { if (this.state.isSaving) {
const currentValue = JSON.stringify(this.state.currentValues.get(key));
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
if (currentValue !== baselineValue) {
return false;
}
}
return true;
};
public isSaveButtonDisabled = (): boolean => {
if (this.state.hasErrors) {
return true; return true;
} }
for (const key of this.state.currentValues.keys()) { for (const key of this.state.currentValues.keys()) {
@@ -264,38 +326,84 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
return true; return true;
}; };
private performRefresh = async (): Promise<RefreshResult> => { public isSaveButtonDisabled = (): boolean => {
if (this.state.hasErrors || this.state.isSaving) {
return true;
}
for (const key of this.state.currentValues.keys()) {
const currentValue = JSON.stringify(this.state.currentValues.get(key));
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
if (currentValue !== baselineValue) {
return false;
}
}
return true;
};
private performRefresh = async (): Promise<void> => {
const refreshResult = await this.props.descriptor.onRefresh(); const refreshResult = await this.props.descriptor.onRefresh();
this.setState({ refreshResult: { ...refreshResult } }); let updateInProgressNotification: SelfServeNotification;
return refreshResult; if (this.state.refreshResult?.isUpdateInProgress && !refreshResult.isUpdateInProgress) {
await this.initializeSmartUiComponent();
}
if (refreshResult.isUpdateInProgress) {
updateInProgressNotification = {
type: MessageBarType.info,
isCancellable: false,
message: this.getTranslation(refreshResult.updateInProgressMessageTKey),
};
}
this.setState({
refreshResult: { ...refreshResult },
notification: updateInProgressNotification,
});
}; };
public onRefreshClicked = async (): Promise<void> => { public onRefreshClicked = async (): Promise<void> => {
this.setState({ isInitializing: true }); this.setState({ isInitializing: true });
const refreshResult = await this.performRefresh(); await this.performRefresh();
if (!refreshResult.isUpdateInProgress) {
this.initializeSmartUiComponent();
}
this.setState({ isInitializing: false }); this.setState({ isInitializing: false });
}; };
public getCommonTranslation = (translationFunction: TFunction, key: string): string => { public pollRefresh = async (): Promise<void> => {
return translationFunction(`Common.${key}`); try {
await this.performRefresh();
} catch (error) {
throw new AbortError(error);
}
const refreshResult = this.state.refreshResult;
if (refreshResult.isUpdateInProgress) {
throw new Error("update in progress. retrying ...");
}
}; };
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => { public getCommonTranslation = (key: string): string => {
return this.getTranslation(key, "Common");
};
private getTranslation = (messageKey: string, prefix = `${this.smartUiGeneratorClassName}`): string => {
const translationKey = `${prefix}.${messageKey}`;
const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey;
if (translation === translationKey) {
return messageKey;
}
return translation;
};
private getCommandBarItems = (): ICommandBarItemProps[] => {
return [ return [
{ {
key: "save", key: "save",
text: this.getCommonTranslation(translate, "Save"), text: this.getCommonTranslation("Save"),
iconProps: { iconName: "Save" }, iconProps: { iconName: "Save" },
split: true, split: true,
disabled: this.isSaveButtonDisabled(), disabled: this.isSaveButtonDisabled(),
onClick: this.onSaveButtonClick, onClick: () => this.onSaveButtonClick(),
}, },
{ {
key: "discard", key: "discard",
text: this.getCommonTranslation(translate, "Discard"), text: this.getCommonTranslation("Discard"),
iconProps: { iconName: "Undo" }, iconProps: { iconName: "Undo" },
split: true, split: true,
disabled: this.isDiscardButtonDisabled(), disabled: this.isDiscardButtonDisabled(),
@@ -305,7 +413,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}, },
{ {
key: "refresh", key: "refresh",
text: this.getCommonTranslation(translate, "Refresh"), text: this.getCommonTranslation("Refresh"),
disabled: this.state.isInitializing, disabled: this.state.isInitializing,
iconProps: { iconName: "Refresh" }, iconProps: { iconName: "Refresh" },
split: true, split: true,
@@ -316,12 +424,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
]; ];
}; };
private getNotificationMessageTranslation = (translationFunction: TFunction, messageKey: string): string => { private sendNotificationMessage = (portalNotificationContent: PortalNotificationContent): void => {
const translation = translationFunction(messageKey); sendMessage({
if (translation === `${this.smartUiGeneratorClassName}.${messageKey}`) { type: SelfServeMessageTypes.Notification,
return messageKey; data: { portalNotificationContent },
} });
return translation;
}; };
public render(): JSX.Element { public render(): JSX.Element {
@@ -332,14 +439,14 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
return ( return (
<Translation> <Translation>
{(translate) => { {(translate) => {
const getTranslation = (key: string): string => { if (!this.translationFunction) {
return translate(`${this.smartUiGeneratorClassName}.${key}`); this.translationFunction = translate;
}; }
return ( return (
<div style={{ overflowX: "auto" }}> <div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}> <Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} /> <CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
{this.state.isInitializing ? ( {this.state.isInitializing ? (
<Spinner <Spinner
size={SpinnerSize.large} size={SpinnerSize.large}
@@ -347,27 +454,25 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
/> />
) : ( ) : (
<> <>
{this.state.refreshResult?.isUpdateInProgress && (
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
{getTranslation(this.state.refreshResult.notificationMessage)}
</MessageBar>
)}
{this.state.notification && ( {this.state.notification && (
<MessageBar <MessageBar
messageBarType={getMessageBarType(this.state.notification.type)} messageBarType={this.state.notification.type}
styles={{ root: { width: 400 } }} onDismiss={
onDismiss={() => this.setState({ notification: undefined })} this.state.notification.isCancellable
? () => this.setState({ notification: undefined })
: undefined
}
> >
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)} {this.state.notification.message}
</MessageBar> </MessageBar>
)} )}
<SmartUiComponent <SmartUiComponent
disabled={this.state.refreshResult?.isUpdateInProgress} disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
descriptor={this.state.root as SmartUiDescriptor} descriptor={this.state.root as SmartUiDescriptor}
currentValues={this.state.currentValues} currentValues={this.state.currentValues}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onError={this.onError} onError={this.onError}
getTranslation={getTranslation} getTranslation={this.getTranslation}
/> />
</> </>
)} )}

View File

@@ -1,56 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import Explorer from "../Explorer/Explorer";
import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
export class SelfServeComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<SelfServeDescriptor>;
public container: Explorer;
constructor(container: Explorer) {
this.container = container;
this.parameters = ko.observable(undefined);
this.container.selfServeType.subscribe(() => {
this.triggerRender();
});
}
public static getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
switch (selfServeType) {
case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
return new SelfServeExample.default().toSelfServeDescriptor();
}
case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
return new SqlX.default().toSelfServeDescriptor();
}
default:
return undefined;
}
};
public renderComponent(): JSX.Element {
if (this.container.selfServeType() === SelfServeType.invalid) {
return <h1>Invalid self serve type!</h1>;
}
const smartUiDescriptor = this.parameters();
return smartUiDescriptor ? <SelfServeComponent descriptor={smartUiDescriptor} /> : <></>;
}
private triggerRender() {
window.requestAnimationFrame(async () => {
const selfServeType = this.container.selfServeType();
const smartUiDescriptor = await SelfServeComponentAdapter.getDescriptor(selfServeType);
this.parameters(smartUiDescriptor);
});
}
}

View File

@@ -1,25 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
import * as React from "react";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
export class SelfServeLoadingComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor() {
this.parameters = ko.observable(Date.now());
}
public renderComponent(): JSX.Element {
return <Spinner size={SpinnerSize.large} />;
}
private triggerRender() {
window.requestAnimationFrame(() => this.renderComponent());
}
}

View File

@@ -3,7 +3,11 @@ interface BaseInput {
errorMessage?: string; errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
labelTKey?: (() => Promise<string>) | string; labelTKey?: (() => Promise<string>) | string;
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>; onChange?: (
newValue: InputType,
currentState: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Map<string, SmartUiInput>;
placeholderTKey?: (() => Promise<string>) | string; placeholderTKey?: (() => Promise<string>) | string;
} }
@@ -44,16 +48,23 @@ export interface Node {
export interface SelfServeDescriptor { export interface SelfServeDescriptor {
root: Node; root: Node;
initialize?: () => Promise<Map<string, SmartUiInput>>; initialize?: () => Promise<Map<string, SmartUiInput>>;
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>; onSave?: (
currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Promise<OnSaveResult>;
inputNames?: string[]; inputNames?: string[];
onRefresh?: () => Promise<RefreshResult>; onRefresh?: () => Promise<RefreshResult>;
refreshParams?: RefreshParams;
} }
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay; export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
export abstract class SelfServeBaseClass { export abstract class SelfServeBaseClass {
public abstract initialize: () => Promise<Map<string, SmartUiInput>>; public abstract initialize: () => Promise<Map<string, SmartUiInput>>;
public abstract onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>; public abstract onSave: (
currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Promise<OnSaveResult>;
public abstract onRefresh: () => Promise<RefreshResult>; public abstract onRefresh: () => Promise<RefreshResult>;
public toSelfServeDescriptor(): SelfServeDescriptor { public toSelfServeDescriptor(): SelfServeDescriptor {
@@ -70,7 +81,7 @@ export abstract class SelfServeBaseClass {
throw new Error(`onRefresh() was not declared for the class '${className}'`); throw new Error(`onRefresh() was not declared for the class '${className}'`);
} }
if (!selfServeDescriptor?.root) { if (!selfServeDescriptor?.root) {
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`); throw new Error(`@IsDisplayable decorator was not declared for the class '${className}'`);
} }
selfServeDescriptor.initialize = this.initialize; selfServeDescriptor.initialize = this.initialize;
@@ -89,7 +100,7 @@ export enum NumberUiType {
export type ChoiceItem = { label: string; key: string }; export type ChoiceItem = { label: string; key: string };
export type InputType = number | string | boolean | ChoiceItem; export type InputType = number | string | boolean | ChoiceItem | Description;
export interface Info { export interface Info {
messageTKey: string; messageTKey: string;
@@ -99,8 +110,15 @@ export interface Info {
}; };
} }
export enum DescriptionType {
Text,
InfoMessageBar,
WarningMessageBar,
}
export interface Description { export interface Description {
textTKey: string; textTKey: string;
type: DescriptionType;
link?: { link?: {
href: string; href: string;
textTKey: string; textTKey: string;
@@ -113,18 +131,29 @@ export interface SmartUiInput {
disabled?: boolean; disabled?: boolean;
} }
export enum SelfServeNotificationType { export interface OnSaveResult {
info = "info", operationStatusUrl: string;
warning = "warning", portalNotification?: {
error = "error", initialize: {
} titleTKey: string;
messageTKey: string;
export interface SelfServeNotification { };
message: string; success: {
type: SelfServeNotificationType; titleTKey: string;
messageTKey: string;
};
failure: {
titleTKey: string;
messageTKey: string;
};
};
} }
export interface RefreshResult { export interface RefreshResult {
isUpdateInProgress: boolean; isUpdateInProgress: boolean;
notificationMessage: string; updateInProgressMessageTKey: string;
}
export interface RefreshParams {
retryIntervalInMs: number;
} }

View File

@@ -1,11 +1,11 @@
import { NumberUiType, RefreshResult, SelfServeBaseClass, SelfServeNotification, SmartUiInput } from "./SelfServeTypes"; import { NumberUiType, OnSaveResult, RefreshResult, SelfServeBaseClass, SmartUiInput } from "./SelfServeTypes";
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils"; import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
describe("SelfServeUtils", () => { describe("SelfServeUtils", () => {
it("initialize should be declared for self serve classes", () => { it("initialize should be declared for self serve classes", () => {
class Test extends SelfServeBaseClass { class Test extends SelfServeBaseClass {
public initialize: () => Promise<Map<string, SmartUiInput>>; public initialize: () => Promise<Map<string, SmartUiInput>>;
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>; public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<OnSaveResult>;
public onRefresh: () => Promise<RefreshResult>; public onRefresh: () => Promise<RefreshResult>;
} }
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'"); expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
@@ -14,7 +14,7 @@ describe("SelfServeUtils", () => {
it("onSave should be declared for self serve classes", () => { it("onSave should be declared for self serve classes", () => {
class Test extends SelfServeBaseClass { class Test extends SelfServeBaseClass {
public initialize = jest.fn(); public initialize = jest.fn();
public onSave: () => Promise<SelfServeNotification>; public onSave: () => Promise<OnSaveResult>;
public onRefresh: () => Promise<RefreshResult>; public onRefresh: () => Promise<RefreshResult>;
} }
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'"); expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
@@ -29,14 +29,14 @@ describe("SelfServeUtils", () => {
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'"); expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'");
}); });
it("@SmartUi decorator must be present for self serve classes", () => { it("@IsDisplayable decorator must be present for self serve classes", () => {
class Test extends SelfServeBaseClass { class Test extends SelfServeBaseClass {
public initialize = jest.fn(); public initialize = jest.fn();
public onSave = jest.fn(); public onSave = jest.fn();
public onRefresh = jest.fn(); public onRefresh = jest.fn();
} }
expect(() => new Test().toSelfServeDescriptor()).toThrow( expect(() => new Test().toSelfServeDescriptor()).toThrow(
"@SmartUi decorator was not declared for the class 'Test'" "@IsDisplayable decorator was not declared for the class 'Test'"
); );
}); });

View File

@@ -1,4 +1,3 @@
import { MessageBarType } from "office-ui-fabric-react";
import "reflect-metadata"; import "reflect-metadata";
import { import {
Node, Node,
@@ -15,8 +14,9 @@ import {
SelfServeDescriptor, SelfServeDescriptor,
SmartUiInput, SmartUiInput,
StringInput, StringInput,
SelfServeNotificationType, RefreshParams,
} from "./SelfServeTypes"; } from "./SelfServeTypes";
import { userContext } from "../UserContext";
export enum SelfServeType { export enum SelfServeType {
// No self serve type passed, launch explorer // No self serve type passed, launch explorer
@@ -28,6 +28,14 @@ export enum SelfServeType {
sqlx = "sqlx", sqlx = "sqlx",
} }
export enum BladeType {
SqlKeys = "keys",
MongoKeys = "mongoDbKeys",
CassandraKeys = "cassandraDbKeys",
GremlinKeys = "keys",
TableKeys = "tableKeys",
}
export interface DecoratorProperties { export interface DecoratorProperties {
id: string; id: string;
info?: (() => Promise<Info>) | Info; info?: (() => Promise<Info>) | Info;
@@ -44,9 +52,13 @@ export interface DecoratorProperties {
uiType?: string; uiType?: string;
errorMessage?: string; errorMessage?: string;
description?: (() => Promise<Description>) | Description; description?: (() => Promise<Description>) | Description;
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>; isDynamicDescription?: boolean;
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<void>; refreshParams?: RefreshParams;
initialize?: () => Promise<Map<string, SmartUiInput>>; onChange?: (
newValue: InputType,
currentState: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Map<string, SmartUiInput>;
} }
const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>( const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
@@ -83,7 +95,7 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
descriptorValue: K descriptorValue: K
): void => { ): void => {
if (!(context instanceof Map)) { if (!(context instanceof Map)) {
throw new Error(`@SmartUi should be the first decorator for the class '${className}'.`); throw new Error(`@IsDisplayable should be the first decorator for the class '${className}'.`);
} }
const propertyObject = context.get(propertyName) ?? { id: propertyName }; const propertyObject = context.get(propertyName) ?? { id: propertyName };
@@ -108,16 +120,17 @@ export const mapToSmartUiDescriptor = (
className: string, className: string,
context: Map<string, DecoratorProperties> context: Map<string, DecoratorProperties>
): SelfServeDescriptor => { ): SelfServeDescriptor => {
const inputNames: string[] = [];
const root = context.get("root"); const root = context.get("root");
context.delete("root"); context.delete("root");
const inputNames: string[] = [];
const smartUiDescriptor: SelfServeDescriptor = { const smartUiDescriptor: SelfServeDescriptor = {
root: { root: {
id: className, id: className,
info: root?.info, info: undefined,
children: [], children: [],
}, },
refreshParams: root?.refreshParams,
}; };
while (context.size > 0) { while (context.size > 0) {
@@ -155,7 +168,10 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
} }
return value as NumberInput; return value as NumberInput;
case "string": case "string":
if (value.description) { if (value.description || value.isDynamicDescription) {
if (value.description && value.isDynamicDescription) {
value.errorMessage = `dynamic descriptions should not have defaults set here.`;
}
return value as DescriptionDisplay; return value as DescriptionDisplay;
} }
if (!value.labelTKey) { if (!value.labelTKey) {
@@ -175,13 +191,9 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
} }
}; };
export const getMessageBarType = (type: SelfServeNotificationType): MessageBarType => { export const generateBladeLink = (blade: BladeType): string => {
switch (type) { const subscriptionId = userContext.subscriptionId;
case SelfServeNotificationType.info: const resourceGroupName = userContext.resourceGroup;
return MessageBarType.info; const databaseAccountName = userContext.databaseAccount.name;
case SelfServeNotificationType.warning: return `www.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`;
return MessageBarType.warning;
case SelfServeNotificationType.error:
return MessageBarType.error;
}
}; };

View File

@@ -1,18 +1,19 @@
import { IsDisplayable, OnChange, Values } from "../Decorators"; import { IsDisplayable, OnChange, Values } from "../Decorators";
import { import {
ChoiceItem, ChoiceItem,
DescriptionType,
InputType, InputType,
NumberUiType, NumberUiType,
OnSaveResult,
RefreshResult, RefreshResult,
SelfServeBaseClass, SelfServeBaseClass,
SelfServeNotification,
SmartUiInput, SmartUiInput,
} from "../SelfServeTypes"; } from "../SelfServeTypes";
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp"; import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
const onEnableDedicatedGatewayChange = ( const onEnableDedicatedGatewayChange = (
currentState: Map<string, SmartUiInput>, newValue: InputType,
newValue: InputType currentState: Map<string, SmartUiInput>
): Map<string, SmartUiInput> => { ): Map<string, SmartUiInput> => {
const sku = currentState.get("sku"); const sku = currentState.get("sku");
const instances = currentState.get("instances"); const instances = currentState.get("instances");
@@ -49,7 +50,7 @@ export default class SqlX extends SelfServeBaseClass {
return refreshDedicatedGatewayProvisioning(); return refreshDedicatedGatewayProvisioning();
}; };
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => { public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<OnSaveResult> => {
validate(currentValues); validate(currentValues);
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call. // TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`); throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
@@ -63,6 +64,7 @@ export default class SqlX extends SelfServeBaseClass {
@Values({ @Values({
description: { description: {
textTKey: "Provisioning dedicated gateways for SqlX accounts.", textTKey: "Provisioning dedicated gateways for SqlX accounts.",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Learn more about dedicated gateway.", textTKey: "Learn more about dedicated gateway.",

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="selfServeViewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Self Serve</title>
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
</head>
<body>
<div class="selfServeComponentContainer" id="selfServeContent"></div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
import { StringUtility } from "./StringUtility"; import * as StringUtility from "./StringUtility";
export class LocalStorageUtility { export class LocalStorageUtility {
public static hasItem(key: StorageKey): boolean { public static hasItem(key: StorageKey): boolean {

View File

@@ -1,4 +1,4 @@
import { StringUtility } from "./StringUtility"; import * as StringUtility from "./StringUtility";
describe("String utility", () => { describe("String utility", () => {
it("Convert to integer from string", () => { it("Convert to integer from string", () => {

View File

@@ -1,9 +1,7 @@
export class StringUtility { export function toNumber(num: string | null): number {
public static toNumber(num: string | null): number { return Number(num);
return Number(num); }
}
export function toBoolean(valueStr: string | null): boolean {
public static toBoolean(valueStr: string | null): boolean { return valueStr === "true";
return valueStr === "true";
}
} }

View File

@@ -17,12 +17,43 @@ interface UserContext {
useSDKOperations?: boolean; useSDKOperations?: boolean;
subscriptionType?: SubscriptionType; subscriptionType?: SubscriptionType;
quotaId?: string; quotaId?: string;
// API Type is not yet provided by ARM. You need to manually inspect all the capabilities+kind so we abstract that logic in userContext
// This is coming in a future Cosmos ARM API version as a prperty on databaseAccount
apiType?: ApiType;
} }
const userContext: Readonly<UserContext> = {} as const; type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra";
const userContext: UserContext = {};
function updateUserContext(newContext: UserContext): void { function updateUserContext(newContext: UserContext): void {
Object.assign(userContext, newContext); Object.assign(userContext, newContext);
Object.assign(userContext, { apiType: apiType(userContext.databaseAccount) });
}
function apiType(account: DatabaseAccount | undefined): ApiType {
if (!account) {
return "SQL";
}
const capabilities = account.properties?.capabilities;
if (capabilities) {
if (capabilities.find((c) => c.name === "EnableCassandra")) {
return "Cassandra";
}
if (capabilities.find((c) => c.name === "EnableGremlin")) {
return "Gremlin";
}
if (capabilities.find((c) => c.name === "EnableMongo")) {
return "Mongo";
}
if (capabilities.find((c) => c.name === "EnableTable")) {
return "Tables";
}
}
if (account.kind === "MongoDB" || account.kind === "Parse") {
return "Mongo";
}
return "SQL";
} }
export { userContext, updateUserContext }; export { userContext, updateUserContext };

View File

@@ -8,85 +8,81 @@ interface KernelConnectionMetadata {
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
} }
export class NotebookConfigurationUtils { export const _configureServiceEndpoints = async (kernelMetadata: KernelConnectionMetadata): Promise<void> => {
private constructor() {} if (!kernelMetadata) {
// should never get into this state
public static async configureServiceEndpoints( Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints");
notebookPath: string, return;
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo,
kernelName: string,
clusterConnectionInfo: DataModels.SparkClusterConnectionInfo
): Promise<void> {
if (!notebookPath || !notebookConnectionInfo || !kernelName) {
Logger.logError(
"Invalid or missing notebook connection info/path",
"NotebookConfigurationUtils/configureServiceEndpoints"
);
return Promise.reject("Invalid or missing notebook connection info");
}
if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) {
Logger.logError(
"Invalid or missing cluster connection info/endpoints",
"NotebookConfigurationUtils/configureServiceEndpoints"
);
return Promise.reject("Invalid or missing cluster connection info");
}
const dataExplorer = window.dataExplorer;
const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map(
(clusterEndpoint) => ({
type: clusterEndpoint.kind.toLowerCase(),
endpoint: clusterEndpoint && clusterEndpoint.endpoint,
username: clusterConnectionInfo.userName,
password: clusterConnectionInfo.password,
token: dataExplorer && dataExplorer.arcadiaToken(),
})
);
const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = {
path: notebookPath,
endpoints: notebookEndpointInfo,
};
const kernelMetadata: KernelConnectionMetadata = {
configurationEndpoints,
notebookConnectionInfo,
name: kernelName,
};
return await NotebookConfigurationUtils._configureServiceEndpoints(kernelMetadata);
} }
private static async _configureServiceEndpoints(kernelMetadata: KernelConnectionMetadata): Promise<void> { const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo;
if (!kernelMetadata) { const configurationEndpoints = kernelMetadata.configurationEndpoints;
// should never get into this state if (notebookConnectionInfo && configurationEndpoints) {
Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints"); try {
return; const headers: HeadersInit = { "Content-Type": "application/json" };
} if (notebookConnectionInfo.authToken) {
headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`;
const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo;
const configurationEndpoints = kernelMetadata.configurationEndpoints;
if (notebookConnectionInfo && configurationEndpoints) {
try {
const headers: any = { "Content-Type": "application/json" };
if (notebookConnectionInfo.authToken) {
headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`;
}
const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, {
method: "POST",
headers,
body: JSON.stringify(configurationEndpoints),
});
if (!response.ok) {
const responseMessage = await response.json();
Logger.logError(
getErrorMessage(responseMessage),
"NotebookConfigurationUtils/configureServiceEndpoints",
response.status
);
}
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints");
} }
const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, {
method: "POST",
headers,
body: JSON.stringify(configurationEndpoints),
});
if (!response.ok) {
const responseMessage = await response.json();
Logger.logError(
getErrorMessage(responseMessage),
"NotebookConfigurationUtils/configureServiceEndpoints",
response.status
);
}
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints");
} }
} }
} };
export const configureServiceEndpoints = async (
notebookPath: string,
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo,
kernelName: string,
clusterConnectionInfo: DataModels.SparkClusterConnectionInfo
): Promise<void> => {
if (!notebookPath || !notebookConnectionInfo || !kernelName) {
Logger.logError(
"Invalid or missing notebook connection info/path",
"NotebookConfigurationUtils/configureServiceEndpoints"
);
return Promise.reject("Invalid or missing notebook connection info");
}
if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) {
Logger.logError(
"Invalid or missing cluster connection info/endpoints",
"NotebookConfigurationUtils/configureServiceEndpoints"
);
return Promise.reject("Invalid or missing cluster connection info");
}
const dataExplorer = window.dataExplorer;
const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map(
(clusterEndpoint) => ({
type: clusterEndpoint.kind.toLowerCase(),
endpoint: clusterEndpoint && clusterEndpoint.endpoint,
username: clusterConnectionInfo.userName,
password: clusterConnectionInfo.password,
token: dataExplorer && dataExplorer.arcadiaToken(),
})
);
const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = {
path: notebookPath,
endpoints: notebookEndpointInfo,
};
const kernelMetadata: KernelConnectionMetadata = {
configurationEndpoints,
notebookConnectionInfo,
name: kernelName,
};
return await _configureServiceEndpoints(kernelMetadata);
};

View File

@@ -47,15 +47,14 @@ interface Options {
queryParams?: ARMQueryParams; queryParams?: ARMQueryParams;
} }
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them. export async function armRequestWithoutPolling<T>({
export async function armRequest<T>({
host, host,
path, path,
apiVersion, apiVersion,
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
}: Options): Promise<T> { }: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host); const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
if (queryParams) { if (queryParams) {
@@ -92,13 +91,33 @@ export async function armRequest<T>({
throw error; throw error;
} }
const operationStatusUrl = response.headers && response.headers.get("location"); const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
const responseBody = (await response.json()) as T;
return { result: responseBody, operationStatusUrl: operationStatusUrl };
}
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
export async function armRequest<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
}: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
});
const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) { if (operationStatusUrl) {
return await promiseRetry(() => getOperationStatus(operationStatusUrl)); return await promiseRetry(() => getOperationStatus(operationStatusUrl));
} }
return armRequestResult.result;
const responseBody = (await response.json()) as T;
return responseBody;
} }
async function getOperationStatus(operationStatusUrl: string) { async function getOperationStatus(operationStatusUrl: string) {

13
src/global.d.ts vendored
View File

@@ -1,11 +1,22 @@
import { AuthType } from "./AuthType";
import Explorer from "./Explorer/Explorer"; import Explorer from "./Explorer/Explorer";
declare global { declare global {
interface Window { interface Window {
/**
* @deprecated
* DO NOT take new usage of window.dataExplorer. If you must use Explorer, find it directly.
* */
dataExplorer: Explorer; dataExplorer: Explorer;
__REACT_DEVTOOLS_GLOBAL_HOOK__: any; __REACT_DEVTOOLS_GLOBAL_HOOK__: any;
/**
* @deprecated
* No new usage of jQuery ($)
* */
$: any; $: any;
/**
* @deprecated
* No new usage of jQuery
* */
jQuery: any; jQuery: any;
gitSha: string; gitSha: string;
} }

View File

@@ -1,9 +1,10 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { AccountKind, DefaultAccountExperience } from "../Common/Constants"; import { AccountKind, DefaultAccountExperience } from "../Common/Constants";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { sendMessage } from "../Common/MessageHandler"; import { sendMessage } from "../Common/MessageHandler";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
@@ -23,7 +24,6 @@ import {
getDatabaseAccountKindFromExperience, getDatabaseAccountKindFromExperience,
getDatabaseAccountPropertiesFromMetadata, getDatabaseAccountPropertiesFromMetadata,
} from "../Platform/Hosted/HostedUtils"; } from "../Platform/Hosted/HostedUtils";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts"; import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts";
@@ -32,54 +32,65 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
// This hook will create a new instance of Explorer.ts and bind it to the DOM // This hook will create a new instance of Explorer.ts and bind it to the DOM
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React // This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
// Pleas tread carefully :) // Pleas tread carefully :)
let explorer: Explorer;
export function useKnockoutExplorer(platform: Platform, explorerParams: ExplorerParams): Explorer { export function useKnockoutExplorer(platform: Platform, explorerParams: ExplorerParams): Explorer {
explorer = explorer || new Explorer(explorerParams); const [explorer, setExplorer] = useState<Explorer>();
useEffect(() => { useEffect(() => {
const effect = async () => { const effect = async () => {
if (platform) { if (platform) {
if (platform === Platform.Hosted) { if (platform === Platform.Hosted) {
await configureHosted(); const explorer = await configureHosted(explorerParams);
applyExplorerBindings(explorer); setExplorer(explorer);
} else if (platform === Platform.Emulator) { } else if (platform === Platform.Emulator) {
configureEmulator(); const explorer = configureEmulator(explorerParams);
applyExplorerBindings(explorer); setExplorer(explorer);
} else if (platform === Platform.Portal) { } else if (platform === Platform.Portal) {
configurePortal(); const explorer = await configurePortal(explorerParams);
setExplorer(explorer);
} }
} }
}; };
effect(); effect();
}, [platform]); }, [platform]);
useEffect(() => {
if (explorer) {
applyExplorerBindings(explorer);
}
}, [explorer]);
return explorer; return explorer;
} }
async function configureHosted() { async function configureHosted(explorerParams: ExplorerParams): Promise<Explorer> {
const win = (window as unknown) as HostedExplorerChildFrame; const win = (window as unknown) as HostedExplorerChildFrame;
explorer.selfServeType(SelfServeType.none);
if (win.hostedConfig.authType === AuthType.EncryptedToken) { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
configureHostedWithEncryptedToken(win.hostedConfig); return configureHostedWithEncryptedToken(win.hostedConfig, explorerParams);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) { } else if (win.hostedConfig.authType === AuthType.ResourceToken) {
configureHostedWithResourceToken(win.hostedConfig); return configureHostedWithResourceToken(win.hostedConfig, explorerParams);
} else if (win.hostedConfig.authType === AuthType.ConnectionString) { } else if (win.hostedConfig.authType === AuthType.ConnectionString) {
configureHostedWithConnectionString(win.hostedConfig); return configureHostedWithConnectionString(win.hostedConfig, explorerParams);
} else if (win.hostedConfig.authType === AuthType.AAD) { } else if (win.hostedConfig.authType === AuthType.AAD) {
await configureHostedWithAAD(win.hostedConfig); return configureHostedWithAAD(win.hostedConfig, explorerParams);
} }
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
} }
async function configureHostedWithAAD(config: AAD) { async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParams): Promise<Explorer> {
const account = config.databaseAccount; const account = config.databaseAccount;
const accountResourceId = account.id; const accountResourceId = account.id;
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
updateUserContext({ updateUserContext({
subscriptionId,
resourceGroup,
authType: AuthType.AAD, authType: AuthType.AAD,
authorizationToken: `Bearer ${config.authorizationToken}`, authorizationToken: `Bearer ${config.authorizationToken}`,
databaseAccount: config.databaseAccount, databaseAccount: config.databaseAccount,
}); });
const keys = await listKeys(subscriptionId, resourceGroup, account.name); const keys = await listKeys(subscriptionId, resourceGroup, account.name);
const explorer = new Explorer(explorerParams);
explorer.configure({ explorer.configure({
databaseAccount: account, databaseAccount: account,
subscriptionId, subscriptionId,
@@ -88,56 +99,69 @@ async function configureHostedWithAAD(config: AAD) {
authorizationToken: `Bearer ${config.authorizationToken}`, authorizationToken: `Bearer ${config.authorizationToken}`,
features: extractFeatures(), features: extractFeatures(),
}); });
return explorer;
} }
function configureHostedWithConnectionString(config: ConnectionString) { function configureHostedWithConnectionString(config: ConnectionString, explorerParams: ExplorerParams): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
const databaseAccount = {
id: "",
location: "",
type: "",
name: config.encryptedTokenMetadata.accountName,
kind: getDatabaseAccountKindFromExperience(apiExperience),
properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata),
tags: {},
};
updateUserContext({ updateUserContext({
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
accessToken: encodeURIComponent(config.encryptedToken), accessToken: encodeURIComponent(config.encryptedToken),
databaseAccount,
}); });
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const explorer = new Explorer(explorerParams);
explorer.configure({ explorer.configure({
databaseAccount: { databaseAccount,
id: "",
name: config.encryptedTokenMetadata.accountName,
kind: getDatabaseAccountKindFromExperience(apiExperience),
properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata),
tags: {},
},
masterKey: config.masterKey, masterKey: config.masterKey,
features: extractFeatures(), features: extractFeatures(),
}); });
return explorer;
} }
function configureHostedWithResourceToken(config: ResourceToken) { function configureHostedWithResourceToken(config: ResourceToken, explorerParams: ExplorerParams): Explorer {
const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken); const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken);
const databaseAccount = {
id: "",
location: "",
type: "",
name: parsedResourceToken.accountEndpoint,
kind: AccountKind.GlobalDocumentDB,
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
// Resource tokens can only be used with SQL API
tags: { defaultExperience: DefaultAccountExperience.DocumentDB },
};
updateUserContext({ updateUserContext({
databaseAccount,
authType: AuthType.ResourceToken, authType: AuthType.ResourceToken,
resourceToken: parsedResourceToken.resourceToken, resourceToken: parsedResourceToken.resourceToken,
endpoint: parsedResourceToken.accountEndpoint, endpoint: parsedResourceToken.accountEndpoint,
}); });
const explorer = new Explorer(explorerParams);
explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId);
explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); explorer.resourceTokenCollectionId(parsedResourceToken.collectionId);
if (parsedResourceToken.partitionKey) { if (parsedResourceToken.partitionKey) {
explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey);
} }
explorer.configure({ explorer.configure({
databaseAccount: { databaseAccount,
id: "",
name: parsedResourceToken.accountEndpoint,
kind: AccountKind.GlobalDocumentDB,
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
// Resource tokens can only be used with SQL API
tags: { defaultExperience: DefaultAccountExperience.DocumentDB },
},
features: extractFeatures(), features: extractFeatures(),
isAuthWithresourceToken: true, isAuthWithresourceToken: true,
}); });
explorer.isRefreshingExplorer(false); explorer.isRefreshingExplorer(false);
return explorer;
} }
function configureHostedWithEncryptedToken(config: EncryptedToken) { function configureHostedWithEncryptedToken(config: EncryptedToken, explorerParams: ExplorerParams): Explorer {
updateUserContext({ updateUserContext({
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
accessToken: encodeURIComponent(config.encryptedToken), accessToken: encodeURIComponent(config.encryptedToken),
@@ -145,6 +169,7 @@ function configureHostedWithEncryptedToken(config: EncryptedToken) {
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
config.encryptedTokenMetadata.apiKind config.encryptedTokenMetadata.apiKind
); );
const explorer = new Explorer(explorerParams);
explorer.configure({ explorer.configure({
databaseAccount: { databaseAccount: {
id: "", id: "",
@@ -155,72 +180,98 @@ function configureHostedWithEncryptedToken(config: EncryptedToken) {
}, },
features: extractFeatures(), features: extractFeatures(),
}); });
return explorer;
} }
function configureEmulator() { function configureEmulator(explorerParams: ExplorerParams): Explorer {
updateUserContext({ updateUserContext({
databaseAccount: emulatorAccount,
authType: AuthType.MasterKey, authType: AuthType.MasterKey,
}); });
explorer.selfServeType(SelfServeType.none); const explorer = new Explorer(explorerParams);
explorer.databaseAccount(emulatorAccount); explorer.databaseAccount(emulatorAccount);
explorer.isAccountReady(true); explorer.isAccountReady(true);
return explorer;
} }
function configurePortal() { async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer> {
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
}); });
// In development mode, try to load the iframe message from session storage. return new Promise((resolve) => {
// This allows webpack hot reload to function properly in the portal // In development mode, try to load the iframe message from session storage.
if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) { // This allows webpack hot reload to function properly in the portal
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) {
if (initMessage) { const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
const message = JSON.parse(initMessage); if (initMessage) {
console.warn( const message = JSON.parse(initMessage);
"Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message" console.warn(
); "Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message"
console.dir(message); );
explorer.configure(message); console.dir(message);
applyExplorerBindings(explorer); const explorer = new Explorer(explorerParams);
explorer.configure(message);
resolve(explorer);
}
} }
}
// In the Portal, configuration of Explorer happens via iframe message // In the Portal, configuration of Explorer happens via iframe message
window.addEventListener( window.addEventListener(
"message", "message",
(event) => { (event) => {
if (isInvalidParentFrameOrigin(event)) { if (isInvalidParentFrameOrigin(event)) {
return; return;
}
if (!shouldProcessMessage(event)) {
return;
}
// Check for init message
const message: PortalMessage = event.data?.data;
const inputs = message?.inputs;
const openAction = message?.openAction;
if (inputs) {
if (
configContext.BACKEND_ENDPOINT &&
configContext.platform === Platform.Portal &&
process.env.NODE_ENV === "development"
) {
inputs.extensionEndpoint = configContext.PROXY_PATH;
} }
explorer.configure(inputs); if (!shouldProcessMessage(event)) {
applyExplorerBindings(explorer); return;
if (openAction) {
handleOpenAction(openAction, explorer.nonSystemDatabases(), explorer);
} }
}
},
false
);
sendMessage("ready"); // Check for init message
const message: PortalMessage = event.data?.data;
const inputs = message?.inputs;
const openAction = message?.openAction;
if (inputs) {
if (
configContext.BACKEND_ENDPOINT &&
configContext.platform === Platform.Portal &&
process.env.NODE_ENV === "development"
) {
inputs.extensionEndpoint = configContext.PROXY_PATH;
}
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount;
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId,
});
const explorer = new Explorer(explorerParams);
explorer.configure(inputs);
resolve(explorer);
if (openAction) {
handleOpenAction(openAction, explorer.nonSystemDatabases(), explorer);
}
}
},
false
);
sendMessage("ready");
});
} }
function shouldProcessMessage(event: MessageEvent): boolean { function shouldProcessMessage(event: MessageEvent): boolean {

149
src/hooks/useNotebooks.ts Normal file
View File

@@ -0,0 +1,149 @@
import { useState } from "react";
import { Notebook } from "../Common/Constants";
import Explorer from "../Explorer/Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../Explorer/Notebook/NotebookContentItem";
import { IPinnedRepo } from "../Juno/JunoClient";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../Utils/GitHubUtils";
export const DataTitle = "DATA";
export const NotebooksTitle = "NOTEBOOKS";
export const PseudoDirPath = "PseudoDir";
export interface NotebookHooks {
lastRefreshTime: number;
galleryContentRoot: NotebookContentItem;
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
refreshList: () => void;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
getMyNotebooksContentRoot: () => NotebookContentItem;
}
export const useNotebooks = (context: { container: Explorer }): NotebookHooks => {
const [lastRefreshTime, setLastRefreshTime] = useState<number>(undefined);
const [galleryContentRoot, setGalleryContentRoot] = useState<NotebookContentItem>(undefined);
const [myNotebooksContentRoot, setMyNotebooksContentRoot] = useState<NotebookContentItem>(undefined);
const [gitHubNotebooksContentRoot, setGitHubNotebooksContentRoot] = useState<NotebookContentItem>(undefined);
const refreshList = (): void => {
initialize();
setLastRefreshTime(new Date().getTime());
};
// TODO For now, we need to rely on this, as setMyNotebooksContentRoot() is not synchronous
let _myNotebooksContentRoot: NotebookContentItem;
const _setMyNotebooksContentRoot = (newValue: NotebookContentItem) => {
_myNotebooksContentRoot = newValue;
setMyNotebooksContentRoot(newValue);
};
const initialize = (): Promise<void[]> => {
const refreshTasks: Promise<void>[] = [];
setGalleryContentRoot({
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
});
const _myNotebooksContentRoot = {
name: Notebook.MyNotebooksTitle,
path: context.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
_setMyNotebooksContentRoot(_myNotebooksContentRoot);
// Only if notebook server is available we can refresh
if (context.container.notebookServerInfo().notebookServerEndpoint) {
refreshTasks.push(
context.container.refreshContentItem(_myNotebooksContentRoot).then((root) => {
_setMyNotebooksContentRoot({ ...root });
traceMyNotebookTreeInfo(root);
})
);
}
initializeGitHubNotebooksContentRoot();
return Promise.all(refreshTasks);
};
const traceMyNotebookTreeInfo = (myNotebooksTree: NotebookContentItem) => {
if (myNotebooksTree.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
myNotebooksTree.children.forEach((treeNode) => {
switch ((treeNode as NotebookContentItem).type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
};
const initializeGitHubNotebooksContentRoot = (): NotebookContentItem => {
let root: NotebookContentItem;
if (context.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
root = {
name: Notebook.GitHubReposTitle,
path: PseudoDirPath,
type: NotebookContentItemType.Directory,
};
}
setGitHubNotebooksContentRoot(root);
return root;
};
const initializeGitHubRepos = (pinnedRepos: IPinnedRepo[]): void => {
const _gitHubNotebooksContentRoot = initializeGitHubNotebooksContentRoot();
if (_gitHubNotebooksContentRoot) {
_gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: PseudoDirPath,
type: NotebookContentItemType.Directory,
children: [],
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
});
});
_gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
setGitHubNotebooksContentRoot({ ..._gitHubNotebooksContentRoot });
}
};
return {
lastRefreshTime,
galleryContentRoot,
myNotebooksContentRoot,
gitHubNotebooksContentRoot,
refreshList,
initializeGitHubRepos,
getMyNotebooksContentRoot: () => _myNotebooksContentRoot,
};
};

View File

@@ -1,8 +1,5 @@
import "expect-puppeteer"; import "expect-puppeteer";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils"; import { createDatabase, generateUniqueName, onClickSaveButton } from "../utils/shared";
import { createDatabase, onClickSaveButton } from "../utils/shared";
import { generateUniqueName } from "../utils/shared";
import { ApiKind } from "../../src/Contracts/DataModels";
const LOADING_STATE_DELAY = 5000; const LOADING_STATE_DELAY = 5000;
jest.setTimeout(300000); jest.setTimeout(300000);
@@ -12,7 +9,9 @@ describe("MongoDB Index policy tests", () => {
try { try {
const singleFieldId = generateUniqueName("key"); const singleFieldId = generateUniqueName("key");
const wildCardId = generateUniqueName("key") + "$**"; const wildCardId = generateUniqueName("key") + "$**";
const frame = await getTestExplorerFrame(ApiKind.MongoDB); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
const dropDown = "Index Type "; const dropDown = "Index Type ";
let index = 0; let index = 0;
@@ -20,24 +19,18 @@ describe("MongoDB Index policy tests", () => {
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
const dbId = await createDatabase(frame); const { databaseId, collectionId } = await createDatabase(frame);
await frame.waitFor(25000); await frame.waitFor(25000);
// click on database // click on database
await frame.waitForSelector(`div[data-test="${dbId}"]`); await frame.waitForSelector(`div[data-test="${databaseId}"]`);
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${dbId}"]`); await frame.click(`div[data-test="${databaseId}"]`);
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
// click on scale & setting // click on scale & setting
const containers = await frame.$$( await frame.waitFor(`div[data-test="${collectionId}"]`), { visible: true };
`div[class="nodeChildren"] > div[class="collectionHeader main2 nodeItem "]> div[class="treeNodeHeader "]`
);
const selectedContainer = (await frame.evaluate((element) => element.innerText, containers[0]))
.replace(/[\u{0080}-\u{FFFF}]/gu, "")
.trim();
await frame.waitFor(`div[data-test="${selectedContainer}"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${selectedContainer}"]`); await frame.click(`div[data-test="${collectionId}"]`);
await frame.waitFor(`div[data-test="Scale & Settings"]`), { visible: true }; await frame.waitFor(`div[data-test="Scale & Settings"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);

View File

@@ -1,19 +1,17 @@
import { uploadNotebookIfNotExist } from "./notebookTestUtils"; import { uploadNotebookIfNotExist } from "./notebookTestUtils";
import { ElementHandle, Frame } from "puppeteer";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
jest.setTimeout(300000); jest.setTimeout(300000);
const notebookName = "GettingStarted.ipynb"; const notebookName = "GettingStarted.ipynb";
let frame: Frame;
let uploadedNotebookNode: ElementHandle<Element>;
describe("Notebook UI tests", () => { describe("Notebook UI tests", () => {
it("Upload, Open and Delete Notebook", async () => { it("Upload, Open and Delete Notebook", async () => {
try { try {
frame = await getTestExplorerFrame(); await page.goto("https://localhost:1234/testExplorer.html");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
await frame.waitForSelector(".galleryHeader"); await frame.waitForSelector(".galleryHeader");
uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName); const uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName);
await uploadedNotebookNode.click(); await uploadedNotebookNode.click();
await frame.waitForSelector(".tabNavText"); await frame.waitForSelector(".tabNavText");
const tabTitle = await frame.$eval(".tabNavText", (element) => element.textContent); const tabTitle = await frame.$eval(".tabNavText", (element) => element.textContent);

View File

@@ -1,25 +1,18 @@
import { Frame } from "puppeteer";
import { TestExplorerParams } from "../testExplorer/TestExplorerParams";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
import { SelfServeType } from "../../src/SelfServe/SelfServeUtils";
import { ApiKind } from "../../src/Contracts/DataModels";
jest.setTimeout(300000); jest.setTimeout(300000);
let frame: Frame;
describe("Self Serve", () => { describe("Self Serve", () => {
it("Launch Self Serve Example", async () => { it("Launch Self Serve Example", async () => {
try { try {
frame = await getTestExplorerFrame( await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
ApiKind.SQL, const handle = await page.waitForSelector("iframe");
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeType.example]]) const frame = await handle.contentFrame();
);
// wait for refresh RP call to end // wait for refresh RP call to end
await frame.waitFor(10000); await frame.waitFor(10000);
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE} // id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
await frame.waitForSelector("#description-text-display"); await frame.waitForSelector("#description-text-display");
await frame.waitForSelector("#currentRegionText-text-display");
const regions = await frame.waitForSelector("#regions-dropdown-input"); const regions = await frame.waitForSelector("#regions-dropdown-input");
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]"); let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");

View File

@@ -1,82 +1,50 @@
/* eslint-disable no-console */
import { ClientSecretCredential } from "@azure/identity";
import "../../less/hostedexplorer.less"; import "../../less/hostedexplorer.less";
import { TestExplorerParams } from "./TestExplorerParams"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { updateUserContext } from "../../src/UserContext";
import * as msRest from "@azure/ms-rest-js"; import { get, listKeys } from "../../src/Utils/arm/generatedClients/2020-04-01/databaseAccounts";
import * as ViewModels from "../../src/Contracts/ViewModels";
import { Capability, DatabaseAccount } from "../../src/Contracts/DataModels";
class CustomSigner implements msRest.ServiceClientCredentials { const resourceGroup = process.env.RESOURCE_GROUP || "";
private token: string; const subscriptionId = process.env.SUBSCRIPTION_ID || "";
constructor(token: string) { const urlSearchParams = new URLSearchParams(window.location.search);
this.token = token; const accountName = urlSearchParams.get("accountName") || "portal-sql-runner";
} const selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
async signRequest(webResource: msRest.WebResourceLike): Promise<msRest.WebResourceLike> { if (!process.env.AZURE_CLIENT_SECRET) {
webResource.headers.set("authorization", `bearer ${this.token}`); throw new Error(
return webResource; "process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server"
} );
} }
const getDatabaseAccount = async ( // Azure SDK clients accept the credential as a parameter
token: string, const credentials = new ClientSecretCredential(
notebooksAccountSubscriptonId: string, process.env.AZURE_TENANT_ID,
notebooksAccountResourceGroup: string, process.env.AZURE_CLIENT_ID,
notebooksAccountName: string process.env.AZURE_CLIENT_SECRET,
): Promise<DatabaseAccount> => { {
const client = new CosmosDBManagementClient(new CustomSigner(token), notebooksAccountSubscriptonId); authorityHost: "https://localhost:1234",
const databaseAccountGetResponse = await client.databaseAccounts.get( }
notebooksAccountResourceGroup, );
notebooksAccountName
);
const databaseAccount: DatabaseAccount = { console.log("Resource Group:", resourceGroup);
id: databaseAccountGetResponse.id, console.log("Subcription: ", subscriptionId);
name: databaseAccountGetResponse.name, console.log("Account Name: ", accountName);
location: databaseAccountGetResponse.location,
type: databaseAccountGetResponse.type,
kind: databaseAccountGetResponse.kind,
tags: databaseAccountGetResponse.tags,
properties: {
documentEndpoint: databaseAccountGetResponse.documentEndpoint,
tableEndpoint: undefined,
gremlinEndpoint: undefined,
cassandraEndpoint: undefined,
capabilities: databaseAccountGetResponse.capabilities.map((capability) => {
return { name: capability.name } as Capability;
}),
},
};
return databaseAccount;
};
const initTestExplorer = async (): Promise<void> => { const initTestExplorer = async (): Promise<void> => {
const urlSearchParams = new URLSearchParams(window.location.search); const { token } = await credentials.getToken("https://management.core.windows.net/.default");
const portalRunnerDatabaseAccount = decodeURIComponent( updateUserContext({
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount) authorizationToken: `bearer ${token}`,
); });
const portalRunnerDatabaseAccountKey = decodeURIComponent( const databaseAccount = await get(subscriptionId, resourceGroup, accountName);
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccountKey) const keys = await listKeys(subscriptionId, resourceGroup, accountName);
);
const portalRunnerSubscripton = decodeURIComponent(urlSearchParams.get(TestExplorerParams.portalRunnerSubscripton));
const portalRunnerResourceGroup = decodeURIComponent(
urlSearchParams.get(TestExplorerParams.portalRunnerResourceGroup)
);
const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType);
const token = decodeURIComponent(urlSearchParams.get(TestExplorerParams.token));
const databaseAccount = await getDatabaseAccount(
token,
portalRunnerSubscripton,
portalRunnerResourceGroup,
portalRunnerDatabaseAccount
);
const initTestExplorerContent = { const initTestExplorerContent = {
inputs: { inputs: {
databaseAccount: databaseAccount, databaseAccount: databaseAccount,
subscriptionId: portalRunnerSubscripton, subscriptionId,
resourceGroup: portalRunnerResourceGroup, resourceGroup,
authorizationToken: `Bearer ${token}`, authorizationToken: `Bearer ${token}`,
features: {}, features: {},
hasWriteAccess: true, hasWriteAccess: true,
@@ -88,7 +56,7 @@ const initTestExplorer = async (): Promise<void> => {
quotaId: "Internal_2014-09-01", quotaId: "Internal_2014-09-01",
addCollectionDefaultFlight: "2", addCollectionDefaultFlight: "2",
isTryCosmosDBSubscription: false, isTryCosmosDBSubscription: false,
masterKey: portalRunnerDatabaseAccountKey, masterKey: keys.primaryMasterKey,
loadDatabaseAccountTimestamp: 1604663109836, loadDatabaseAccountTimestamp: 1604663109836,
dataExplorerVersion: "1.0.1", dataExplorerVersion: "1.0.1",
sharedThroughputMinimum: 400, sharedThroughputMinimum: 400,
@@ -101,7 +69,7 @@ const initTestExplorer = async (): Promise<void> => {
// add UI test only when feature is not dependent on flights anymore // add UI test only when feature is not dependent on flights anymore
flights: [], flights: [],
selfServeType, selfServeType,
} as ViewModels.DataExplorerInputsFrame, } as DataExplorerInputsFrame,
}; };
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
@@ -127,7 +95,7 @@ const initTestExplorer = async (): Promise<void> => {
iframe.name = "explorer"; iframe.name = "explorer";
iframe.classList.add("iframe"); iframe.classList.add("iframe");
iframe.title = "explorer"; iframe.title = "explorer";
iframe.src = "explorer.html?platform=Portal&disablePortalInitCache"; iframe.src = iframeSrc;
document.body.appendChild(iframe); document.body.appendChild(iframe);
}; };

View File

@@ -1,8 +0,0 @@
export enum TestExplorerParams {
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
portalRunnerSubscripton = "portalRunnerSubscripton",
portalRunnerResourceGroup = "portalRunnerResourceGroup",
selfServeType = "selfServeType",
token = "token",
}

View File

@@ -1,64 +0,0 @@
import { Frame } from "puppeteer";
import { TestExplorerParams } from "./TestExplorerParams";
import { ClientSecretCredential } from "@azure/identity";
import { ApiKind } from "../../src/Contracts/DataModels";
let testExplorerFrame: Frame;
export const getTestExplorerFrame = async (apiKind?: ApiKind, params?: Map<string, string>): Promise<Frame> => {
if (testExplorerFrame) {
return testExplorerFrame;
}
let portalRunnerDatabaseAccount: string;
let portalRunnerDatabaseAccountKey: string;
switch (apiKind) {
case ApiKind.MongoDB:
portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT;
portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY;
break;
default:
portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
}
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
const credentials = new ClientSecretCredential(
notebooksTestRunnerTenantId,
notebooksTestRunnerClientId,
notebooksTestRunnerClientSecret
);
const { token } = await credentials.getToken("https://management.core.windows.net/.default");
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccount,
encodeURI(portalRunnerDatabaseAccount)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccountKey,
encodeURI(portalRunnerDatabaseAccountKey)
);
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerResourceGroup,
encodeURI(portalRunnerResourceGroup)
);
testExplorerUrl.searchParams.append(TestExplorerParams.token, encodeURI(token));
if (params) {
for (const key of params.keys()) {
testExplorerUrl.searchParams.append(key, encodeURI(params.get(key)));
}
}
await page.goto(testExplorerUrl.toString());
const handle = await page.waitForSelector("iframe");
return await handle.contentFrame();
};

View File

@@ -30,8 +30,8 @@ export function generateDatabaseName(baseName = "db", length = 1): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
} }
export async function createDatabase(frame: Frame): Promise<string> { export async function createDatabase(frame: Frame): Promise<{ databaseId: string; collectionId: string }> {
const dbId = generateDatabaseName(); const databaseId = generateDatabaseName();
const collectionId = generateUniqueName("col"); const collectionId = generateUniqueName("col");
const shardKey = "partitionKey"; const shardKey = "partitionKey";
// create new collection // create new collection
@@ -50,7 +50,7 @@ export async function createDatabase(frame: Frame): Promise<string> {
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]'); await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]'); const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]');
await dbInput.press("Backspace"); await dbInput.press("Backspace");
await dbInput.type(dbId); await dbInput.type(databaseId);
// type collection id // type collection id
await frame.waitFor('input[data-test="addCollection-collectionId"]'); await frame.waitFor('input[data-test="addCollection-collectionId"]');
@@ -67,7 +67,7 @@ export async function createDatabase(frame: Frame): Promise<string> {
// click submit // click submit
await frame.waitFor("#submitBtnAddCollection"); await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection"); await frame.click("#submitBtnAddCollection");
return dbId; return { databaseId, collectionId };
} }
export async function onClickSaveButton(frame: Frame): Promise<void> { export async function onClickSaveButton(frame: Frame): Promise<void> {

View File

@@ -10,6 +10,7 @@
"./src/Contracts/ActionContracts.ts", "./src/Contracts/ActionContracts.ts",
"./src/Contracts/Diagnostics.ts", "./src/Contracts/Diagnostics.ts",
"./src/Contracts/ExplorerContracts.ts", "./src/Contracts/ExplorerContracts.ts",
"./src/Contracts/SelfServeContracts.ts",
"./src/Contracts/Versions.ts" "./src/Contracts/Versions.ts"
], ],
} }

View File

@@ -1,7 +1,6 @@
const msRestNodeAuth = require("@azure/ms-rest-nodeauth"); const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
const ms = require("ms"); const ms = require("ms");
const { time } = require("console");
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
@@ -9,7 +8,15 @@ const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners"; const resourceGroupName = "runners";
const sixtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 60).getTime(); const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
function friendlyTime(date) {
try {
return ms(date);
} catch (error) {
return "Unknown";
}
}
// Deletes all SQL and Mongo databases created more than 20 minutes ago in the test runner accounts // Deletes all SQL and Mongo databases created more than 20 minutes ago in the test runner accounts
async function main() { async function main() {
@@ -21,22 +28,22 @@ async function main() {
const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name); const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name);
for (const database of mongoDatabases) { for (const database of mongoDatabases) {
const timestamp = Number(database.name.split("-")[1]); const timestamp = Number(database.name.split("-")[1]);
if (timestamp && timestamp < sixtyMinutesAgo) { if (timestamp && timestamp < thirtyMinutesAgo) {
await client.mongoDBResources.deleteMongoDBDatabase(resourceGroupName, account.name, database.name); await client.mongoDBResources.deleteMongoDBDatabase(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${ms(Date.now() - timestamp)}`); console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else { } else {
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${ms(Date.now() - timestamp)}`); console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} }
} }
} else if (account.kind === "GlobalDocumentDB") { } else if (account.kind === "GlobalDocumentDB") {
const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name); const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name);
for (const database of sqlDatabases) { for (const database of sqlDatabases) {
const timestamp = Number(database.name.split("-")[1]); const timestamp = Number(database.name.split("-")[1]);
if (timestamp && timestamp < sixtyMinutesAgo) { if (timestamp && timestamp < thirtyMinutesAgo) {
await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name); await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${ms(Date.now() - timestamp)}`); console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else { } else {
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${ms(Date.now() - timestamp)}`); console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} }
} }
} }

View File

@@ -1,3 +1,4 @@
require("dotenv/config");
const path = require("path"); const path = require("path");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
@@ -14,6 +15,16 @@ const isCI = require("is-ci");
const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8"); const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
const AZURE_CLIENT_ID = "fd8753b0-0707-4e32-84e9-2532af865fb4";
const AZURE_TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const SUBSCRIPTION_ID = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const RESOURCE_GROUP = "runners";
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET || process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET; // TODO Remove. Exists for backwards compat with old .env files. Prefer AZURE_CLIENT_SECRET
if (!AZURE_CLIENT_SECRET) {
console.warn("AZURE_CLIENT_SECRET is not set. testExplorer.html will not work.");
}
const cssRule = { const cssRule = {
test: /\.css$/, test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"], use: [MiniCssExtractPlugin.loader, "css-loader"],
@@ -102,6 +113,11 @@ module.exports = function (env = {}, argv = {}) {
if (mode === "development") { if (mode === "development") {
envVars.NODE_ENV = "development"; envVars.NODE_ENV = "development";
envVars.AZURE_CLIENT_ID = AZURE_CLIENT_ID;
envVars.AZURE_TENANT_ID = AZURE_TENANT_ID;
envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET;
envVars.SUBSCRIPTION_ID = SUBSCRIPTION_ID;
envVars.RESOURCE_GROUP = RESOURCE_GROUP;
typescriptRule.use[0].options.compilerOptions = { target: "ES2018" }; typescriptRule.use[0].options.compilerOptions = { target: "ES2018" };
} }
@@ -166,6 +182,11 @@ module.exports = function (env = {}, argv = {}) {
template: "src/connectToGitHub.html", template: "src/connectToGitHub.html",
chunks: ["connectToGitHub"], chunks: ["connectToGitHub"],
}), }),
new HtmlWebpackPlugin({
filename: "selfServe.html",
template: "src/SelfServe/selfServe.html",
chunks: ["selfServe"],
}),
new MonacoWebpackPlugin(), new MonacoWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }], patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }],
@@ -189,6 +210,7 @@ module.exports = function (env = {}, argv = {}) {
terminal: "./src/Terminal/index.ts", terminal: "./src/Terminal/index.ts",
notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx", notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",
galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx", galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx",
selfServe: "./src/SelfServe/SelfServe.tsx",
connectToGitHub: "./src/GitHub/GitHubConnector.ts", connectToGitHub: "./src/GitHub/GitHubConnector.ts",
}, },
node: { node: {
@@ -276,6 +298,12 @@ module.exports = function (env = {}, argv = {}) {
secure: false, secure: false,
logLevel: "debug", logLevel: "debug",
}, },
[`/${AZURE_TENANT_ID}`]: {
target: "https://login.microsoftonline.com/",
changeOrigin: true,
secure: false,
logLevel: "debug",
},
}, },
}, },
stats: "minimal", stats: "minimal",