Compare commits

...

34 Commits

Author SHA1 Message Date
Steve Faulkner
f738723f5a Remove ExplorerOptions 2020-08-12 21:43:58 -05:00
Steve Faulkner
c0ce637eec Turn off HMR (#147) 2020-08-12 20:12:31 -05:00
victor-meng
b61a235bf6 Move readCollection, readCollections, and readDatabases to ARM (#134) 2020-08-12 17:13:43 -07:00
Steve Faulkner
0fa97c2ce9 Increase Cypress Timeout and Disable LiveReload in test (#145)
Co-authored-by: Tanuj Mittal <tamitta@microsoft.com>
2020-08-12 15:25:53 -05:00
victor-meng
fb71fb4e82 Refactor GenericRightPaneComponent to accept a ReactComponent as its content (#146) 2020-08-12 11:41:19 -07:00
victor-meng
455722c316 Fix deleteDatabase and deleteCollection with ARM (#143) 2020-08-11 18:36:42 -07:00
Tanuj Mittal
5886db81e9 Copy To functionality for notebooks (#141)
* Add Copy To functionality for notebooks

* Fix formatting

* Fix linting errors

* Fixes

* Fix build failure

* Rebase and address feedback

* Increase test coverage
2020-08-11 09:27:57 -07:00
Srinath Narayanan
7a3e54d43e Added support for acknowledging code of conduct for using public Notebook Gallery (#117)
* minro code edits

* Added support for acknowledging code of conduct

- Added CodeOfConduct component that shows links and a checkbox that must be acknwledged before seeing the public galley
- Added verbose message for notebook publish error (when another notebook with the same name exists in the gallery)
- Added a feature flag for enabling code of conduct acknowledgement

* Added Info Component

* minor edit

* fixed failign tests

* publish tab displayed only when code of conduct accepted

* added code of conduct fetch during publish

* fixed bug

* added test and addressed PR comments

* changed line endings

* added comment

* addressed PR comments
2020-08-11 00:37:05 -07:00
Steve Faulkner
3051961093 Add subscriptionId and authType to telemetry (#140) 2020-08-10 18:43:45 -05:00
Vignesh Rangaishenvi
abce15a6b2 Add init message when warming up notebook workspace (#139)
* Add init message

* Address comments
2020-08-10 15:02:24 -07:00
Steve Faulkner
a5b824ebb5 Fix master compile error 2020-08-10 12:02:18 -05:00
victor-meng
e28765d740 Add null check when reading offerAutopilotSettings (#138) 2020-08-10 11:55:43 -05:00
Srinath Narayanan
95f1efc03f Added support for notebook viewer link injection (#124)
* Added support for notebook viewer link injection

* updated tests
2020-08-10 01:53:51 -07:00
Steve Faulkner
455a6ac81b Fix update offer beyond throughput limit error (#135) 2020-08-06 18:15:40 -05:00
Vignesh Rangaishenvi
08ee86ecf1 Fix connection string renew token pane (#136)
* Fix IcM issue + conn string parsing

* format code

* Undo fix for IcM issue
2020-08-06 16:15:31 -07:00
Steve Faulkner
0011007d5f Refactor Global state into Context Files (#128) 2020-08-06 14:03:46 -05:00
vchske
d45af21996 Updating error message on mongo collection create (#118)
* 1) Updated mongo collection create pane to display a better error when a shard key is ill formed.
2) Updated an error message to use double quotes since no string interpolation is used. I thought it was included in a previoue PR but I guess the change didn't get staged.
2020-08-06 12:56:40 -05:00
Steve Faulkner
a64109ebaa Fix Generated ARM types (#131) 2020-08-06 09:27:41 -05:00
victor-meng
70f9b28499 Fix tabs manager test (#130) 2020-08-05 18:01:13 -07:00
Steve Faulkner
78e70cc7cc Refresh Caches only in Portal (#129) 2020-08-05 15:54:17 -05:00
Steve Faulkner
f132a8546c Move SQL database deletion to ARM (#126) 2020-08-04 18:03:14 -05:00
Tanuj Mittal
e6acf6686f Update NotebookReadOnlyRenderer.tsx (#127) 2020-08-04 10:24:25 -07:00
Steve Faulkner
2904a1a60d Move Delete Container call to use ARM when logged in with AAD (#110) 2020-08-03 17:11:07 -05:00
Tanuj Mittal
8c792fd147 Hide Azure Synapse Link button for Serverless accounts (#121) 2020-07-31 15:31:21 -07:00
Steve Faulkner
2a53dfabb5 Fix refresh resources button and remove portal specific notebooks call (#123) 2020-07-31 16:45:36 -05:00
Srinath Narayanan
14ef40029d Added session based view updation for gallery notebooks (#120) 2020-07-31 12:31:05 -07:00
Steve Faulkner
dab6e43d0d Hotfix: Remove extra JSON.stringify in Monogo update code path (#119) 2020-07-28 15:13:48 -05:00
Steve Faulkner
aea168c893 Add lint rule to prefer arror function (#114) 2020-07-27 16:40:04 -05:00
Steve Faulkner
fea321cd68 More ViewModel cleanup (#116) 2020-07-27 16:05:25 -05:00
Steve Faulkner
2e49ed45c3 Refactor DocumentClientUtilityBase to not be a class (#115) 2020-07-27 12:58:27 -05:00
Steve Faulkner
6d142f16f9 Refactor Data Access Utility (#112) 2020-07-24 16:45:48 -05:00
Steve Faulkner
6dcdacc8c4 Update CODEOWNERS 2020-07-24 15:45:42 -05:00
Tanuj Mittal
33969581ac Support serverless accounts (#109)
* Changes for serverless accounts

* Dont show upsell message for serverless accounts

* Update CassandraAddCollectionPane to support serverless
2020-07-24 13:13:54 -07:00
Srinath Narayanan
dc67c5f40b Added support for taking screenshot during Notebook publish to Gallery (#108)
* Added support for taking screenshot

- Screenshot is taken using html2canvas package
- Converted to base 64 and uploaded to metadata
- For Using first display output
  - Notebok object is passed instead of string, to publish pane
  - The first cell with output present is parsed out
  - The dom is also parsed to get corresponding div element to take screenshot of the first output

* fixed bug

* Addressed PR comments

- FIxed bug that didn't capture screenshot when mutiple notebook tabs are opened

* removed unnecessary dependencies

* fixed compile issues

* more edits
2020-07-23 00:43:53 -07:00
253 changed files with 9434 additions and 5938 deletions

View File

@@ -298,11 +298,9 @@ src/Utils/DatabaseAccountUtils.ts
src/Utils/JunoUtils.ts src/Utils/JunoUtils.ts
src/Utils/MessageValidation.ts src/Utils/MessageValidation.ts
src/Utils/NotebookConfigurationUtils.ts src/Utils/NotebookConfigurationUtils.ts
src/Utils/NotificationConsoleUtils.ts
src/Utils/OfferUtils.test.ts src/Utils/OfferUtils.test.ts
src/Utils/OfferUtils.ts src/Utils/OfferUtils.ts
src/Utils/PricingUtils.test.ts src/Utils/PricingUtils.test.ts
src/Utils/PricingUtils.ts
src/Utils/QueryUtils.test.ts src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts src/Utils/QueryUtils.ts
src/Utils/StringUtils.test.ts src/Utils/StringUtils.test.ts

View File

@@ -3,7 +3,7 @@ module.exports = {
browser: true, browser: true,
es6: true es6: true
}, },
plugins: ["@typescript-eslint", "no-null"], plugins: ["@typescript-eslint", "no-null", "prefer-arrow"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: { globals: {
Atomics: "readonly", Atomics: "readonly",
@@ -40,6 +40,7 @@ module.exports = {
"@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-extraneous-class": "error",
"no-null/no-null": "error", "no-null/no-null": "error",
"@typescript-eslint/no-explicit-any": "error" "@typescript-eslint/no-explicit-any": "error",
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }]
} }
}; };

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @Azure/cosmos-explorer-owners * @Azure/cosmos-explorer-owners @Azure/azure-cosmos-explorer-developers

View File

@@ -1,9 +1,13 @@
name: CI name: CI
on: on:
push: push:
branches: [master] branches:
- master
- hotfix/*
- release/*
pull_request: pull_request:
branches: [master] branches:
- master
jobs: jobs:
compile: compile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -52,6 +56,7 @@ jobs:
- run: npm run test - run: npm run test
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint, format, compile, unittest]
name: "Build" name: "Build"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -75,6 +80,7 @@ jobs:
path: dist/ path: dist/
endtoendemulator: endtoendemulator:
name: "End To End Tests | Emulator | SQL" name: "End To End Tests | Emulator | SQL"
needs: [lint, format, compile, unittest]
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -101,6 +107,7 @@ jobs:
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
endtoendsql: endtoendsql:
name: "End To End Tests | SQL" name: "End To End Tests | SQL"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -127,8 +134,14 @@ jobs:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }} CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
- uses: actions/upload-artifact@v2
name: videos
if: ${{ failure() }}
with:
path: "**/*.mp4"
endtoendmongo: endtoendmongo:
name: "End To End Tests | Mongo" name: "End To End Tests | Mongo"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -155,8 +168,14 @@ jobs:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }} CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
- uses: actions/upload-artifact@v2
if: ${{ failure() }}
name: videos
with:
path: "**/*.mp4"
accessibility: accessibility:
name: "Accessibility | Hosted" name: "Accessibility | Hosted"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -179,7 +198,7 @@ jobs:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
@@ -203,7 +222,7 @@ jobs:
path: "*.nupkg" path: "*.nupkg"
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:

View File

@@ -3,7 +3,7 @@
"pluginsFile": false, "pluginsFile": false,
"fixturesFolder": false, "fixturesFolder": false,
"supportFile": "./support/index.js", "supportFile": "./support/index.js",
"defaultCommandTimeout": 60000, "defaultCommandTimeout": 90000,
"chromeWebSecurity": false, "chromeWebSecurity": false,
"reporter": "mochawesome", "reporter": "mochawesome",
"reporterOptions": { "reporterOptions": {

View File

@@ -6,7 +6,7 @@
"scripts": { "scripts": {
"test": "cypress run", "test": "cypress run",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/", "wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"test:sql": "cypress run --browser chrome --headless --spec \"./integration/dataexplorer/SQL/*\"", "test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless", "test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
"test:debug": "cypress open" "test:debug": "cypress open"
}, },

View File

@@ -39,10 +39,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results // An object that configures minimum threshold enforcement for coverage results
coverageThreshold: { coverageThreshold: {
global: { global: {
branches: 18, branches: 20,
functions: 22, functions: 24,
lines: 28, lines: 30,
statements: 27 statements: 29.0
} }
}, },

237
package-lock.json generated
View File

@@ -6024,6 +6024,16 @@
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"pretty-format": "^24.9.0", "pretty-format": "^24.9.0",
"semver": "^6.2.0" "semver": "^6.2.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
} }
}, },
"jest-validate": { "jest-validate": {
@@ -7577,11 +7587,40 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true "dev": true
}, },
"@types/mkdirp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz",
"integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==",
"requires": {
"@types/node": "*"
}
},
"@types/node": { "@types/node": {
"version": "12.11.1", "version": "12.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz",
"integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==" "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A=="
}, },
"@types/node-fetch": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz",
"integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==",
"requires": {
"@types/node": "*",
"form-data": "^3.0.0"
},
"dependencies": {
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -7681,6 +7720,11 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
},
"@types/shallowequal": { "@types/shallowequal": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz",
@@ -8403,7 +8447,6 @@
"graceful-fs": "^4.1.11", "graceful-fs": "^4.1.11",
"lru-cache": "^4.1.1", "lru-cache": "^4.1.1",
"mississippi": "^2.0.0", "mississippi": "^2.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1", "move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1", "promise-inflight": "^1.0.1",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
@@ -9342,6 +9385,15 @@
"pkg-dir": "^3.0.0" "pkg-dir": "^3.0.0"
} }
}, },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"pify": { "pify": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@@ -9519,6 +9571,11 @@
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
"integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=" "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
}, },
"base64-arraybuffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
},
"base64-js": { "base64-js": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@@ -10075,9 +10132,9 @@
"dev": true "dev": true
}, },
"canvas": { "canvas": {
"version": "2.6.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.0.tgz", "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
"integrity": "sha512-bEO9f1ThmbknLPxCa8Es7obPlN9W3stB1bo7njlhOFKIdUTldeTqXCh9YclCPAi2pSQs84XA0jq/QEZXSzgyMw==", "integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
"requires": { "requires": {
"nan": "^2.14.0", "nan": "^2.14.0",
"node-pre-gyp": "^0.11.0", "node-pre-gyp": "^0.11.0",
@@ -10656,6 +10713,14 @@
"run-queue": "^1.0.0" "run-queue": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"rimraf": { "rimraf": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -10874,6 +10939,14 @@
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.1.1.tgz", "resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.1.1.tgz",
"integrity": "sha512-/PX6Bkk77ShgbOx/mpawHdEvS3PGgy1mmMktcztDPndWdMJxcorcQiivrs+nEljqtBpvNEhAmQky9tQR6FSm8Q==" "integrity": "sha512-/PX6Bkk77ShgbOx/mpawHdEvS3PGgy1mmMktcztDPndWdMJxcorcQiivrs+nEljqtBpvNEhAmQky9tQR6FSm8Q=="
}, },
"css-line-break": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
"requires": {
"base64-arraybuffer": "^0.2.0"
}
},
"css-loader": { "css-loader": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz",
@@ -12744,6 +12817,12 @@
"integrity": "sha1-EjaoEjkTkKGHetQAfCbnRTQclR8=", "integrity": "sha1-EjaoEjkTkKGHetQAfCbnRTQclR8=",
"dev": true "dev": true
}, },
"eslint-plugin-prefer-arrow": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.2.tgz",
"integrity": "sha512-C8YMhL+r8RMeMdYAw/rQtE6xNdMulj+zGWud/qIGnlmomiPRaLDGLMeskZ3alN6uMBojmooRimtdrXebLN4svQ==",
"dev": true
},
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.20.0", "version": "7.20.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.0.tgz",
@@ -15557,6 +15636,14 @@
} }
} }
}, },
"html2canvas": {
"version": "1.0.0-rc.5",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.5.tgz",
"integrity": "sha512-DtNqPxJNXPoTajs+lVQzGS1SULRI4GQaROeU5R41xH8acffHukxRh/NBVcTBsfCkJSkLq91rih5TpbEwUP9yWA==",
"requires": {
"css-line-break": "1.1.1"
}
},
"htmlparser2": { "htmlparser2": {
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -19754,6 +19841,16 @@
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"slash": "^2.0.0", "slash": "^2.0.0",
"source-map": "^0.6.0" "source-map": "^0.6.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
} }
}, },
"jest-validate": { "jest-validate": {
@@ -20337,6 +20434,16 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
},
"promise": { "promise": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@@ -21287,12 +21394,9 @@
} }
}, },
"mkdirp": { "mkdirp": {
"version": "0.5.5", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
"requires": {
"minimist": "^1.2.5"
}
}, },
"mkdirp-classic": { "mkdirp-classic": {
"version": "0.5.3", "version": "0.5.3",
@@ -21338,6 +21442,14 @@
"run-queue": "^1.0.3" "run-queue": "^1.0.3"
}, },
"dependencies": { "dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"rimraf": { "rimraf": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -21422,9 +21534,9 @@
} }
}, },
"needle": { "needle": {
"version": "2.4.1", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz", "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz",
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==", "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==",
"requires": { "requires": {
"debug": "^3.2.6", "debug": "^3.2.6",
"iconv-lite": "^0.4.4", "iconv-lite": "^0.4.4",
@@ -21677,6 +21789,14 @@
"tar": "^4" "tar": "^4"
}, },
"dependencies": { "dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"rimraf": { "rimraf": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -22171,11 +22291,11 @@
"integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=" "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo="
}, },
"p-retry": { "p-retry": {
"version": "3.0.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz",
"integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", "integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==",
"dev": true,
"requires": { "requires": {
"@types/retry": "^0.12.0",
"retry": "^0.12.0" "retry": "^0.12.0"
} }
}, },
@@ -22576,6 +22696,15 @@
"requires": { "requires": {
"ms": "^2.1.1" "ms": "^2.1.1"
} }
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
} }
} }
}, },
@@ -23944,8 +24073,7 @@
"retry": { "retry": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
"dev": true
}, },
"reusify": { "reusify": {
"version": "1.0.4", "version": "1.0.4",
@@ -24502,9 +24630,9 @@
"integrity": "sha1-ZfDBWZNSs1Ny7KrlolDmEHN27Wk=" "integrity": "sha1-ZfDBWZNSs1Ny7KrlolDmEHN27Wk="
}, },
"simple-concat": { "simple-concat": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
}, },
"simple-get": { "simple-get": {
"version": "3.1.0", "version": "3.1.0",
@@ -25476,6 +25604,16 @@
"mkdirp": "^0.5.0", "mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.3" "yallist": "^3.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
} }
}, },
"tar-fs": { "tar-fs": {
@@ -25833,6 +25971,14 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
}, },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"yargs-parser": { "yargs-parser": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
@@ -26901,6 +27047,15 @@
"readable-stream": "^2.0.1" "readable-stream": "^2.0.1"
} }
}, },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"readable-stream": { "readable-stream": {
"version": "2.3.7", "version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@@ -27012,6 +27167,15 @@
"integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==", "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==",
"dev": true "dev": true
}, },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ws": { "ws": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
@@ -27224,6 +27388,15 @@
"readable-stream": "^2.0.1" "readable-stream": "^2.0.1"
} }
}, },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"readable-stream": { "readable-stream": {
"version": "2.3.7", "version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@@ -27303,6 +27476,15 @@
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true "dev": true
}, },
"p-retry": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
"integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
"dev": true,
"requires": {
"retry": "^0.12.0"
}
},
"schema-utils": { "schema-utils": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
@@ -27531,6 +27713,17 @@
"dev": true, "dev": true,
"requires": { "requires": {
"mkdirp": "^0.5.1" "mkdirp": "^0.5.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
}
} }
}, },
"write-file-atomic": { "write-file-atomic": {

View File

@@ -34,13 +34,15 @@
"@nteract/transform-vega": "7.0.6", "@nteract/transform-vega": "7.0.6",
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3", "@phosphor/widgets": "1.9.3",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110", "@uifabric/react-cards": "0.109.110",
"@uifabric/styling": "7.13.7", "@uifabric/styling": "7.13.7",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "2.6.0", "canvas": "2.6.1",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2", "crossroads": "0.12.2",
@@ -54,15 +56,18 @@
"es6-symbol": "3.1.3", "es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2", "eslint-plugin-jest": "23.13.2",
"hasher": "1.2.0", "hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.5.1", "jquery": "3.5.1",
"jquery-typeahead": "2.10.6", "jquery-typeahead": "2.10.6",
"jquery-ui-dist": "1.12.1", "jquery-ui-dist": "1.12.1",
"knockout": "3.5.1", "knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.15.6", "monaco-editor": "0.15.6",
"object.entries": "1.1.0", "object.entries": "1.1.0",
"office-ui-fabric-react": "7.121.10", "office-ui-fabric-react": "7.121.10",
"p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"promise-polyfill": "8.1.0", "promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0", "promise.prototype.finally": "3.1.0",
@@ -130,6 +135,7 @@
"eslint": "7.3.1", "eslint": "7.3.1",
"eslint-cli": "1.1.1", "eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2", "eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
"expose-loader": "0.7.5", "expose-loader": "0.7.5",
"file-loader": "2.0.0", "file-loader": "2.0.0",
@@ -147,6 +153,7 @@
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "0.4.3", "mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.0",
"prettier": "1.19.1", "prettier": "1.19.1",
"puppeteer": "4.0.0", "puppeteer": "4.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
@@ -187,7 +194,8 @@
"build:contracts": "npm run compile:contracts", "build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js", "strictEligibleFiles": "node ./strict-migration-tools/index.js",
"autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js", "autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks" "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -1,13 +0,0 @@
import * as ViewModels from "../Contracts/ViewModels";
export class DefaultApi implements ViewModels.CosmosDbApi {
public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return false;
};
}
export class CassandraApi implements ViewModels.CosmosDbApi {
public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system";
};
}

View File

@@ -1,5 +1,5 @@
import { AutopilotTier } from "../Contracts/DataModels"; import { AutopilotTier } from "../Contracts/DataModels";
import { config } from "../Config"; import { configContext } from "../ConfigContext";
import { HashMap } from "./HashMap"; import { HashMap } from "./HashMap";
export class AuthorizationEndpoints { export class AuthorizationEndpoints {
@@ -7,14 +7,23 @@ export class AuthorizationEndpoints {
public static common: string = "https://login.windows.net/"; public static common: string = "https://login.windows.net/";
} }
export class CodeOfConductEndpoints {
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
}
export class BackendEndpoints { export class BackendEndpoints {
public static localhost: string = "https://localhost:12900"; public static localhost: string = "https://localhost:12900";
public static dev: string = "https://ext.documents-dev.windows-int.net"; public static dev: string = "https://ext.documents-dev.windows-int.net";
public static productionPortal: string = config.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com"; public static productionPortal: string = configContext.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
} }
export class EndpointsRegex { export class EndpointsRegex {
public static readonly cassandra = "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com"; public static readonly cassandra = [
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
"HostName=(.*).cassandra.cosmos.azure.com"
];
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com"; public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com"; public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com"; public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
@@ -101,6 +110,7 @@ export class CapabilityNames {
public static readonly EnableNotebooks: string = "EnableNotebooks"; public static readonly EnableNotebooks: string = "EnableNotebooks";
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless";
} }
export class Features { export class Features {
@@ -112,6 +122,8 @@ export class Features {
public static readonly enableTtl = "enablettl"; public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks"; public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish"; public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableCodeOfConduct = "enablecodeofconduct";
public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark"; public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint"; public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl"; public static readonly notebookServerUrl = "notebookserverurl";

View File

@@ -1,6 +1,7 @@
import { CosmosClient, tokenProvider, endpoint, requestPlugin, getTokenFromAuthService } from "./CosmosClient";
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants"; import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
import { config, Platform } from "../Config"; import { configContext, Platform, updateConfigContext, resetConfigContext } from "../ConfigContext";
import { updateUserContext } from "../UserContext";
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
describe("tokenProvider", () => { describe("tokenProvider", () => {
const options = { const options = {
@@ -32,7 +33,9 @@ describe("tokenProvider", () => {
}); });
it("does not call the auth service if a master key is set", async () => { it("does not call the auth service if a master key is set", async () => {
CosmosClient.masterKey("foo"); updateUserContext({
masterKey: "foo"
});
await tokenProvider(options); await tokenProvider(options);
expect((window.fetch as any).mock.calls.length).toBe(0); expect((window.fetch as any).mock.calls.length).toBe(0);
}); });
@@ -41,7 +44,7 @@ describe("tokenProvider", () => {
describe("getTokenFromAuthService", () => { describe("getTokenFromAuthService", () => {
beforeEach(() => { beforeEach(() => {
delete window.dataExplorer; delete window.dataExplorer;
delete config.BACKEND_ENDPOINT; resetConfigContext();
window.fetch = jest.fn().mockImplementation(() => { window.fetch = jest.fn().mockImplementation(() => {
return { return {
json: () => "{}", json: () => "{}",
@@ -64,7 +67,9 @@ describe("getTokenFromAuthService", () => {
}); });
it("builds the correct URL in dev", () => { it("builds the correct URL in dev", () => {
config.BACKEND_ENDPOINT = "https://localhost:1234"; updateConfigContext({
BACKEND_ENDPOINT: "https://localhost:1234"
});
getTokenFromAuthService("GET", "dbs", "foo"); getTokenFromAuthService("GET", "dbs", "foo");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens", "https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
@@ -75,7 +80,8 @@ describe("getTokenFromAuthService", () => {
describe("endpoint", () => { describe("endpoint", () => {
it("falls back to _databaseAccount", () => { it("falls back to _databaseAccount", () => {
CosmosClient.databaseAccount({ updateUserContext({
databaseAccount: {
id: "foo", id: "foo",
name: "foo", name: "foo",
location: "foo", location: "foo",
@@ -88,11 +94,14 @@ describe("endpoint", () => {
tableEndpoint: "foo", tableEndpoint: "foo",
cassandraEndpoint: "foo" cassandraEndpoint: "foo"
} }
}
}); });
expect(endpoint()).toEqual("bar"); expect(endpoint()).toEqual("bar");
}); });
it("uses _endpoint if set", () => { it("uses _endpoint if set", () => {
CosmosClient.endpoint("baz"); updateUserContext({
endpoint: "baz"
});
expect(endpoint()).toEqual("baz"); expect(endpoint()).toEqual("baz");
}); });
}); });
@@ -100,17 +109,17 @@ describe("endpoint", () => {
describe("requestPlugin", () => { describe("requestPlugin", () => {
beforeEach(() => { beforeEach(() => {
delete window.dataExplorerPlatform; delete window.dataExplorerPlatform;
delete config.PROXY_PATH; resetConfigContext();
delete config.BACKEND_ENDPOINT;
delete config.PROXY_PATH;
}); });
describe("Hosted", () => { describe("Hosted", () => {
it("builds a proxy URL in development", () => { it("builds a proxy URL in development", () => {
const next = jest.fn(); const next = jest.fn();
config.platform = Platform.Hosted; updateConfigContext({
config.BACKEND_ENDPOINT = "https://localhost:1234"; platform: Platform.Hosted,
config.PROXY_PATH = "/proxy"; BACKEND_ENDPOINT: "https://localhost:1234",
PROXY_PATH: "/proxy"
});
const headers = {}; const headers = {};
const endpoint = "https://docs.azure.com"; const endpoint = "https://docs.azure.com";
const path = "/dbs/foo"; const path = "/dbs/foo";
@@ -122,8 +131,7 @@ describe("requestPlugin", () => {
describe("Emulator", () => { describe("Emulator", () => {
it("builds a url for emulator proxy via webpack", () => { it("builds a url for emulator proxy via webpack", () => {
const next = jest.fn(); const next = jest.fn();
config.platform = Platform.Emulator; updateConfigContext({ platform: Platform.Emulator, PROXY_PATH: "/proxy" });
config.PROXY_PATH = "/proxy";
const headers = {}; const headers = {};
const endpoint = ""; const endpoint = "";
const path = "/dbs/foo"; const path = "/dbs/foo";

View File

@@ -1,39 +1,28 @@
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 { DatabaseAccount } from "../Contracts/DataModels"; import { configContext, Platform } from "../ConfigContext";
import { HttpHeaders, EmulatorMasterKey } from "./Constants"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { userContext } from "../UserContext";
import { config, Platform } from "../Config";
let _client: Cosmos.CosmosClient;
let _masterKey: string;
let _endpoint: string;
let _authorizationToken: string;
let _accessToken: string;
let _databaseAccount: DatabaseAccount;
let _subscriptionId: string;
let _resourceGroup: string;
let _resourceToken: string;
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: RequestInfo) => { export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo; const { verb, resourceId, resourceType, headers } = requestInfo;
if (config.platform === Platform.Emulator) { if (configContext.platform === Platform.Emulator) {
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization); return decodeURIComponent(headers.authorization);
} }
if (_masterKey) { if (userContext.masterKey) {
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization); return decodeURIComponent(headers.authorization);
} }
if (_resourceToken) { if (userContext.resourceToken) {
return _resourceToken; return userContext.resourceToken;
} }
const result = await getTokenFromAuthService(verb, resourceType, resourceId); const result = await getTokenFromAuthService(verb, resourceType, resourceId);
@@ -42,28 +31,33 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
}; };
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => { export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
requestContext.endpoint = config.PROXY_PATH; requestContext.endpoint = configContext.PROXY_PATH;
requestContext.headers["x-ms-proxy-target"] = endpoint(); requestContext.headers["x-ms-proxy-target"] = endpoint();
return next(requestContext); return next(requestContext);
}; };
export const endpoint = () => { export const endpoint = () => {
if (config.platform === Platform.Emulator) { if (configContext.platform === Platform.Emulator) {
// In worker scope, _global(self).parent does not exist // In worker scope, _global(self).parent does not exist
const location = _global.parent ? _global.parent.location : _global.location; const location = _global.parent ? _global.parent.location : _global.location;
return config.EMULATOR_ENDPOINT || location.origin; return configContext.EMULATOR_ENDPOINT || location.origin;
} }
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint); return (
userContext.endpoint ||
(userContext.databaseAccount &&
userContext.databaseAccount.properties &&
userContext.databaseAccount.properties.documentEndpoint)
);
}; };
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> { export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
try { try {
const host = config.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint(); const host = configContext.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint();
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", { const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-encrypted-auth-token": _accessToken "x-ms-encrypted-auth-token": userContext.accessToken
}, },
body: JSON.stringify({ body: JSON.stringify({
verb, verb,
@@ -75,22 +69,15 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
const result = JSON.parse(await response.json()); const result = JSON.parse(await response.json());
return result; return result;
} catch (error) { } catch (error) {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`
);
return Promise.reject(error); return Promise.reject(error);
} }
} }
export const CosmosClient = { export function client(): Cosmos.CosmosClient {
client(): Cosmos.CosmosClient {
if (_client) {
return _client;
}
const options: Cosmos.CosmosClientOptions = { const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || " ", // CosmosClient gets upset if we pass a falsy value here endpoint: endpoint() || " ", // CosmosClient gets upset if we pass a falsy value here
key: _masterKey, key: userContext.masterKey,
tokenProvider, tokenProvider,
connectionPolicy: { connectionPolicy: {
enableEndpointDiscovery: false enableEndpointDiscovery: false
@@ -102,79 +89,5 @@ export const CosmosClient = {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }]; (options as any).plugins = [{ on: "request", plugin: requestPlugin }];
} }
_client = new Cosmos.CosmosClient(options); return new Cosmos.CosmosClient(options);
return _client;
},
authorizationToken(value?: string): string {
if (typeof value === "undefined") {
return _authorizationToken;
} }
_authorizationToken = value;
_client = null;
return value;
},
accessToken(value?: string): string {
if (typeof value === "undefined") {
return _accessToken;
}
_accessToken = value;
_client = null;
return value;
},
masterKey(value?: string): string {
if (typeof value === "undefined") {
return _masterKey;
}
_client = null;
_masterKey = value;
return value;
},
endpoint(value?: string): string {
if (typeof value === "undefined") {
return _endpoint;
}
_client = null;
_endpoint = value;
return value;
},
databaseAccount(value?: DatabaseAccount): DatabaseAccount {
if (typeof value === "undefined") {
return _databaseAccount || ({} as any);
}
_client = null;
_databaseAccount = value;
return value;
},
subscriptionId(value?: string): string {
if (typeof value === "undefined") {
return _subscriptionId;
}
_client = null;
_subscriptionId = value;
return value;
},
resourceGroup(value?: string): string {
if (typeof value === "undefined") {
return _resourceGroup;
}
_client = null;
_resourceGroup = value;
return value;
},
resourceToken(value?: string): string {
if (typeof value === "undefined") {
return _resourceToken;
}
_client = null;
_resourceToken = value;
return value;
}
};

View File

@@ -17,14 +17,17 @@ import {
TriggerDefinition TriggerDefinition
} from "@azure/cosmos"; } from "@azure/cosmos";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest"; import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
import { CosmosClient } from "./CosmosClient"; import { client } from "./CosmosClient";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { MessageHandler } from "./MessageHandler"; import { sendCachedDataMessage } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { OfferUtils } from "../Utils/OfferUtils"; import { OfferUtils } from "../Utils/OfferUtils";
import { RequestOptions } from "@azure/cosmos/dist-esm"; import { RequestOptions } from "@azure/cosmos/dist-esm";
import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { Platform, configContext } from "../ConfigContext";
import DocumentId from "../Explorer/Tree/DocumentId";
import ConflictId from "../Explorer/Tree/ConflictId";
export function getCommonQueryOptions(options: FeedOptions): any { export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
@@ -43,28 +46,26 @@ export function getCommonQueryOptions(options: FeedOptions): any {
return options; return options;
} }
// TODO: Add timeout for all promises export function queryDocuments(
export abstract class DataAccessUtilityBase {
public queryDocuments(
databaseId: string, databaseId: string,
containerId: string, containerId: string,
query: string, query: string,
options: any options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> { ): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
options = getCommonQueryOptions(options); options = getCommonQueryOptions(options);
const documentsIterator = CosmosClient.client() const documentsIterator = client()
.database(databaseId) .database(databaseId)
.container(containerId) .container(containerId)
.items.query(query, options); .items.query(query, options);
return Q(documentsIterator); return Q(documentsIterator);
} }
public readStoredProcedures( export function readStoredProcedures(
collection: ViewModels.Collection, collection: ViewModels.Collection,
options?: any options?: any
): Q.Promise<DataModels.StoredProcedure[]> { ): Q.Promise<DataModels.StoredProcedure[]> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.storedProcedures.readAll(options) .scripts.storedProcedures.readAll(options)
@@ -73,13 +74,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public readStoredProcedure( export function readStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
requestedResource: DataModels.Resource, requestedResource: DataModels.Resource,
options?: any options?: any
): Q.Promise<DataModels.StoredProcedure> { ): Q.Promise<DataModels.StoredProcedure> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.storedProcedure(requestedResource.id) .scripts.storedProcedure(requestedResource.id)
@@ -87,12 +88,12 @@ export abstract class DataAccessUtilityBase {
.then(response => response.resource as DataModels.StoredProcedure) .then(response => response.resource as DataModels.StoredProcedure)
); );
} }
public readUserDefinedFunctions( export function readUserDefinedFunctions(
collection: ViewModels.Collection, collection: ViewModels.Collection,
options: any options: any
): Q.Promise<DataModels.UserDefinedFunction[]> { ): Q.Promise<DataModels.UserDefinedFunction[]> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.userDefinedFunctions.readAll(options) .scripts.userDefinedFunctions.readAll(options)
@@ -100,13 +101,13 @@ export abstract class DataAccessUtilityBase {
.then(response => response.resources as DataModels.UserDefinedFunction[]) .then(response => response.resources as DataModels.UserDefinedFunction[])
); );
} }
public readUserDefinedFunction( export function readUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
requestedResource: DataModels.Resource, requestedResource: DataModels.Resource,
options?: any options?: any
): Q.Promise<DataModels.UserDefinedFunction> { ): Q.Promise<DataModels.UserDefinedFunction> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.userDefinedFunction(requestedResource.id) .scripts.userDefinedFunction(requestedResource.id)
@@ -115,9 +116,9 @@ export abstract class DataAccessUtilityBase {
); );
} }
public readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> { export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.triggers.readAll(options) .scripts.triggers.readAll(options)
@@ -126,13 +127,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public readTrigger( export function readTrigger(
collection: ViewModels.Collection, collection: ViewModels.Collection,
requestedResource: DataModels.Resource, requestedResource: DataModels.Resource,
options?: any options?: any
): Q.Promise<DataModels.Trigger> { ): Q.Promise<DataModels.Trigger> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.trigger(requestedResource.id) .scripts.trigger(requestedResource.id)
@@ -141,7 +142,7 @@ export abstract class DataAccessUtilityBase {
); );
} }
public executeStoredProcedure( export function executeStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
storedProcedure: StoredProcedure, storedProcedure: StoredProcedure,
partitionKeyValue: any, partitionKeyValue: any,
@@ -150,7 +151,7 @@ export abstract class DataAccessUtilityBase {
// TODO remove this deferred. Kept it because of timeout code at bottom of function // TODO remove this deferred. Kept it because of timeout code at bottom of function
const deferred = Q.defer<any>(); const deferred = Q.defer<any>();
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.storedProcedure(storedProcedure.id()) .scripts.storedProcedure(storedProcedure.id())
@@ -169,11 +170,11 @@ export abstract class DataAccessUtilityBase {
); );
} }
public readDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> { export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue; const partitionKey = documentId.partitionKeyValue;
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.item(documentId.id(), partitionKey) .item(documentId.id(), partitionKey)
@@ -182,14 +183,14 @@ export abstract class DataAccessUtilityBase {
); );
} }
public getPartitionKeyHeaderForConflict(conflictId: ViewModels.ConflictId): Object { export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey; const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey;
const partitionKeyValue: any = conflictId.partitionKeyValue; const partitionKeyValue: any = conflictId.partitionKeyValue;
return this.getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue); return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
} }
public getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object { export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
if (!partitionKeyDefinition) { if (!partitionKeyDefinition) {
return undefined; return undefined;
} }
@@ -201,32 +202,32 @@ export abstract class DataAccessUtilityBase {
return [partitionKeyValue]; return [partitionKeyValue];
} }
public updateCollection( export function updateCollection(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: DataModels.Collection, newCollection: DataModels.Collection,
options: any = {} options: any = {}
): Q.Promise<DataModels.Collection> { ): Q.Promise<DataModels.Collection> {
return Q( return Q(
CosmosClient.client() client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.replace(newCollection as ContainerDefinition, options) .replace(newCollection as ContainerDefinition, options)
.then(async (response: ContainerResponse) => { .then(async (response: ContainerResponse) => {
return this.refreshCachedResources().then(() => response.resource as DataModels.Collection); return refreshCachedResources().then(() => response.resource as DataModels.Collection);
}) })
); );
} }
public updateDocument( export function updateDocument(
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
documentId: ViewModels.DocumentId, documentId: DocumentId,
newDocument: any newDocument: any
): Q.Promise<any> { ): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue; const partitionKey = documentId.partitionKeyValue;
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.item(documentId.id(), partitionKey) .item(documentId.id(), partitionKey)
@@ -235,28 +236,28 @@ export abstract class DataAccessUtilityBase {
); );
} }
public updateOffer( export function updateOffer(
offer: DataModels.Offer, offer: DataModels.Offer,
newOffer: DataModels.Offer, newOffer: DataModels.Offer,
options?: RequestOptions options?: RequestOptions
): Q.Promise<DataModels.Offer> { ): Q.Promise<DataModels.Offer> {
return Q( return Q(
CosmosClient.client() client()
.offer(offer.id) .offer(offer.id)
.replace(newOffer, options) .replace(newOffer, options)
.then(response => { .then(response => {
return Promise.all([this.refreshCachedOffers(), this.refreshCachedResources()]).then(() => response.resource); return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
}) })
); );
} }
public updateStoredProcedure( export function updateStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure, storedProcedure: DataModels.StoredProcedure,
options: any options: any
): Q.Promise<DataModels.StoredProcedure> { ): Q.Promise<DataModels.StoredProcedure> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.storedProcedure(storedProcedure.id) .scripts.storedProcedure(storedProcedure.id)
@@ -265,13 +266,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public updateUserDefinedFunction( export function updateUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction, userDefinedFunction: DataModels.UserDefinedFunction,
options?: any options?: any
): Q.Promise<DataModels.UserDefinedFunction> { ): Q.Promise<DataModels.UserDefinedFunction> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.userDefinedFunction(userDefinedFunction.id) .scripts.userDefinedFunction(userDefinedFunction.id)
@@ -280,13 +281,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public updateTrigger( export function updateTrigger(
collection: ViewModels.Collection, collection: ViewModels.Collection,
trigger: DataModels.Trigger, trigger: DataModels.Trigger,
options?: any options?: any
): Q.Promise<DataModels.Trigger> { ): Q.Promise<DataModels.Trigger> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.trigger(trigger.id) .scripts.trigger(trigger.id)
@@ -295,9 +296,9 @@ export abstract class DataAccessUtilityBase {
); );
} }
public createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> { export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.items.create(newDocument) .items.create(newDocument)
@@ -305,13 +306,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public createStoredProcedure( export function createStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
newStoredProcedure: DataModels.StoredProcedure, newStoredProcedure: DataModels.StoredProcedure,
options?: any options?: any
): Q.Promise<DataModels.StoredProcedure> { ): Q.Promise<DataModels.StoredProcedure> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.storedProcedures.create(newStoredProcedure, options) .scripts.storedProcedures.create(newStoredProcedure, options)
@@ -319,13 +320,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public createUserDefinedFunction( export function createUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
newUserDefinedFunction: DataModels.UserDefinedFunction, newUserDefinedFunction: DataModels.UserDefinedFunction,
options: any options: any
): Q.Promise<DataModels.UserDefinedFunction> { ): Q.Promise<DataModels.UserDefinedFunction> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.userDefinedFunctions.create(newUserDefinedFunction, options) .scripts.userDefinedFunctions.create(newUserDefinedFunction, options)
@@ -333,13 +334,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public createTrigger( export function createTrigger(
collection: ViewModels.Collection, collection: ViewModels.Collection,
newTrigger: DataModels.Trigger, newTrigger: DataModels.Trigger,
options?: any options?: any
): Q.Promise<DataModels.Trigger> { ): Q.Promise<DataModels.Trigger> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.triggers.create(newTrigger as TriggerDefinition, options) .scripts.triggers.create(newTrigger as TriggerDefinition, options)
@@ -347,11 +348,11 @@ export abstract class DataAccessUtilityBase {
); );
} }
public deleteDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> { export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue; const partitionKey = documentId.partitionKeyValue;
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.item(documentId.id(), partitionKey) .item(documentId.id(), partitionKey)
@@ -359,15 +360,15 @@ export abstract class DataAccessUtilityBase {
); );
} }
public deleteConflict( export function deleteConflict(
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
conflictId: ViewModels.ConflictId, conflictId: ConflictId,
options: any = {} options: any = {}
): Q.Promise<any> { ): Q.Promise<any> {
options.partitionKey = options.partitionKey || this.getPartitionKeyHeaderForConflict(conflictId); options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.conflict(conflictId.id()) .conflict(conflictId.id())
@@ -375,32 +376,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public deleteCollection(collection: ViewModels.Collection, options: any): Q.Promise<any> { export function deleteStoredProcedure(
return Q(
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.delete()
.then(() => this.refreshCachedResources())
);
}
public deleteDatabase(database: ViewModels.Database, options: any): Q.Promise<any> {
return Q(
CosmosClient.client()
.database(database.id())
.delete()
.then(() => this.refreshCachedResources())
);
}
public deleteStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure, storedProcedure: DataModels.StoredProcedure,
options: any options: any
): Q.Promise<any> { ): Q.Promise<any> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.storedProcedure(storedProcedure.id) .scripts.storedProcedure(storedProcedure.id)
@@ -408,13 +390,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public deleteUserDefinedFunction( export function deleteUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction, userDefinedFunction: DataModels.UserDefinedFunction,
options: any options: any
): Q.Promise<any> { ): Q.Promise<any> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.userDefinedFunction(userDefinedFunction.id) .scripts.userDefinedFunction(userDefinedFunction.id)
@@ -422,9 +404,13 @@ export abstract class DataAccessUtilityBase {
); );
} }
public deleteTrigger(collection: ViewModels.Collection, trigger: DataModels.Trigger, options: any): Q.Promise<any> { export function deleteTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger,
options: any
): Q.Promise<any> {
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.trigger(trigger.id) .scripts.trigger(trigger.id)
@@ -432,27 +418,7 @@ export abstract class DataAccessUtilityBase {
); );
} }
public readCollections(database: ViewModels.Database, options: any): Q.Promise<DataModels.Collection[]> { export function readCollectionQuotaInfo(
return Q(
CosmosClient.client()
.database(database.id())
.containers.readAll()
.fetchAll()
.then(response => response.resources as DataModels.Collection[])
);
}
public readCollection(databaseId: string, collectionId: string): Q.Promise<DataModels.Collection> {
return Q(
CosmosClient.client()
.database(databaseId)
.container(collectionId)
.read()
.then(response => response.resource as DataModels.Collection)
);
}
public readCollectionQuotaInfo(
collection: ViewModels.Collection, collection: ViewModels.Collection,
options: any options: any
): Q.Promise<DataModels.CollectionQuotaInfo> { ): Q.Promise<DataModels.CollectionQuotaInfo> {
@@ -462,7 +428,7 @@ export abstract class DataAccessUtilityBase {
options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true; options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true;
return Q( return Q(
CosmosClient.client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.read(options) .read(options)
@@ -487,16 +453,26 @@ export abstract class DataAccessUtilityBase {
); );
} }
public readOffers(options: any): Q.Promise<DataModels.Offer[]> { export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
(<any>window).dataExplorer.databaseAccount().id,
Constants.ClientDefaults.portalCacheTimeoutMs
]);
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK
}
return Q( return Q(
CosmosClient.client() client()
.offers.readAll() .offers.readAll()
.fetchAll() .fetchAll()
.then(response => response.resources) .then(response => response.resources)
); );
} }
public readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise<DataModels.OfferWithHeaders> { export function readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise<DataModels.OfferWithHeaders> {
options = options || {}; options = options || {};
options.initialHeaders = options.initialHeaders || {}; options.initialHeaders = options.initialHeaders || {};
if (!OfferUtils.isOfferV1(requestedResource)) { if (!OfferUtils.isOfferV1(requestedResource)) {
@@ -504,23 +480,14 @@ export abstract class DataAccessUtilityBase {
} }
return Q( return Q(
CosmosClient.client() client()
.offer(requestedResource.id) .offer(requestedResource.id)
.read(options) .read(options)
.then(response => ({ ...response.resource, headers: response.headers })) .then(response => ({ ...response.resource, headers: response.headers }))
); );
} }
public readDatabases(options: any): Q.Promise<DataModels.Database[]> { export function getOrCreateDatabaseAndCollection(
return Q(
CosmosClient.client()
.databases.readAll()
.fetchAll()
.then(response => response.resources as DataModels.Database[])
);
}
public getOrCreateDatabaseAndCollection(
request: DataModels.CreateDatabaseAndCollectionRequest, request: DataModels.CreateDatabaseAndCollectionRequest,
options: any options: any
): Q.Promise<DataModels.Collection> { ): Q.Promise<DataModels.Collection> {
@@ -561,7 +528,7 @@ export abstract class DataAccessUtilityBase {
} }
return Q( return Q(
CosmosClient.client() client()
.databases.createIfNotExists(createBody, databaseOptions) .databases.createIfNotExists(createBody, databaseOptions)
.then(response => { .then(response => {
return response.database.containers.create( return response.database.containers.create(
@@ -579,16 +546,19 @@ export abstract class DataAccessUtilityBase {
); );
}) })
.then(containerResponse => containerResponse.resource as DataModels.Collection) .then(containerResponse => containerResponse.resource as DataModels.Collection)
.finally(() => this.refreshCachedResources(options)) .finally(() => refreshCachedResources(options))
); );
} }
public createDatabase(request: DataModels.CreateDatabaseRequest, options: any): Q.Promise<DataModels.Database> { export function createDatabase(
request: DataModels.CreateDatabaseRequest,
options: any
): Q.Promise<DataModels.Database> {
var deferred = Q.defer<DataModels.Database>(); var deferred = Q.defer<DataModels.Database>();
this._createDatabase(request, options).then( _createDatabase(request, options).then(
(createdDatabase: DataModels.Database) => { (createdDatabase: DataModels.Database) => {
this.refreshCachedOffers().then(() => { refreshCachedOffers().then(() => {
deferred.resolve(createdDatabase); deferred.resolve(createdDatabase);
}); });
}, },
@@ -600,46 +570,36 @@ export abstract class DataAccessUtilityBase {
return deferred.promise; return deferred.promise;
} }
public refreshCachedOffers(): Q.Promise<void> { export function refreshCachedOffers(): Q.Promise<void> {
if (MessageHandler.canSendMessage()) { if (configContext.platform === Platform.Portal) {
return MessageHandler.sendCachedDataMessage(MessageTypes.RefreshOffers, []); return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
} else { } else {
return Q(); return Q();
} }
} }
public refreshCachedResources(options?: any): Q.Promise<void> { export function refreshCachedResources(options?: any): Q.Promise<void> {
if (MessageHandler.canSendMessage()) { if (configContext.platform === Platform.Portal) {
return MessageHandler.sendCachedDataMessage(MessageTypes.RefreshResources, []); return sendCachedDataMessage(MessageTypes.RefreshResources, []);
} else { } else {
return Q(); return Q();
} }
} }
public queryConflicts( export function queryConflicts(
databaseId: string, databaseId: string,
containerId: string, containerId: string,
query: string, query: string,
options: any options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> { ): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = CosmosClient.client() const documentsIterator = client()
.database(databaseId) .database(databaseId)
.container(containerId) .container(containerId)
.conflicts.query(query, options); .conflicts.query(query, options);
return Q(documentsIterator); return Q(documentsIterator);
} }
public updateOfferThroughputBeyondLimit( function _createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise<DataModels.Database> {
request: DataModels.UpdateOfferThroughputRequest,
options: any
): Q.Promise<void> {
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
}
private _createDatabase(
request: DataModels.CreateDatabaseRequest,
options: any = {}
): Q.Promise<DataModels.Database> {
const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request; const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request;
const createBody: DatabaseRequest = { id: databaseId }; const createBody: DatabaseRequest = { id: databaseId };
const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput"); const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput");
@@ -661,11 +621,10 @@ export abstract class DataAccessUtilityBase {
} }
return Q( return Q(
CosmosClient.client() client()
.databases.create(createBody, databaseOptions) .databases.create(createBody, databaseOptions)
.then((response: DatabaseResponse) => { .then((response: DatabaseResponse) => {
return this.refreshCachedResources(databaseOptions).then(() => response.resource); return refreshCachedResources(databaseOptions).then(() => response.resource);
}) })
); );
} }
}

View File

@@ -5,38 +5,36 @@ import * as ViewModels from "../Contracts/ViewModels";
import Q from "q"; import Q from "q";
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { DataAccessUtilityBase } from "./DataAccessUtilityBase"; import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import * as Logger from "./Logger"; import * as Logger from "./Logger";
import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities"; import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { RequestOptions } from "@azure/cosmos/dist-esm"; import { RequestOptions } from "@azure/cosmos/dist-esm";
import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import { sendNotificationForError } from "./dataAccess/sendNotificationForError";
// TODO: Log all promise resolutions and errors with verbosity levels // TODO: Log all promise resolutions and errors with verbosity levels
export default class DocumentClientUtilityBase { export function queryDocuments(
constructor(private _dataAccessUtility: DataAccessUtilityBase) {}
public queryDocuments(
databaseId: string, databaseId: string,
containerId: string, containerId: string,
query: string, query: string,
options: any options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> { ): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
return this._dataAccessUtility.queryDocuments(databaseId, containerId, query, options); return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
} }
public queryConflicts( export function queryConflicts(
databaseId: string, databaseId: string,
containerId: string, containerId: string,
query: string, query: string,
options: any options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> { ): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
return this._dataAccessUtility.queryConflicts(databaseId, containerId, query, options); return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
} }
public getEntityName() { export function getEntityName() {
const defaultExperience = const defaultExperience =
window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience(); window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience();
if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) { if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) {
@@ -45,7 +43,7 @@ export default class DocumentClientUtilityBase {
return "item"; return "item";
} }
public readStoredProcedures( export function readStoredProcedures(
collection: ViewModels.Collection, collection: ViewModels.Collection,
options: any = {} options: any = {}
): Q.Promise<DataModels.StoredProcedure[]> { ): Q.Promise<DataModels.StoredProcedure[]> {
@@ -54,8 +52,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Querying stored procedures for container ${collection.id()}` `Querying stored procedures for container ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.readStoredProcedures(collection, options)
.readStoredProcedures(collection, options)
.then( .then(
(storedProcedures: DataModels.StoredProcedure[]) => { (storedProcedures: DataModels.StoredProcedure[]) => {
deferred.resolve(storedProcedures); deferred.resolve(storedProcedures);
@@ -66,7 +63,7 @@ export default class DocumentClientUtilityBase {
`Failed to query stored procedures for container ${collection.id()}: ${JSON.stringify(error)}` `Failed to query stored procedures for container ${collection.id()}: ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "ReadStoredProcedures", error.code); Logger.logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -77,15 +74,15 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public readStoredProcedure( export function readStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
requestedResource: DataModels.Resource, requestedResource: DataModels.Resource,
options?: any options?: any
): Q.Promise<DataModels.StoredProcedure> { ): Q.Promise<DataModels.StoredProcedure> {
return this._dataAccessUtility.readStoredProcedure(collection, requestedResource, options); return DataAccessUtilityBase.readStoredProcedure(collection, requestedResource, options);
} }
public readUserDefinedFunctions( export function readUserDefinedFunctions(
collection: ViewModels.Collection, collection: ViewModels.Collection,
options: any = {} options: any = {}
): Q.Promise<DataModels.UserDefinedFunction[]> { ): Q.Promise<DataModels.UserDefinedFunction[]> {
@@ -94,8 +91,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Querying user defined functions for collection ${collection.id()}` `Querying user defined functions for collection ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.readUserDefinedFunctions(collection, options)
.readUserDefinedFunctions(collection, options)
.then( .then(
(userDefinedFunctions: DataModels.UserDefinedFunction[]) => { (userDefinedFunctions: DataModels.UserDefinedFunction[]) => {
deferred.resolve(userDefinedFunctions); deferred.resolve(userDefinedFunctions);
@@ -106,7 +102,7 @@ export default class DocumentClientUtilityBase {
`Failed to query user defined functions for container ${collection.id()}: ${JSON.stringify(error)}` `Failed to query user defined functions for container ${collection.id()}: ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "ReadUDFs", error.code); Logger.logError(JSON.stringify(error), "ReadUDFs", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -117,23 +113,22 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public readUserDefinedFunction( export function readUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
requestedResource: DataModels.Resource, requestedResource: DataModels.Resource,
options: any options: any
): Q.Promise<DataModels.UserDefinedFunction> { ): Q.Promise<DataModels.UserDefinedFunction> {
return this._dataAccessUtility.readUserDefinedFunction(collection, requestedResource, options); return DataAccessUtilityBase.readUserDefinedFunction(collection, requestedResource, options);
} }
public readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> { export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
var deferred = Q.defer<DataModels.Trigger[]>(); var deferred = Q.defer<DataModels.Trigger[]>();
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Querying triggers for container ${collection.id()}` `Querying triggers for container ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.readTriggers(collection, options)
.readTriggers(collection, options)
.then( .then(
(triggers: DataModels.Trigger[]) => { (triggers: DataModels.Trigger[]) => {
deferred.resolve(triggers); deferred.resolve(triggers);
@@ -144,7 +139,7 @@ export default class DocumentClientUtilityBase {
`Failed to query triggers for container ${collection.id()}: ${JSON.stringify(error)}` `Failed to query triggers for container ${collection.id()}: ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "ReadTriggers", error.code); Logger.logError(JSON.stringify(error), "ReadTriggers", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -155,15 +150,15 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public readTrigger( export function readTrigger(
collection: ViewModels.Collection, collection: ViewModels.Collection,
requestedResource: DataModels.Resource, requestedResource: DataModels.Resource,
options?: any options?: any
): Q.Promise<DataModels.Trigger> { ): Q.Promise<DataModels.Trigger> {
return this._dataAccessUtility.readTrigger(collection, requestedResource, options); return DataAccessUtilityBase.readTrigger(collection, requestedResource, options);
} }
public executeStoredProcedure( export function executeStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
storedProcedure: StoredProcedure, storedProcedure: StoredProcedure,
partitionKeyValue: any, partitionKeyValue: any,
@@ -175,8 +170,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Executing stored procedure ${storedProcedure.id()}` `Executing stored procedure ${storedProcedure.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then( .then(
(response: any) => { (response: any) => {
deferred.resolve(response); deferred.resolve(response);
@@ -193,7 +187,7 @@ export default class DocumentClientUtilityBase {
)}` )}`
); );
Logger.logError(JSON.stringify(error), "ExecuteStoredProcedure", error.code); Logger.logError(JSON.stringify(error), "ExecuteStoredProcedure", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -204,14 +198,14 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public queryDocumentsPage( export function queryDocumentsPage(
resourceName: string, resourceName: string,
documentsIterator: MinimalQueryIterator, documentsIterator: MinimalQueryIterator,
firstItemIndex: number, firstItemIndex: number,
options: any options: any
): Q.Promise<ViewModels.QueryResults> { ): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>(); var deferred = Q.defer<ViewModels.QueryResults>();
const entityName = this.getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Querying ${entityName} for container ${resourceName}` `Querying ${entityName} for container ${resourceName}`
@@ -232,7 +226,7 @@ export default class DocumentClientUtilityBase {
`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}` `Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code); Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -243,15 +237,14 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public readDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> { export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = this.getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Reading ${entityName} ${documentId.id()}` `Reading ${entityName} ${documentId.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.readDocument(collection, documentId)
.readDocument(collection, documentId)
.then( .then(
(document: any) => { (document: any) => {
deferred.resolve(document); deferred.resolve(document);
@@ -262,7 +255,7 @@ export default class DocumentClientUtilityBase {
`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}` `Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "ReadDocument", error.code); Logger.logError(JSON.stringify(error), "ReadDocument", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -273,7 +266,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public updateCollection( export function updateCollection(
databaseId: string, databaseId: string,
collection: ViewModels.Collection, collection: ViewModels.Collection,
newCollection: DataModels.Collection newCollection: DataModels.Collection
@@ -283,8 +276,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Updating container ${collection.id()}` `Updating container ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.updateCollection(databaseId, collection.id(), newCollection)
.updateCollection(databaseId, collection.id(), newCollection)
.then( .then(
(replacedCollection: DataModels.Collection) => { (replacedCollection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -299,7 +291,7 @@ export default class DocumentClientUtilityBase {
`Failed to update container ${collection.id()}: ${JSON.stringify(error)}` `Failed to update container ${collection.id()}: ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "UpdateCollection", error.code); Logger.logError(JSON.stringify(error), "UpdateCollection", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -310,19 +302,18 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public updateDocument( export function updateDocument(
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
documentId: ViewModels.DocumentId, documentId: DocumentId,
newDocument: any newDocument: any
): Q.Promise<any> { ): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = this.getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Updating ${entityName} ${documentId.id()}` `Updating ${entityName} ${documentId.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
.updateDocument(collection, documentId, newDocument)
.then( .then(
(updatedDocument: any) => { (updatedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -337,7 +328,7 @@ export default class DocumentClientUtilityBase {
`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}` `Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "UpdateDocument", error.code); Logger.logError(JSON.stringify(error), "UpdateDocument", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -348,7 +339,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public updateOffer( export function updateOffer(
offer: DataModels.Offer, offer: DataModels.Offer,
newOffer: DataModels.Offer, newOffer: DataModels.Offer,
options: RequestOptions options: RequestOptions
@@ -358,8 +349,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Updating offer for resource ${offer.resource}` `Updating offer for resource ${offer.resource}`
); );
this._dataAccessUtility DataAccessUtilityBase.updateOffer(offer, newOffer, options)
.updateOffer(offer, newOffer, options)
.then( .then(
(replacedOffer: DataModels.Offer) => { (replacedOffer: DataModels.Offer) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -382,7 +372,7 @@ export default class DocumentClientUtilityBase {
"UpdateOffer", "UpdateOffer",
error.code error.code
); );
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -393,43 +383,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public updateOfferThroughputBeyondLimit( export function updateStoredProcedure(
requestPayload: DataModels.UpdateOfferThroughputRequest,
options: any = {}
): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>();
const resourceDescriptionInfo: string = requestPayload.collectionName
? `database ${requestPayload.databaseName} and container ${requestPayload.collectionName}`
: `database ${requestPayload.databaseName}`;
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Requesting increase in throughput to ${requestPayload.throughput} for ${resourceDescriptionInfo}`
);
this._dataAccessUtility
.updateOfferThroughputBeyondLimit(requestPayload, options)
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully requested an increase in throughput to ${requestPayload.throughput} for ${resourceDescriptionInfo}`
);
deferred.resolve();
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to request an increase in throughput for ${requestPayload.throughput}: ${JSON.stringify(error)}`
);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
}
public updateStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure, storedProcedure: DataModels.StoredProcedure,
options?: any options?: any
@@ -439,8 +393,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Updating stored procedure ${storedProcedure.id}` `Updating stored procedure ${storedProcedure.id}`
); );
this._dataAccessUtility DataAccessUtilityBase.updateStoredProcedure(collection, storedProcedure, options)
.updateStoredProcedure(collection, storedProcedure, options)
.then( .then(
(updatedStoredProcedure: DataModels.StoredProcedure) => { (updatedStoredProcedure: DataModels.StoredProcedure) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -455,7 +408,7 @@ export default class DocumentClientUtilityBase {
`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}` `Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "UpdateStoredProcedure", error.code); Logger.logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -466,7 +419,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public updateUserDefinedFunction( export function updateUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction, userDefinedFunction: DataModels.UserDefinedFunction,
options: any = {} options: any = {}
@@ -476,8 +429,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Updating user defined function ${userDefinedFunction.id}` `Updating user defined function ${userDefinedFunction.id}`
); );
this._dataAccessUtility DataAccessUtilityBase.updateUserDefinedFunction(collection, userDefinedFunction, options)
.updateUserDefinedFunction(collection, userDefinedFunction, options)
.then( .then(
(updatedUserDefinedFunction: DataModels.UserDefinedFunction) => { (updatedUserDefinedFunction: DataModels.UserDefinedFunction) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -492,7 +444,7 @@ export default class DocumentClientUtilityBase {
`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}` `Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "UpdateUDF", error.code); Logger.logError(JSON.stringify(error), "UpdateUDF", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -503,11 +455,13 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public updateTrigger(collection: ViewModels.Collection, trigger: DataModels.Trigger): Q.Promise<DataModels.Trigger> { export function updateTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger
): Q.Promise<DataModels.Trigger> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Updating trigger ${trigger.id}`); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Updating trigger ${trigger.id}`);
this._dataAccessUtility DataAccessUtilityBase.updateTrigger(collection, trigger)
.updateTrigger(collection, trigger)
.then( .then(
(updatedTrigger: DataModels.Trigger) => { (updatedTrigger: DataModels.Trigger) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Updated trigger ${trigger.id}`); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Updated trigger ${trigger.id}`);
@@ -519,7 +473,7 @@ export default class DocumentClientUtilityBase {
`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}` `Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "UpdateTrigger", error.code); Logger.logError(JSON.stringify(error), "UpdateTrigger", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -530,15 +484,14 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> { export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = this.getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Creating new ${entityName} for container ${collection.id()}` `Creating new ${entityName} for container ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.createDocument(collection, newDocument)
.createDocument(collection, newDocument)
.then( .then(
(savedDocument: any) => { (savedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -553,7 +506,7 @@ export default class DocumentClientUtilityBase {
`Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}` `Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "CreateDocument", error.code); Logger.logError(JSON.stringify(error), "CreateDocument", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -564,7 +517,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public createStoredProcedure( export function createStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
newStoredProcedure: DataModels.StoredProcedure, newStoredProcedure: DataModels.StoredProcedure,
options?: any options?: any
@@ -574,8 +527,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Creating stored procedure for container ${collection.id()}` `Creating stored procedure for container ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.createStoredProcedure(collection, newStoredProcedure, options)
.createStoredProcedure(collection, newStoredProcedure, options)
.then( .then(
(createdStoredProcedure: DataModels.StoredProcedure) => { (createdStoredProcedure: DataModels.StoredProcedure) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -590,7 +542,7 @@ export default class DocumentClientUtilityBase {
`Error while creating stored procedure for container ${collection.id()}:\n ${JSON.stringify(error)}` `Error while creating stored procedure for container ${collection.id()}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "CreateStoredProcedure", error.code); Logger.logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -601,7 +553,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public createUserDefinedFunction( export function createUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
newUserDefinedFunction: DataModels.UserDefinedFunction, newUserDefinedFunction: DataModels.UserDefinedFunction,
options?: any options?: any
@@ -611,8 +563,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Creating user defined function for container ${collection.id()}` `Creating user defined function for container ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.createUserDefinedFunction(collection, newUserDefinedFunction, options)
.createUserDefinedFunction(collection, newUserDefinedFunction, options)
.then( .then(
(createdUserDefinedFunction: DataModels.UserDefinedFunction) => { (createdUserDefinedFunction: DataModels.UserDefinedFunction) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -627,7 +578,7 @@ export default class DocumentClientUtilityBase {
`Error while creating user defined function for container ${collection.id()}:\n ${JSON.stringify(error)}` `Error while creating user defined function for container ${collection.id()}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "CreateUDF", error.code); Logger.logError(JSON.stringify(error), "CreateUDF", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -638,7 +589,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public createTrigger( export function createTrigger(
collection: ViewModels.Collection, collection: ViewModels.Collection,
newTrigger: DataModels.Trigger, newTrigger: DataModels.Trigger,
options: any = {} options: any = {}
@@ -648,8 +599,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Creating trigger for container ${collection.id()}` `Creating trigger for container ${collection.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.createTrigger(collection, newTrigger, options)
.createTrigger(collection, newTrigger, options)
.then( .then(
(createdTrigger: DataModels.Trigger) => { (createdTrigger: DataModels.Trigger) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -664,7 +614,7 @@ export default class DocumentClientUtilityBase {
`Error while creating trigger for container ${collection.id()}:\n ${JSON.stringify(error)}` `Error while creating trigger for container ${collection.id()}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "CreateTrigger", error.code); Logger.logError(JSON.stringify(error), "CreateTrigger", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -675,15 +625,14 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public deleteDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> { export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = this.getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Deleting ${entityName} ${documentId.id()}` `Deleting ${entityName} ${documentId.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.deleteDocument(collection, documentId)
.deleteDocument(collection, documentId)
.then( .then(
(response: any) => { (response: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -698,7 +647,7 @@ export default class DocumentClientUtilityBase {
`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}` `Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "DeleteDocument", error.code); Logger.logError(JSON.stringify(error), "DeleteDocument", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -709,9 +658,9 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public deleteConflict( export function deleteConflict(
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
conflictId: ViewModels.ConflictId, conflictId: ConflictId,
options?: any options?: any
): Q.Promise<any> { ): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
@@ -720,8 +669,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Deleting conflict ${conflictId.id()}` `Deleting conflict ${conflictId.id()}`
); );
this._dataAccessUtility DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
.deleteConflict(collection, conflictId, options)
.then( .then(
(response: any) => { (response: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -736,7 +684,7 @@ export default class DocumentClientUtilityBase {
`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}` `Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "DeleteConflict", error.code); Logger.logError(JSON.stringify(error), "DeleteConflict", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -747,75 +695,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public deleteCollection(collection: ViewModels.Collection, options: any = {}): Q.Promise<any> { export function deleteStoredProcedure(
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting container ${collection.id()}`
);
this._dataAccessUtility
.deleteCollection(collection, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted container ${collection.id()}`
);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteCollection", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public deleteDatabase(database: ViewModels.Database, options: any = {}): Q.Promise<any> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting database ${database.id()}`
);
this._dataAccessUtility
.deleteDatabase(database, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted database ${database.id()}`
);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting database ${database.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteDatabase", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public deleteStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure, storedProcedure: DataModels.StoredProcedure,
options: any = {} options: any = {}
@@ -826,8 +706,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Deleting stored procedure ${storedProcedure.id}` `Deleting stored procedure ${storedProcedure.id}`
); );
this._dataAccessUtility DataAccessUtilityBase.deleteStoredProcedure(collection, storedProcedure, options)
.deleteStoredProcedure(collection, storedProcedure, options)
.then( .then(
(response: any) => { (response: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -842,7 +721,7 @@ export default class DocumentClientUtilityBase {
`Error while deleting stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}` `Error while deleting stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "DeleteStoredProcedure", error.code); Logger.logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -853,7 +732,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public deleteUserDefinedFunction( export function deleteUserDefinedFunction(
collection: ViewModels.Collection, collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction, userDefinedFunction: DataModels.UserDefinedFunction,
options: any = {} options: any = {}
@@ -863,8 +742,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Deleting user defined function ${userDefinedFunction.id}` `Deleting user defined function ${userDefinedFunction.id}`
); );
this._dataAccessUtility DataAccessUtilityBase.deleteUserDefinedFunction(collection, userDefinedFunction, options)
.deleteUserDefinedFunction(collection, userDefinedFunction, options)
.then( .then(
(response: any) => { (response: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -879,7 +757,7 @@ export default class DocumentClientUtilityBase {
`Error while deleting user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}` `Error while deleting user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "DeleteUDF", error.code); Logger.logError(JSON.stringify(error), "DeleteUDF", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -890,21 +768,17 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public deleteTrigger( export function deleteTrigger(
collection: ViewModels.Collection, collection: ViewModels.Collection,
trigger: DataModels.Trigger, trigger: DataModels.Trigger,
options: any = {} options: any = {}
): Q.Promise<DataModels.Trigger> { ): Q.Promise<DataModels.Trigger> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Deleting trigger ${trigger.id}`); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Deleting trigger ${trigger.id}`);
this._dataAccessUtility DataAccessUtilityBase.deleteTrigger(collection, trigger, options)
.deleteTrigger(collection, trigger, options)
.then( .then(
(response: any) => { (response: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted trigger ${trigger.id}`);
ConsoleDataType.Info,
`Successfully deleted trigger ${trigger.id}`
);
deferred.resolve(response); deferred.resolve(response);
}, },
(error: any) => { (error: any) => {
@@ -913,7 +787,7 @@ export default class DocumentClientUtilityBase {
`Error while deleting trigger ${trigger.id}:\n ${JSON.stringify(error)}` `Error while deleting trigger ${trigger.id}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "DeleteTrigger", error.code); Logger.logError(JSON.stringify(error), "DeleteTrigger", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -924,74 +798,15 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public refreshCachedResources(options: any = {}): Q.Promise<void> { export function refreshCachedResources(options: any = {}): Q.Promise<void> {
return this._dataAccessUtility.refreshCachedResources(options); return DataAccessUtilityBase.refreshCachedResources(options);
} }
public refreshCachedOffers(): Q.Promise<void> { export function refreshCachedOffers(): Q.Promise<void> {
return this._dataAccessUtility.refreshCachedOffers(); return DataAccessUtilityBase.refreshCachedOffers();
} }
public readCollections(database: ViewModels.Database, options: any = {}): Q.Promise<DataModels.Collection[]> { export function readCollectionQuotaInfo(
var deferred = Q.defer<DataModels.Collection[]>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying containers for database ${database.id()}`
);
this._dataAccessUtility
.readCollections(database, options)
.then(
(collections: DataModels.Collection[]) => {
deferred.resolve(collections);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying containers for database ${database.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadCollections", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public readCollection(databaseId: string, collectionId: string): Q.Promise<DataModels.Collection> {
const deferred = Q.defer<DataModels.Collection>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying container ${collectionId}`
);
this._dataAccessUtility
.readCollection(databaseId, collectionId)
.then(
(collection: DataModels.Collection) => {
deferred.resolve(collection);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadCollections", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public readCollectionQuotaInfo(
collection: ViewModels.Collection, collection: ViewModels.Collection,
options?: any options?: any
): Q.Promise<DataModels.CollectionQuotaInfo> { ): Q.Promise<DataModels.CollectionQuotaInfo> {
@@ -1001,8 +816,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Querying quota info for container ${collection.id}` `Querying quota info for container ${collection.id}`
); );
this._dataAccessUtility DataAccessUtilityBase.readCollectionQuotaInfo(collection, options)
.readCollectionQuotaInfo(collection, options)
.then( .then(
(quota: DataModels.CollectionQuotaInfo) => { (quota: DataModels.CollectionQuotaInfo) => {
deferred.resolve(quota); deferred.resolve(quota);
@@ -1013,7 +827,7 @@ export default class DocumentClientUtilityBase {
`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}` `Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code); Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -1024,12 +838,11 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> { export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
var deferred = Q.defer<DataModels.Offer[]>(); var deferred = Q.defer<DataModels.Offer[]>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offers"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offers");
this._dataAccessUtility DataAccessUtilityBase.readOffers(options)
.readOffers(options)
.then( .then(
(offers: DataModels.Offer[]) => { (offers: DataModels.Offer[]) => {
deferred.resolve(offers); deferred.resolve(offers);
@@ -1040,7 +853,7 @@ export default class DocumentClientUtilityBase {
`Error while querying offers:\n ${JSON.stringify(error)}` `Error while querying offers:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "ReadOffers", error.code); Logger.logError(JSON.stringify(error), "ReadOffers", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -1051,12 +864,14 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public readOffer(requestedResource: DataModels.Offer, options: any = {}): Q.Promise<DataModels.OfferWithHeaders> { export function readOffer(
requestedResource: DataModels.Offer,
options: any = {}
): Q.Promise<DataModels.OfferWithHeaders> {
var deferred = Q.defer<DataModels.OfferWithHeaders>(); var deferred = Q.defer<DataModels.OfferWithHeaders>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offer"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offer");
this._dataAccessUtility DataAccessUtilityBase.readOffer(requestedResource, options)
.readOffer(requestedResource, options)
.then( .then(
(offer: DataModels.OfferWithHeaders) => { (offer: DataModels.OfferWithHeaders) => {
deferred.resolve(offer); deferred.resolve(offer);
@@ -1067,7 +882,7 @@ export default class DocumentClientUtilityBase {
`Error while querying offer:\n ${JSON.stringify(error)}` `Error while querying offer:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "ReadOffer", error.code); Logger.logError(JSON.stringify(error), "ReadOffer", error.code);
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -1078,33 +893,7 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public readDatabases(options: any): Q.Promise<DataModels.Database[]> { export function getOrCreateDatabaseAndCollection(
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying databases");
this._dataAccessUtility
.readDatabases(options)
.then(
(databases: DataModels.Database[]) => {
deferred.resolve(databases);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying databases:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadDatabases", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public getOrCreateDatabaseAndCollection(
request: DataModels.CreateDatabaseAndCollectionRequest, request: DataModels.CreateDatabaseAndCollectionRequest,
options: any = {} options: any = {}
): Q.Promise<DataModels.Collection> { ): Q.Promise<DataModels.Collection> {
@@ -1114,8 +903,7 @@ export default class DocumentClientUtilityBase {
`Creating a new container ${request.collectionId} for database ${request.databaseId}` `Creating a new container ${request.collectionId} for database ${request.databaseId}`
); );
this._dataAccessUtility DataAccessUtilityBase.getOrCreateDatabaseAndCollection(request, options)
.getOrCreateDatabaseAndCollection(request, options)
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -1130,7 +918,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.Error, ConsoleDataType.Error,
`Error while creating container ${request.collectionId}:\n ${sanitizedError}` `Error while creating container ${request.collectionId}:\n ${sanitizedError}`
); );
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -1139,15 +927,17 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise<DataModels.Database> { export function createDatabase(
request: DataModels.CreateDatabaseRequest,
options: any = {}
): Q.Promise<DataModels.Database> {
const deferred: Q.Deferred<DataModels.Database> = Q.defer<DataModels.Database>(); const deferred: Q.Deferred<DataModels.Database> = Q.defer<DataModels.Database>();
const id = NotificationConsoleUtils.logConsoleMessage( const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Creating a new database ${request.databaseId}` `Creating a new database ${request.databaseId}`
); );
this._dataAccessUtility DataAccessUtilityBase.createDatabase(request, options)
.createDatabase(request, options)
.then( .then(
(database: DataModels.Database) => { (database: DataModels.Database) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -1161,7 +951,7 @@ export default class DocumentClientUtilityBase {
ConsoleDataType.Error, ConsoleDataType.Error,
`Error while creating database ${request.databaseId}:\n ${JSON.stringify(error)}` `Error while creating database ${request.databaseId}:\n ${JSON.stringify(error)}`
); );
this.sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -1169,16 +959,3 @@ export default class DocumentClientUtilityBase {
return deferred.promise; return deferred.promise;
} }
public sendNotificationForError(error: any) {
if (error && error.code === Constants.HttpStatusCodes.Forbidden) {
if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
MessageHandler.sendMessage({
type: MessageTypes.ForbiddenError,
reason: error && error.message ? error.message : error
});
}
}
}

View File

@@ -1,4 +1,4 @@
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
export function replaceKnownError(err: string): string { export function replaceKnownError(err: string): string {
@@ -7,6 +7,8 @@ export function replaceKnownError(err: string): string {
err.indexOf("SharedOffer is Disabled for your account") >= 0 err.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.";
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
} }
return err; return err;

View File

@@ -1,46 +1,26 @@
jest.mock("./MessageHandler");
import { LogEntryLevel } from "../Contracts/Diagnostics"; import { LogEntryLevel } from "../Contracts/Diagnostics";
import * as Logger from "./Logger"; import * as Logger from "./Logger";
import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { sendMessage } from "./MessageHandler";
describe("Logger", () => { describe("Logger", () => {
let sendMessageSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
sendMessageSpy = spyOn(MessageHandler, "sendMessage"); jest.resetAllMocks();
});
afterEach(() => {
sendMessageSpy = null;
}); });
it("should log info messages", () => { it("should log info messages", () => {
Logger.logInfo("Test info", "DocDB"); Logger.logInfo("Test info", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; expect(sendMessage).toBeCalled();
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Verbose);
expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test info");
}); });
it("should log error messages", () => { it("should log error messages", () => {
Logger.logError("Test error", "DocDB"); Logger.logError("Test error", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; expect(sendMessage).toBeCalled();
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Error);
expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test error");
}); });
it("should log warnings", () => { it("should log warnings", () => {
Logger.logWarning("Test warning", "DocDB"); Logger.logWarning("Test warning", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; expect(sendMessage).toBeCalled();
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Warning);
expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test warning");
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { MessageHandler } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts"; import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts";
import { appInsights } from "../Shared/appInsights"; import { appInsights } from "../Shared/appInsights";
import { SeverityLevel } from "@microsoft/applicationinsights-web"; import { SeverityLevel } from "@microsoft/applicationinsights-web";
@@ -33,7 +33,7 @@ export function logError(message: string | Error, area: string, code?: number):
} }
function _logEntry(entry: Diagnostics.LogEntry): void { function _logEntry(entry: Diagnostics.LogEntry): void {
MessageHandler.sendMessage({ sendMessage({
type: MessageTypes.LogInfo, type: MessageTypes.LogInfo,
data: JSON.stringify(entry) data: JSON.stringify(entry)
}); });

View File

@@ -1,65 +1,29 @@
import Q from "q"; import Q from "q";
import { CachedDataPromise, MessageHandler } from "./MessageHandler"; import * as MessageHandler from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
class MockMessageHandler extends MessageHandler {
public static addToMap(key: string, value: CachedDataPromise<any>): void {
MessageHandler.RequestMap[key] = value;
}
public static mapContainsKey(key: string): boolean {
return MessageHandler.RequestMap[key] != null;
}
public static clearAllEntries(): void {
MessageHandler.RequestMap = {};
}
public static runGarbageCollector(): void {
MessageHandler.runGarbageCollector();
}
}
describe("Message Handler", () => { describe("Message Handler", () => {
beforeEach(() => { it("should handle cached message", async () => {
MockMessageHandler.clearAllEntries(); let mockPromise = {
});
xit("should send cached data message", (done: any) => {
const testValidationCallback = (e: MessageEvent) => {
expect(e.data.data).toEqual(
jasmine.objectContaining({ type: MessageTypes.AllDatabases, params: ["some param"] })
);
e.currentTarget.removeEventListener(e.type, testValidationCallback);
done();
};
window.parent.addEventListener("message", testValidationCallback);
MockMessageHandler.sendCachedDataMessage(MessageTypes.AllDatabases, ["some param"]);
});
it("should handle cached message", () => {
let mockPromise: CachedDataPromise<any> = {
id: "123", id: "123",
startTime: new Date(), startTime: new Date(),
deferred: Q.defer<any>() deferred: Q.defer<any>()
}; };
let mockMessage = { message: { id: "123", data: "{}" } }; let mockMessage = { message: { id: "123", data: "{}" } };
MessageHandler.RequestMap[mockPromise.id] = mockPromise;
MockMessageHandler.addToMap(mockPromise.id, mockPromise); MessageHandler.handleCachedDataMessage(mockMessage);
MockMessageHandler.handleCachedDataMessage(mockMessage);
expect(mockPromise.deferred.promise.isFulfilled()).toBe(true); expect(mockPromise.deferred.promise.isFulfilled()).toBe(true);
}); });
it("should delete fulfilled promises on running the garbage collector", () => { it("should delete fulfilled promises on running the garbage collector", async () => {
let mockPromise: CachedDataPromise<any> = { let message = {
id: "123", id: "123",
startTime: new Date(), startTime: new Date(),
deferred: Q.defer<any>() deferred: Q.defer<any>()
}; };
MockMessageHandler.addToMap(mockPromise.id, mockPromise); MessageHandler.handleCachedDataMessage(message);
mockPromise.deferred.reject("some error"); MessageHandler.runGarbageCollector();
MockMessageHandler.runGarbageCollector(); expect(MessageHandler.RequestMap["123"]).toBeUndefined();
expect(MockMessageHandler.mapContainsKey(mockPromise.id)).toBe(false);
}); });
}); });

View File

@@ -9,36 +9,24 @@ export interface CachedDataPromise<T> {
id: string; id: string;
} }
/** export const RequestMap: Record<string, CachedDataPromise<any>> = {};
* For some reason, typescript emits a Map() in the compiled js output(despite the target being set to ES5) forcing us to define our own polyfill,
* so we define our own custom implementation of the ES6 Map to work around it.
*/
type Map = { [key: string]: CachedDataPromise<any> };
export class MessageHandler { export function handleCachedDataMessage(message: any): void {
protected static RequestMap: Map = {};
public static handleCachedDataMessage(message: any): void {
const messageContent = message && message.message; const messageContent = message && message.message;
if ( if (message == null || messageContent == null || messageContent.id == null || !RequestMap[messageContent.id]) {
message == null ||
messageContent == null ||
messageContent.id == null ||
!MessageHandler.RequestMap[messageContent.id]
) {
return; return;
} }
const cachedDataPromise = MessageHandler.RequestMap[messageContent.id]; const cachedDataPromise = RequestMap[messageContent.id];
if (messageContent.error != null) { if (messageContent.error != null) {
cachedDataPromise.deferred.reject(messageContent.error); cachedDataPromise.deferred.reject(messageContent.error);
} else { } else {
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data)); cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
} }
MessageHandler.runGarbageCollector(); runGarbageCollector();
} }
public static sendCachedDataMessage<TResponseDataModel>( export function sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes, messageType: MessageTypes,
params: Object[], params: Object[],
timeoutInMs?: number timeoutInMs?: number
@@ -48,8 +36,8 @@ export class MessageHandler {
startTime: new Date(), startTime: new Date(),
id: _.uniqueId() id: _.uniqueId()
}; };
MessageHandler.RequestMap[cachedDataPromise.id] = cachedDataPromise; RequestMap[cachedDataPromise.id] = cachedDataPromise;
MessageHandler.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id }); sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
//TODO: Use telemetry to measure optimal time to resolve/reject promises //TODO: Use telemetry to measure optimal time to resolve/reject promises
return cachedDataPromise.deferred.promise.timeout( return cachedDataPromise.deferred.promise.timeout(
@@ -58,8 +46,8 @@ export class MessageHandler {
); );
} }
public static sendMessage(data: any): void { export function sendMessage(data: any): void {
if (MessageHandler.canSendMessage()) { if (canSendMessage()) {
window.parent.postMessage( window.parent.postMessage(
{ {
signature: "pcIframe", signature: "pcIframe",
@@ -70,16 +58,16 @@ export class MessageHandler {
} }
} }
public static canSendMessage(): boolean { export function canSendMessage(): boolean {
return window.parent !== window; return window.parent !== window;
} }
protected static runGarbageCollector() { // TODO: This is exported just for testing. It should not be.
Object.keys(MessageHandler.RequestMap).forEach((key: string) => { export function runGarbageCollector() {
const promise: Q.Promise<any> = MessageHandler.RequestMap[key].deferred.promise; Object.keys(RequestMap).forEach((key: string) => {
const promise: Q.Promise<any> = RequestMap[key].deferred.promise;
if (promise.isFulfilled() || promise.isRejected()) { if (promise.isFulfilled() || promise.isRejected()) {
delete MessageHandler.RequestMap[key]; delete RequestMap[key];
} }
}); });
} }
}

View File

@@ -1,16 +1,18 @@
import { AuthType } from "../AuthType";
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { updateUserContext } from "../UserContext";
import { import {
_createMongoCollectionWithARM,
deleteDocument, deleteDocument,
getEndpoint, getEndpoint,
queryDocuments, queryDocuments,
readDocument, readDocument,
updateDocument updateDocument,
_createMongoCollectionWithARM
} from "./MongoProxyClient"; } from "./MongoProxyClient";
import { AuthType } from "../AuthType";
import { Collection, DatabaseAccount, DocumentId } from "../Contracts/ViewModels";
import { config } from "../Config";
import { CosmosClient } from "./CosmosClient";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
jest.mock("../ResourceProvider/ResourceProviderClient.ts"); jest.mock("../ResourceProvider/ResourceProviderClient.ts");
const databaseId = "testDB"; const databaseId = "testDB";
@@ -60,13 +62,15 @@ const databaseAccount = {
tableEndpoint: "foo", tableEndpoint: "foo",
cassandraEndpoint: "foo" cassandraEndpoint: "foo"
} }
}; } as DatabaseAccount;
describe("MongoProxyClient", () => { describe("MongoProxyClient", () => {
describe("queryDocuments", () => { describe("queryDocuments", () => {
beforeEach(() => { beforeEach(() => {
delete config.BACKEND_ENDPOINT; resetConfigContext();
CosmosClient.databaseAccount(databaseAccount as any); updateUserContext({
databaseAccount
});
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "" serverId: () => ""
@@ -86,7 +90,7 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234"; updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
queryDocuments(databaseId, collection, true, "{}"); queryDocuments(databaseId, collection, true, "{}");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk", "https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -96,8 +100,10 @@ describe("MongoProxyClient", () => {
}); });
describe("readDocument", () => { describe("readDocument", () => {
beforeEach(() => { beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT; resetConfigContext();
CosmosClient.databaseAccount(databaseAccount as any); updateUserContext({
databaseAccount
});
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "" serverId: () => ""
@@ -117,7 +123,7 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234"; updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
readDocument(databaseId, collection, documentId); readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -127,8 +133,10 @@ describe("MongoProxyClient", () => {
}); });
describe("createDocument", () => { describe("createDocument", () => {
beforeEach(() => { beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT; resetConfigContext();
CosmosClient.databaseAccount(databaseAccount as any); updateUserContext({
databaseAccount
});
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "" serverId: () => ""
@@ -148,7 +156,7 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234"; updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
readDocument(databaseId, collection, documentId); readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -158,8 +166,10 @@ describe("MongoProxyClient", () => {
}); });
describe("updateDocument", () => { describe("updateDocument", () => {
beforeEach(() => { beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT; resetConfigContext();
CosmosClient.databaseAccount(databaseAccount as any); updateUserContext({
databaseAccount
});
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "" serverId: () => ""
@@ -171,7 +181,7 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct URL", () => { it("builds the correct URL", () => {
updateDocument(databaseId, collection, documentId, {}); updateDocument(databaseId, collection, documentId, "{}");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object) expect.any(Object)
@@ -179,8 +189,8 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234"; updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
updateDocument(databaseId, collection, documentId, {}); updateDocument(databaseId, collection, documentId, "{}");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object) expect.any(Object)
@@ -189,8 +199,10 @@ describe("MongoProxyClient", () => {
}); });
describe("deleteDocument", () => { describe("deleteDocument", () => {
beforeEach(() => { beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT; resetConfigContext();
CosmosClient.databaseAccount(databaseAccount as any); updateUserContext({
databaseAccount
});
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "" serverId: () => ""
@@ -210,7 +222,7 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234"; updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
deleteDocument(databaseId, collection, documentId); deleteDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -220,9 +232,11 @@ describe("MongoProxyClient", () => {
}); });
describe("getEndpoint", () => { describe("getEndpoint", () => {
beforeEach(() => { beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT; resetConfigContext();
delete window.authType; delete window.authType;
CosmosClient.databaseAccount(databaseAccount as any); updateUserContext({
databaseAccount
});
window.dataExplorer = { window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com", extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => "" serverId: () => ""
@@ -235,7 +249,7 @@ describe("MongoProxyClient", () => {
}); });
it("returns a development endpoint", () => { it("returns a development endpoint", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234"; updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
const endpoint = getEndpoint(databaseAccount as DatabaseAccount); const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer"); expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
}); });

View File

@@ -1,22 +1,22 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import queryString from "querystring";
import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as DataExplorerConstants from "../Common/Constants"; import * as DataExplorerConstants from "../Common/Constants";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import EnvironmentUtility from "./EnvironmentUtility";
import queryString from "querystring";
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { AuthType } from "../AuthType";
import { Collection } from "../Contracts/ViewModels";
import { config } from "../Config";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { CosmosClient } from "./CosmosClient";
import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import { Collection } from "../Contracts/ViewModels";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient"; import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { userContext } from "../UserContext";
import EnvironmentUtility from "./EnvironmentUtility";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler";
const defaultHeaders = { const defaultHeaders = {
[HttpHeaders.apiType]: ApiType.MongoDB.toString(), [HttpHeaders.apiType]: ApiType.MongoDB.toString(),
@@ -26,9 +26,9 @@ const defaultHeaders = {
function authHeaders() { function authHeaders() {
if (window.authType === AuthType.EncryptedToken) { if (window.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: CosmosClient.accessToken() }; return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
} else { } else {
return { [HttpHeaders.authorization]: CosmosClient.authorizationToken() }; return { [HttpHeaders.authorization]: userContext.authorizationToken };
} }
} }
@@ -67,7 +67,7 @@ export function queryDocuments(
query: string, query: string,
continuationToken?: string continuationToken?: string
): Promise<QueryResponse> { ): Promise<QueryResponse> {
const databaseAccount = CosmosClient.databaseAccount(); const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = { const params = {
db: databaseId, db: databaseId,
@@ -75,8 +75,8 @@ export function queryDocuments(
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`, resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
rid: collection.rid, rid: collection.rid,
rtype: "docs", rtype: "docs",
sid: CosmosClient.subscriptionId(), sid: userContext.subscriptionId,
rg: CosmosClient.resourceGroup(), rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : "" collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
@@ -123,9 +123,9 @@ export function queryDocuments(
export function readDocument( export function readDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
documentId: ViewModels.DocumentId documentId: DocumentId
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
const databaseAccount = CosmosClient.databaseAccount(); const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 4).join("/"); const path = idComponents.slice(0, 4).join("/");
@@ -136,8 +136,8 @@ export function readDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`, resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid, rid,
rtype: "docs", rtype: "docs",
sid: CosmosClient.subscriptionId(), sid: userContext.subscriptionId,
rg: CosmosClient.resourceGroup(), rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -169,7 +169,7 @@ export function createDocument(
partitionKeyProperty: string, partitionKeyProperty: string,
documentContent: unknown documentContent: unknown
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
const databaseAccount = CosmosClient.databaseAccount(); const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = { const params = {
db: databaseId, db: databaseId,
@@ -177,8 +177,8 @@ export function createDocument(
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`, resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
rid: collection.rid, rid: collection.rid,
rtype: "docs", rtype: "docs",
sid: CosmosClient.subscriptionId(), sid: userContext.subscriptionId,
rg: CosmosClient.resourceGroup(), rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "" pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
}; };
@@ -205,10 +205,10 @@ export function createDocument(
export function updateDocument( export function updateDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
documentId: ViewModels.DocumentId, documentId: DocumentId,
documentContent: unknown documentContent: string
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
const databaseAccount = CosmosClient.databaseAccount(); const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/"); const path = idComponents.slice(0, 5).join("/");
@@ -219,8 +219,8 @@ export function updateDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`, resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid, rid,
rtype: "docs", rtype: "docs",
sid: CosmosClient.subscriptionId(), sid: userContext.subscriptionId,
rg: CosmosClient.resourceGroup(), rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -230,7 +230,7 @@ export function updateDocument(
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
method: "PUT", method: "PUT",
body: JSON.stringify(documentContent), body: documentContent,
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders(),
@@ -246,12 +246,8 @@ export function updateDocument(
}); });
} }
export function deleteDocument( export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
databaseId: string, const databaseAccount = userContext.databaseAccount;
collection: Collection,
documentId: ViewModels.DocumentId
): Promise<void> {
const databaseAccount = CosmosClient.databaseAccount();
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/"); const path = idComponents.slice(0, 5).join("/");
@@ -262,8 +258,8 @@ export function deleteDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`, resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid, rid,
rtype: "docs", rtype: "docs",
sid: CosmosClient.subscriptionId(), sid: userContext.subscriptionId,
rg: CosmosClient.resourceGroup(), rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -298,7 +294,7 @@ export function createMongoCollectionWithProxy(
isSharded: boolean, isSharded: boolean,
autopilotOptions?: DataModels.RpOptions autopilotOptions?: DataModels.RpOptions
): Promise<DataModels.Collection> { ): Promise<DataModels.Collection> {
const databaseAccount = CosmosClient.databaseAccount(); const databaseAccount = userContext.databaseAccount;
const params: DataModels.MongoParameters = { const params: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint, resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: databaseId, db: databaseId,
@@ -310,8 +306,8 @@ export function createMongoCollectionWithProxy(
is: isSharded, is: isSharded,
rid: "", rid: "",
rtype: "colls", rtype: "colls",
sid: CosmosClient.subscriptionId(), sid: userContext.subscriptionId,
rg: CosmosClient.resourceGroup(), rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
isAutoPilot: false isAutoPilot: false
}; };
@@ -355,7 +351,7 @@ export function createMongoCollectionWithARM(
isSharded: boolean, isSharded: boolean,
additionalOptions?: DataModels.RpOptions additionalOptions?: DataModels.RpOptions
): Promise<DataModels.CreateCollectionWithRpResponse> { ): Promise<DataModels.CreateCollectionWithRpResponse> {
const databaseAccount = CosmosClient.databaseAccount(); const databaseAccount = userContext.databaseAccount;
const params: DataModels.MongoParameters = { const params: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint, resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: databaseId, db: databaseId,
@@ -367,8 +363,8 @@ export function createMongoCollectionWithARM(
is: isSharded, is: isSharded,
rid: "", rid: "",
rtype: "colls", rtype: "colls",
sid: CosmosClient.subscriptionId(), sid: userContext.subscriptionId,
rg: CosmosClient.resourceGroup(), rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
analyticalStorageTtl analyticalStorageTtl
}; };
@@ -385,11 +381,11 @@ export function createMongoCollectionWithARM(
return _createMongoCollectionWithARM(armEndpoint, params, additionalOptions); return _createMongoCollectionWithARM(armEndpoint, params, additionalOptions);
} }
export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string { export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
const serverId = window.dataExplorer.serverId(); const serverId = window.dataExplorer.serverId();
const extensionEndpoint = window.dataExplorer.extensionEndpoint(); const extensionEndpoint = window.dataExplorer.extensionEndpoint();
let url = config.MONGO_BACKEND_ENDPOINT let url = configContext.MONGO_BACKEND_ENDPOINT
? config.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer" ? configContext.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint); : EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
if (window.authType === AuthType.EncryptedToken) { if (window.authType === AuthType.EncryptedToken) {
@@ -408,16 +404,14 @@ async function errorHandling(response: Response, action: string, params: unknown
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}` `Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
); );
if (response.status === HttpStatusCodes.Forbidden) { if (response.status === HttpStatusCodes.Forbidden) {
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
return; return;
} }
throw new Error(errorMessage); throw new Error(errorMessage);
} }
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${ return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
CosmosClient.databaseAccount().name
}/mongodbDatabases/${params.db}/collections/${params.coll}`;
} }
export async function _createMongoCollectionWithARM( export async function _createMongoCollectionWithARM(

View File

@@ -1,47 +0,0 @@
import "jquery";
import * as Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { CosmosClient } from "./CosmosClient";
export class NotificationsClientBase implements ViewModels.NotificationsClient {
private _extensionEndpoint: string;
private _notificationsApiSuffix: string;
protected constructor(notificationsApiSuffix: string) {
this._notificationsApiSuffix = notificationsApiSuffix;
}
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
const subscriptionId: string = CosmosClient.subscriptionId();
const resourceGroup: string = CosmosClient.resourceGroup();
const url: string = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers: any = {};
headers[authorizationHeader.header] = authorizationHeader.token;
$.ajax({
url: url,
type: "GET",
headers: headers,
cache: false
}).then(
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
deferred.resolve(notifications);
},
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
deferred.reject(xhr.responseText);
}
);
return deferred.promise;
}
public setExtensionEndpoint(extensionEndpoint: string): void {
this._extensionEndpoint = extensionEndpoint;
}
}

View File

@@ -1,18 +1,26 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as _ from "underscore"; import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import * as ErrorParserUtility from "./ErrorParserUtility";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "./CosmosClient";
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as Logger from "./Logger";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext";
import {
createDocument,
deleteDocument,
getOrCreateDatabaseAndCollection,
queryDocuments,
queryDocumentsPage
} from "./DocumentClientUtilityBase";
import * as ErrorParserUtility from "./ErrorParserUtility";
import * as Logger from "./Logger";
export class QueriesClient implements ViewModels.QueriesClient { export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
paths: [`/${SavedQueries.PartitionKeyProperty}`], paths: [`/${SavedQueries.PartitionKeyProperty}`],
kind: BackendDefaults.partitionKeyKind, kind: BackendDefaults.partitionKeyKind,
@@ -33,8 +41,7 @@ export class QueriesClient implements ViewModels.QueriesClient {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
"Setting up account for saving queries" "Setting up account for saving queries"
); );
return this.container.documentClientUtility return getOrCreateDatabaseAndCollection({
.getOrCreateDatabaseAndCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
databaseId: SavedQueries.DatabaseName, databaseId: SavedQueries.DatabaseName,
partitionKey: QueriesClient.PartitionKey, partitionKey: QueriesClient.PartitionKey,
@@ -89,8 +96,7 @@ export class QueriesClient implements ViewModels.QueriesClient {
`Saving query ${query.queryName}` `Saving query ${query.queryName}`
); );
query.id = query.queryName; query.id = query.queryName;
return this.container.documentClientUtility return createDocument(queriesCollection, query)
.createDocument(queriesCollection, query)
.then( .then(
(savedQuery: DataModels.Query) => { (savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -131,17 +137,11 @@ export class QueriesClient implements ViewModels.QueriesClient {
const options: any = { enableCrossPartitionQuery: true }; const options: any = { enableCrossPartitionQuery: true };
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
return this.container.documentClientUtility return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
.queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
.then( .then(
(queryIterator: QueryIterator<ItemDefinition & Resource>) => { (queryIterator: QueryIterator<ItemDefinition & Resource>) => {
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> => const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
this.container.documentClientUtility.queryDocumentsPage( queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
queriesCollection.id(),
queryIterator,
firstItemIndex,
options
);
return QueryUtils.queryAllPages(fetchQueries).then( return QueryUtils.queryAllPages(fetchQueries).then(
(results: ViewModels.QueryResults) => { (results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
@@ -217,17 +217,16 @@ export class QueriesClient implements ViewModels.QueriesClient {
`Deleting query ${query.queryName}` `Deleting query ${query.queryName}`
); );
query.id = query.queryName; query.id = query.queryName;
const documentId: ViewModels.DocumentId = new DocumentId( const documentId = new DocumentId(
{ {
partitionKey: QueriesClient.PartitionKey, partitionKey: QueriesClient.PartitionKey,
partitionKeyProperty: "id" partitionKeyProperty: "id"
} as ViewModels.DocumentsTab, } as DocumentsTab,
query, query,
query.queryName query.queryName
); // TODO: Remove DocumentId's dependency on DocumentsTab ); // TODO: Remove DocumentId's dependency on DocumentsTab
const options: any = { partitionKey: query.resourceId }; const options: any = { partitionKey: query.resourceId };
return this.container.documentClientUtility return deleteDocument(queriesCollection, documentId)
.deleteDocument(queriesCollection, documentId)
.then( .then(
() => { () => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
@@ -250,10 +249,10 @@ export class QueriesClient implements ViewModels.QueriesClient {
} }
public getResourceId(): string { public getResourceId(): string {
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount(); const databaseAccount = userContext.databaseAccount;
const databaseAccountName: string = (databaseAccount && databaseAccount.name) || ""; const databaseAccountName = (databaseAccount && databaseAccount.name) || "";
const subscriptionId: string = CosmosClient.subscriptionId() || ""; const subscriptionId = userContext.subscriptionId || "";
const resourceGroup: string = CosmosClient.resourceGroup() || ""; const resourceGroup = userContext.resourceGroup || "";
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`; return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`;
} }

View File

@@ -0,0 +1,46 @@
jest.mock("../../Utils/arm/request");
jest.mock("../MessageHandler");
jest.mock("../CosmosClient");
import { deleteCollection } from "./deleteCollection";
import { armRequest } from "../../Utils/arm/request";
import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteCollection", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await deleteCollection("database", "collection");
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
container: () => {
return {
delete: (): unknown => undefined
};
}
};
}
});
await deleteCollection("database", "collection");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,57 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try {
if (window.authType === AuthType.AAD) {
await deleteCollectionWithARM(databaseId, collectionId);
} else {
await client()
.database(databaseId)
.container(collectionId)
.delete();
}
} catch (error) {
logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteCollection", error.code);
sendNotificationForError(error);
throw error;
}
logConsoleInfo(`Successfully deleted container ${collectionId}`);
clearMessage();
await refreshCachedResources();
}
function deleteCollectionWithARM(databaseId: string, collectionId: string): Promise<void> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return deleteSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.MongoDB:
return deleteMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.Cassandra:
return deleteCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.Graph:
return deleteGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.Table:
return deleteTable(subscriptionId, resourceGroup, accountName, collectionId);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}

View File

@@ -0,0 +1,42 @@
jest.mock("../../Utils/arm/request");
jest.mock("../MessageHandler");
jest.mock("../CosmosClient");
import { deleteDatabase } from "./deleteDatabase";
import { armRequest } from "../../Utils/arm/request";
import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteDatabase", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await deleteDatabase("database");
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
delete: (): unknown => undefined
};
}
});
await deleteDatabase("database");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,54 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function deleteDatabase(databaseId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
try {
if (window.authType === AuthType.AAD) {
await deleteDatabaseWithARM(databaseId);
} else {
await client()
.database(databaseId)
.delete();
}
} catch (error) {
logConsoleError(`Error while deleting database ${databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteDatabase", error.code);
sendNotificationForError(error);
throw error;
}
logConsoleInfo(`Successfully deleted database ${databaseId}`);
clearMessage();
await refreshCachedResources();
}
function deleteDatabaseWithARM(databaseId: string): Promise<void> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return deleteSqlDatabase(subscriptionId, resourceGroup, accountName, databaseId);
case DefaultAccountExperienceType.MongoDB:
return deleteMongoDBDatabase(subscriptionId, resourceGroup, accountName, databaseId);
case DefaultAccountExperienceType.Cassandra:
return deleteCassandraKeyspace(subscriptionId, resourceGroup, accountName, databaseId);
case DefaultAccountExperienceType.Graph:
return deleteGremlinDatabase(subscriptionId, resourceGroup, accountName, databaseId);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}

View File

@@ -0,0 +1,35 @@
jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient";
import { readCollection } from "./readCollection";
import { updateUserContext } from "../../UserContext";
describe("readCollection", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
});
it("should call SDK if logged in with resource token", async () => {
window.authType = AuthType.ResourceToken;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
container: () => {
return {
read: (): unknown => ({})
};
}
};
}
});
await readCollection("database", "collection");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,24 @@
import * as DataModels from "../../Contracts/DataModels";
import { client } from "../CosmosClient";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
let collection: DataModels.Collection;
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.read();
collection = response.resource as DataModels.Collection;
} catch (error) {
logConsoleError(`Error while querying container ${collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollection", error.code);
sendNotificationForError(error);
throw error;
}
clearMessage();
return collection;
}

View File

@@ -0,0 +1,45 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
import { readCollections } from "./readCollections";
import { updateUserContext } from "../../UserContext";
describe("readCollections", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await readCollections("database");
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
containers: {
readAll: () => {
return {
fetchAll: (): unknown => []
};
}
}
};
}
});
await readCollections("database");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,66 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
let collections: DataModels.Collection[];
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try {
if (window.authType === AuthType.AAD) {
collections = await readCollectionsWithARM(databaseId);
} else {
const sdkResponse = await client()
.database(databaseId)
.containers.readAll()
.fetchAll();
collections = sdkResponse.resources as DataModels.Collection[];
}
} catch (error) {
logConsoleError(`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollections", error.code);
sendNotificationForError(error);
throw error;
}
clearMessage();
return collections;
}
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await listSqlContainers(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await listMongoDBCollections(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await listCassandraTables(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await listGremlinGraphs(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Table:
rpResponse = await listTables(subscriptionId, resourceGroup, accountName);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.value?.map(collection => collection.properties?.resource as DataModels.Collection);
}

View File

@@ -0,0 +1,41 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
import { readDatabases } from "./readDatabases";
import { updateUserContext } from "../../UserContext";
describe("readDatabases", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await readDatabases();
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
databases: {
readAll: () => {
return {
fetchAll: (): unknown => []
};
}
}
});
await readDatabases();
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,61 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient";
import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`);
try {
if (window.authType === AuthType.AAD) {
databases = await readDatabasesWithARM();
} else {
const sdkResponse = await client()
.databases.readAll()
.fetchAll();
databases = sdkResponse.resources as DataModels.Database[];
}
} catch (error) {
logConsoleError(`Error while querying databases:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadDatabases", error.code);
sendNotificationForError(error);
throw error;
}
clearMessage();
return databases;
}
async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.value?.map(database => database.properties?.resource as DataModels.Database);
}

View File

@@ -0,0 +1,20 @@
import * as Constants from "../Constants";
import { sendMessage } from "../MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
interface CosmosError {
code: number;
message?: string;
}
export function sendNotificationForError(error: CosmosError): void {
if (error && error.code === Constants.HttpStatusCodes.Forbidden) {
if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
sendMessage({
type: MessageTypes.ForbiddenError,
reason: error && error.message ? error.message : error
});
}
}

View File

@@ -0,0 +1,26 @@
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
describe("updateOfferThroughputBeyondLimit", () => {
it("should call fetch", async () => {
window.fetch = jest.fn(() => {
return {
ok: true
};
});
window.dataExplorer = {
logConsoleData: jest.fn(),
deleteInProgressConsoleDataWithId: jest.fn(),
extensionEndpoint: jest.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
await updateOfferThroughputBeyondLimit({
subscriptionId: "foo",
resourceGroup: "foo",
databaseAccountName: "foo",
databaseName: "foo",
throughput: 1000000000,
offerIsRUPerMinuteThroughputEnabled: false
});
expect(window.fetch).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,52 @@
import { Platform, configContext } from "../../ConfigContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { HttpHeaders } from "../Constants";
interface UpdateOfferThroughputRequest {
subscriptionId: string;
resourceGroup: string;
databaseAccountName: string;
databaseName: string;
collectionName?: string;
throughput: number;
offerIsRUPerMinuteThroughputEnabled: boolean;
offerAutopilotSettings?: AutoPilotOfferSettings;
}
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
if (configContext.platform !== Platform.Portal) {
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
}
const resourceDescriptionInfo = request.collectionName
? `database ${request.databaseName} and container ${request.collectionName}`
: `database ${request.databaseName}`;
const clearMessage = logConsoleProgress(
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
);
const explorer = window.dataExplorer;
const url = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
const authorizationHeader = getAuthorizationHeader();
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(request),
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
});
if (response.ok) {
logConsoleInfo(
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
);
clearMessage();
return undefined;
}
const error = await response.json();
logConsoleError(`Failed to request an increase in throughput for ${request.throughput}: ${error.message}`);
clearMessage();
throw new Error(error.message);
}

View File

@@ -4,7 +4,7 @@ export enum Platform {
Emulator = "Emulator" Emulator = "Emulator"
} }
interface Config { interface ConfigContext {
platform: Platform; platform: Platform;
allowedParentFrameOrigins: RegExp; allowedParentFrameOrigins: RegExp;
gitSha?: string; gitSha?: string;
@@ -28,7 +28,7 @@ interface Config {
} }
// Default configuration // Default configuration
let config: Config = { let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal, platform: Platform.Portal,
allowedParentFrameOrigins: /^https:\/\/portal\.azure\.com$|^https:\/\/portal\.azure\.us$|^https:\/\/portal\.azure\.cn$|^https:\/\/portal\.microsoftazure\.de$|^https:\/\/.+\.portal\.azure\.com$|^https:\/\/.+\.portal\.azure\.us$|^https:\/\/.+\.portal\.azure\.cn$|^https:\/\/.+\.portal\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.com$|^https:\/\/main\.documentdb\.ext\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.cn$|^https:\/\/main\.documentdb\.ext\.azure\.us$/, allowedParentFrameOrigins: /^https:\/\/portal\.azure\.com$|^https:\/\/portal\.azure\.us$|^https:\/\/portal\.azure\.cn$|^https:\/\/portal\.microsoftazure\.de$|^https:\/\/.+\.portal\.azure\.com$|^https:\/\/.+\.portal\.azure\.us$|^https:\/\/.+\.portal\.azure\.cn$|^https:\/\/.+\.portal\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.com$|^https:\/\/main\.documentdb\.ext\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.cn$|^https:\/\/main\.documentdb\.ext\.azure\.us$/,
// Webpack injects this at build time // Webpack injects this at build time
@@ -46,22 +46,35 @@ let config: Config = {
JUNO_ENDPOINT: "https://tools.cosmos.azure.com" JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
}; };
export function resetConfigContext(): void {
if (process.env.NODE_ENV !== "test") {
throw new Error("resetConfigContext can only becalled in a test environment");
}
configContext = {} as ConfigContext;
}
export function updateConfigContext(newContext: Partial<ConfigContext>): void {
Object.assign(configContext, newContext);
}
// Injected for local develpment. These will be removed in the production bundle by webpack // Injected for local develpment. These will be removed in the production bundle by webpack
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
const port: string = process.env.PORT || "1234"; const port: string = process.env.PORT || "1234";
config.BACKEND_ENDPOINT = "https://localhost:" + port; updateConfigContext({
config.MONGO_BACKEND_ENDPOINT = "https://localhost:" + port; BACKEND_ENDPOINT: "https://localhost:" + port,
config.PROXY_PATH = "/proxy"; MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
config.EMULATOR_ENDPOINT = "https://localhost:8081"; PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: "https://localhost:8081"
});
} }
export async function initializeConfiguration(): Promise<Config> { export async function initializeConfiguration(): Promise<ConfigContext> {
try { try {
const response = await fetch("./config.json"); const response = await fetch("./config.json");
if (response.status === 200) { if (response.status === 200) {
try { try {
const externalConfig = await response.json(); const externalConfig = await response.json();
config = Object.assign({}, config, externalConfig); Object.assign(configContext, externalConfig);
} catch (error) { } catch (error) {
console.error("Unable to parse json in config file"); console.error("Unable to parse json in config file");
console.error(error); console.error(error);
@@ -70,12 +83,13 @@ export async function initializeConfiguration(): Promise<Config> {
// Allow override of any config value with URL query parameters // Allow override of any config value with URL query parameters
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
params.forEach((value, key) => { params.forEach((value, key) => {
(config as any)[key] = value; // eslint-disable-next-line @typescript-eslint/no-explicit-any
(configContext as any)[key] = value;
}); });
} catch (error) { } catch (error) {
console.log("No configuration file found using defaults"); console.log("No configuration file found using defaults");
} }
return config; return configContext;
} }
export { config }; export { configContext };

View File

@@ -312,17 +312,6 @@ export interface Query {
query: string; query: string;
} }
export interface UpdateOfferThroughputRequest {
subscriptionId: string;
resourceGroup: string;
databaseAccountName: string;
databaseName: string;
collectionName: string;
throughput: number;
offerIsRUPerMinuteThroughputEnabled: boolean;
offerAutopilotSettings?: AutoPilotOfferSettings;
}
export interface AutoPilotOfferSettings { export interface AutoPilotOfferSettings {
tier?: AutopilotTier; tier?: AutopilotTier;
maximumTierThroughput?: number; maximumTierThroughput?: number;

View File

@@ -1,41 +1,16 @@
import * as DataModels from "./DataModels"; import * as DataModels from "./DataModels";
import * as monaco from "monaco-editor";
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase";
import Q from "q"; import Q from "q";
import { AccessibleVerticalList } from "../Explorer/Tree/AccessibleVerticalList";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient"; import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { GitHubClient } from "../GitHub/GitHubClient";
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
import { NotebookContentItem } from "../Explorer/Notebook/NotebookContentItem";
import { QueryMetrics } from "@azure/cosmos"; import { QueryMetrics } from "@azure/cosmos";
import { UploadDetails } from "../workers/upload/definitions"; import { UploadDetails } from "../workers/upload/definitions";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import ConflictsTab from "../Explorer/Tabs/ConflictsTab";
import Trigger from "../Explorer/Tree/Trigger"; import Trigger from "../Explorer/Tree/Trigger";
import DocumentId from "../Explorer/Tree/DocumentId";
export interface ExplorerOptions { import ConflictId from "../Explorer/Tree/ConflictId";
documentClientUtility: DocumentClientUtilityBase;
notificationsClient: NotificationsClient;
isEmulator: boolean;
}
export interface Capability extends DataModels.Capability {}
export interface ConfigurationOverrides extends DataModels.ConfigurationOverrides {}
export interface NavbarButtonConfig extends CommandButtonComponentProps {}
export interface DatabaseAccount extends DataModels.DatabaseAccount {}
export interface KernelConnectionMetadata {
name: string;
configurationEndpoints: DataModels.NotebookConfigurationEndpoints;
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
}
export interface TokenProvider { export interface TokenProvider {
getAuthHeader(): Promise<Headers>; getAuthHeader(): Promise<Headers>;
@@ -75,11 +50,6 @@ export interface WaitsForTemplate {
isTemplateReady: ko.Observable<boolean>; isTemplateReady: ko.Observable<boolean>;
} }
export interface AdHocAccessData {
readWriteUrl: string;
readUrl: string;
}
export interface TreeNode { export interface TreeNode {
nodeKind: string; nodeKind: string;
rid: string; rid: string;
@@ -201,118 +171,15 @@ export interface Collection extends CollectionBase {
getLabel(): string; getLabel(): string;
} }
export interface DocumentId {
container: DocumentsTab;
rid: string;
self: string;
ts: string;
partitionKeyValue: any;
partitionKeyProperty: string;
partitionKey: DataModels.PartitionKey;
stringPartitionKeyValue: string;
id: ko.Observable<string>;
isDirty: ko.Observable<boolean>;
click(): void;
getPartitionKeyValueAsString(): string;
loadDocument(): Q.Promise<any>;
partitionKeyHeader(): Object;
}
export interface ConflictId {
container: ConflictsTab;
rid: string;
self: string;
ts: string;
partitionKeyValue: any;
partitionKeyProperty: string;
partitionKey: DataModels.PartitionKey;
stringPartitionKeyValue: string;
id: ko.Observable<string>;
operationType: string;
resourceId: string;
resourceType: string;
isDirty: ko.Observable<boolean>;
click(): void;
buildDocumentIdFromConflict(partitionKeyValue: any): DocumentId;
getPartitionKeyValueAsString(): string;
loadConflict(): Q.Promise<any>;
}
/** /**
* Options used to initialize pane * Options used to initialize pane
*/ */
export interface PaneOptions { export interface PaneOptions {
id: string; id: string;
documentClientUtility: DocumentClientUtilityBase;
visible: ko.Observable<boolean>; visible: ko.Observable<boolean>;
container?: Explorer; container?: Explorer;
} }
export interface ContextualPane {
documentClientUtility: DocumentClientUtilityBase;
formErrors: ko.Observable<string>;
formErrorsDetails: ko.Observable<string>;
id: string;
title: ko.Observable<string>;
visible: ko.Observable<boolean>;
firstFieldHasFocus: ko.Observable<boolean>;
isExecuting: ko.Observable<boolean>;
submit: () => void;
cancel: () => void;
open: () => void;
close: () => void;
resetData: () => void;
showErrorDetails: () => void;
onCloseKeyPress(source: any, event: KeyboardEvent): void;
onPaneKeyDown(source: any, event: KeyboardEvent): boolean;
}
export interface GitHubReposPaneOptions extends PaneOptions {
gitHubClient: GitHubClient;
junoClient: JunoClient;
}
export interface PublishNotebookPaneOptions extends PaneOptions {
junoClient: JunoClient;
}
export interface PublishNotebookPaneOpenOptions {
name: string;
author: string;
content: string;
}
export interface AddCollectionPaneOptions extends PaneOptions {
isPreferredApiTable: ko.Computed<boolean>;
databaseId?: string;
databaseSelfLink?: string;
}
export interface UploadFilePaneOpenOptions {
paneTitle: string;
selectFileInputLabel: string;
errorMessage: string; // Could not upload notebook
inProgressMessage: string; // Uploading notebook
successMessage: string; // Successfully uploaded notebook
onSubmit: (file: File) => Promise<any>;
extensions?: string; // input accept field. E.g: .ipynb
submitButtonLabel?: string;
}
export interface StringInputPaneOpenOptions {
paneTitle: string;
inputLabel: string;
errorMessage: string;
inProgressMessage: string;
successMessage: string;
onSubmit: (input: string) => Promise<any>;
submitButtonLabel: string;
defaultInput?: string;
}
/** /**
* Graph configuration * Graph configuration
*/ */
@@ -382,19 +249,6 @@ export interface DocumentRequestContainer {
resourceName?: string; resourceName?: string;
} }
export interface NotificationsClient {
fetchNotifications(): Q.Promise<DataModels.Notification[]>;
setExtensionEndpoint(extensionEndpoint: string): void;
}
export interface QueriesClient {
setupQueriesCollection(): Promise<DataModels.Collection>;
saveQuery(query: DataModels.Query): Promise<void>;
getQueries(): Promise<DataModels.Query[]>;
deleteQuery(query: DataModels.Query): Promise<void>;
getResourceId(): string;
}
export interface DocumentClientOption { export interface DocumentClientOption {
endpoint?: string; endpoint?: string;
masterKey?: string; masterKey?: string;
@@ -406,11 +260,10 @@ export interface TabOptions {
tabKind: CollectionTabKind; tabKind: CollectionTabKind;
title: string; title: string;
tabPath: string; tabPath: string;
documentClientUtility: DocumentClientUtilityBase;
selfLink: string; selfLink: string;
isActive: ko.Observable<boolean>; isActive: ko.Observable<boolean>;
hashLocation: string; hashLocation: string;
onUpdateTabsButtons: (buttons: NavbarButtonConfig[]) => void; onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>; isTabsContentExpanded?: ko.Observable<boolean>;
onLoadStartKey?: number; onLoadStartKey?: number;
@@ -423,47 +276,6 @@ export interface TabOptions {
theme?: string; theme?: string;
} }
export interface SparkMasterTabOptions extends TabOptions {
clusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
container: Explorer;
}
export interface GraphTabOptions extends TabOptions {
account: DatabaseAccount;
masterKey: string;
collectionId: string;
databaseId: string;
collectionPartitionKeyProperty: string;
}
export interface NotebookTabOptions extends TabOptions {
account: DatabaseAccount;
masterKey: string;
container: Explorer;
notebookContentItem: NotebookContentItem;
}
export interface TerminalTabOptions extends TabOptions {
account: DatabaseAccount;
container: Explorer;
kind: TerminalKind;
}
export interface GalleryTabOptions extends TabOptions {
account: DatabaseAccount;
container: Explorer;
junoClient: JunoClient;
notebookUrl?: string;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
}
export interface NotebookViewerTabOptions extends TabOptions {
account: DatabaseAccount;
container: Explorer;
notebookUrl: string;
}
export interface DocumentsTabOptions extends TabOptions { export interface DocumentsTabOptions extends TabOptions {
partitionKey: DataModels.PartitionKey; partitionKey: DataModels.PartitionKey;
documentIds: ko.ObservableArray<DocumentId>; documentIds: ko.ObservableArray<DocumentId>;
@@ -491,157 +303,15 @@ export interface ScriptTabOption extends TabOptions {
partitionKey?: DataModels.PartitionKey; partitionKey?: DataModels.PartitionKey;
} }
// Tabs
export interface Tab {
documentClientUtility: DocumentClientUtilityBase;
node: TreeNode; // Can be null
collection: CollectionBase;
rid: string;
tabKind: CollectionTabKind;
tabId: string;
isActive: ko.Observable<boolean>;
isMouseOver: ko.Observable<boolean>;
tabPath: ko.Observable<string>;
tabTitle: ko.Observable<string>;
hashLocation: ko.Observable<string>;
closeTabButton: Button;
onCloseTabButtonClick(): void;
onTabClick(): Q.Promise<any>;
onKeyPressActivate(source: any, event: KeyboardEvent): void;
onKeyPressClose(source: any, event: KeyboardEvent): void;
onActivate(): Q.Promise<any>;
refresh(): void;
closeButtonTabIndex: ko.Computed<number>;
isExecutionError: ko.Observable<boolean>;
isExecuting: ko.Observable<boolean>;
}
export interface DocumentsTab extends Tab {
/* Documents Grid */
selectDocument(documentId: DocumentId): Q.Promise<any>;
selectedDocumentId: ko.Observable<DocumentId>;
selectedDocumentContent: Editable<any>;
onDocumentIdClick(documentId: DocumentId): Q.Promise<any>;
dataContentsGridScrollHeight: ko.Observable<string>;
accessibleDocumentList: AccessibleVerticalList;
documentContentsGridId: string;
partitionKey: DataModels.PartitionKey;
idHeader: string;
partitionKeyPropertyHeader: string;
partitionKeyProperty: string;
documentIds: ko.ObservableArray<DocumentId>;
/* Documents Filter */
filterContent: ko.Observable<string>;
appliedFilter: ko.Observable<string>;
lastFilterContents: ko.ObservableArray<string>;
isFilterExpanded: ko.Observable<boolean>;
applyFilterButton: Button;
onShowFilterClick(): Q.Promise<any>;
onHideFilterClick(): Q.Promise<any>;
onApplyFilterClick(): Q.Promise<any>;
/* Document Editor */
isEditorDirty: ko.Computed<boolean>;
editorState: ko.Observable<DocumentExplorerState>;
onValidDocumentEdit(content: any): Q.Promise<any>;
onInvalidDocumentEdit(content: any): Q.Promise<any>;
onNewDocumentClick(): Q.Promise<any>;
onSaveNewDocumentClick(): Q.Promise<any>;
onRevertNewDocumentClick(): Q.Promise<any>;
onSaveExisitingDocumentClick(): Q.Promise<any>;
onRevertExisitingDocumentClick(): Q.Promise<any>;
onDeleteExisitingDocumentClick(): Q.Promise<any>;
/* Errors */
displayedError: ko.Observable<string>;
initDocumentEditor(documentId: DocumentId, content: any): Q.Promise<any>;
loadNextPage(): Q.Promise<any>;
}
export interface WaitsForTemplate { export interface WaitsForTemplate {
isTemplateReady: ko.Observable<boolean>; isTemplateReady: ko.Observable<boolean>;
} }
export interface QueryTab extends Tab {
queryEditorId: string;
isQueryMetricsEnabled: ko.Computed<boolean>;
activityId: ko.Observable<string>;
/* Command Bar */
executeQueryButton: Button;
fetchNextPageButton: Button;
saveQueryButton: Button;
onExecuteQueryClick(): Q.Promise<any>;
onFetchNextPageClick(): Q.Promise<any>;
/*Query Editor*/
initialEditorContent: ko.Observable<string>;
sqlQueryEditorContent: ko.Observable<string>;
sqlStatementToExecute: ko.Observable<string>;
/* Results */
allResultsMetadata: ko.ObservableArray<QueryResultsMetadata>;
/* Errors */
errors: ko.ObservableArray<QueryError>;
/* Status */
statusMessge: ko.Observable<string>;
statusIcon: ko.Observable<string>;
}
export interface ScriptTab extends Tab {
id: Editable<string>;
editorId: string;
saveButton: Button;
updateButton: Button;
discardButton: Button;
deleteButton: Button;
editorState: ko.Observable<ScriptEditorState>;
editorContent: ko.Observable<string>;
editor: ko.Observable<monaco.editor.IStandaloneCodeEditor>;
errors: ko.ObservableArray<QueryError>;
statusMessge: ko.Observable<string>;
statusIcon: ko.Observable<string>;
formFields: ko.ObservableArray<Editable<any>>;
formIsValid: ko.Computed<boolean>;
formIsDirty: ko.Computed<boolean>;
isNew: ko.Observable<boolean>;
resource: ko.Observable<DataModels.Resource>;
setBaselines(): void;
}
export interface StoredProcedureTab extends ScriptTab {
onExecuteSprocsResult(result: any, logsData: any): void;
onExecuteSprocsError(error: string): void;
}
export interface UserDefinedFunctionTab extends ScriptTab {}
export interface TriggerTab extends ScriptTab {
triggerType: Editable<string>;
triggerOperation: Editable<string>;
}
export interface GraphTab extends Tab {}
export interface EditorPosition { export interface EditorPosition {
line: number; line: number;
column: number; column: number;
} }
export interface MongoShellTab extends Tab {}
export enum DocumentExplorerState { export enum DocumentExplorerState {
noDocumentSelected, noDocumentSelected,
newDocumentValid, newDocumentValid,
@@ -759,40 +429,8 @@ export interface AuthorizationTokenHeaderMetadata {
token: string; token: string;
} }
export interface TelemetryActions {
sendEvent(name: string, telemetryProperties?: { [propertyName: string]: string }): Q.Promise<any>;
sendError(errorInfo: DataModels.ITelemetryError): Q.Promise<any>;
sendMetric(
name: string,
metricNumber: number,
telemetryProperties?: { [propertyName: string]: string }
): Q.Promise<any>;
}
export interface ConfigurationOverrides {
EnableBsonSchema: string;
}
export interface CosmosDbApi {
isSystemDatabasePredicate: (database: Database) => boolean;
}
export interface DropdownOption<T> { export interface DropdownOption<T> {
text: string; text: string;
value: T; value: T;
disable?: boolean; disable?: boolean;
} }
export interface INotebookContainerClient {
resetWorkspace: () => Promise<void>;
}
export interface INotebookContentClient {
updateItemChildren: (item: NotebookContentItem) => Promise<void>;
createNewNotebookFile: (parent: NotebookContentItem) => Promise<NotebookContentItem>;
deleteContentItem: (item: NotebookContentItem) => Promise<void>;
uploadFileAsync: (name: string, content: string, parent: NotebookContentItem) => Promise<NotebookContentItem>;
renameNotebook: (item: NotebookContentItem, targetName: string) => Promise<NotebookContentItem>;
createDirectory: (parent: NotebookContentItem, newDirectoryName: string) => Promise<NotebookContentItem>;
readFileContent: (filePath: string) => Promise<string>;
}

View File

@@ -12,7 +12,7 @@ import {
PortalTheme PortalTheme
} from "./HeatmapDatatypes"; } from "./HeatmapDatatypes";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import { MessageHandler } from "../../Common/MessageHandler"; import { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts"; import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { StyleConstants } from "../../Common/Constants"; import { StyleConstants } from "../../Common/Constants";
import "./Heatmap.less"; import "./Heatmap.less";
@@ -209,7 +209,7 @@ export class Heatmap {
for (let i = 0; i < this._chartData.dataPoints.length; i++) { for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]); output.push(this._chartData.dataPoints[i][xAxisIndex]);
} }
MessageHandler.sendCachedDataMessage(MessageTypes.LogInfo, output); sendCachedDataMessage(MessageTypes.LogInfo, output);
}); });
} }
} }
@@ -266,4 +266,4 @@ export function handleMessage(event: MessageEvent) {
} }
window.addEventListener("message", handleMessage, false); window.addEventListener("message", handleMessage, false);
MessageHandler.sendMessage("ready"); sendMessage("ready");

View File

@@ -0,0 +1,8 @@
export enum DefaultAccountExperienceType {
DocumentDB = "DocumentDB",
Graph = "Graph",
MongoDB = "MongoDB",
Table = "Table",
Cassandra = "Cassandra",
ApiForMongoDB = "Azure Cosmos DB for MongoDB API"
}

View File

@@ -13,9 +13,7 @@ import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponen
import { TabsManagerKOComponent } from "./Tabs/TabsManager"; import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent"; import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
ko.components.register("toolbar", new ToolbarComponent());
ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent); ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("error-display", new ErrorDisplayComponent()); ko.components.register("error-display", new ErrorDisplayComponent());

View File

@@ -49,6 +49,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" }, { key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
{
key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell",
value: "true"
},
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" }, { key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
{ {
key: "feature.enablefixedcollectionwithsharedthroughput", key: "feature.enablefixedcollectionwithsharedthroughput",

View File

@@ -163,8 +163,14 @@ exports[`Feature panel renders all flags 1`] = `
/> />
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.canexceedmaximumvalue" key="feature.enablecodeofconduct"
label="Can exceed max value" label="Enable Code Of Conduct Acknowledgement"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enableLinkInjection"
label="Enable Injecting Notebook Viewer Link into the first cell"
onChange={[Function]} onChange={[Function]}
/> />
</Stack> </Stack>
@@ -172,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow" className="checkboxRow"
horizontalAlign="space-between" horizontalAlign="space-between"
> >
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.enablefixedcollectionwithsharedthroughput" key="feature.enablefixedcollectionwithsharedthroughput"

View File

@@ -5,7 +5,7 @@
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as Logger from "../../../Common/Logger"; import * as Logger from "../../../Common/Logger";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils"; import { StringUtils } from "../../../Utils/StringUtils";

View File

@@ -17,7 +17,8 @@ describe("GalleryCardComponent", () => {
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}, },
isFavorite: false, isFavorite: false,
showDownload: true, showDownload: true,

View File

@@ -36,6 +36,8 @@ export interface GalleryCardComponentProps {
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> { export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
public static readonly CARD_WIDTH = 256; public static readonly CARD_WIDTH = 256;
private static readonly cardImageHeight = 144; private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio =
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
private static readonly cardDescriptionMaxChars = 88; private static readonly cardDescriptionMaxChars = 88;
private static readonly cardItemGapBig = 10; private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8; private static readonly cardItemGapSmall = 8;

View File

@@ -0,0 +1,43 @@
import { shallow } from "enzyme";
import * as sinon from "sinon";
import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
import { IJunoResponse, JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes } from "../../../Common/Constants";
describe("CodeOfConductComponent", () => {
let sandbox: sinon.SinonSandbox;
let codeOfConductProps: CodeOfConductComponentProps;
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(JunoClient.prototype, "acceptCodeOfConduct").returns({
status: HttpStatusCodes.OK,
data: true
} as IJunoResponse<boolean>);
const junoClient = new JunoClient(undefined);
codeOfConductProps = {
junoClient: junoClient,
onAcceptCodeOfConduct: jest.fn()
};
});
afterEach(() => {
sandbox.restore();
});
it("renders", () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
expect(wrapper).toMatchSnapshot();
});
it("onAcceptedCodeOfConductCalled", async () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
wrapper
.find(".genericPaneSubmitBtn")
.first()
.simulate("click");
await Promise.resolve();
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
});
});

View File

@@ -0,0 +1,112 @@
import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import * as Logger from "../../../Common/Logger";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void;
}
interface CodeOfConductComponentState {
readCodeOfConduct: boolean;
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
private link2: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
this.state = {
readCodeOfConduct: false
};
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
this.descriptionPara2 =
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
}
private async acceptCodeOfConduct(): Promise<void> {
try {
const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
const message = `Failed to accept code of conduct: ${error}`;
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
logConsoleError(message);
}
}
private onChangeCheckbox = (): void => {
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
};
public render(): JSX.Element {
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{this.descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<Text>
{this.descriptionPara3}
<Link href={this.link1.url} target="_blank">
{this.link1.label}
</Link>
{" and "}
<Link href={this.link2.url} target="_blank">
{this.link2.label}
</Link>
</Text>
</Stack.Item>
<Stack.Item>
<Checkbox
styles={{
label: {
margin: 0,
padding: "2 0 2 0"
},
text: {
fontSize: 12
}
}}
label="I have read and accepted the code of conduct and privacy statement"
onChange={this.onChangeCheckbox}
/>
</Stack.Item>
<Stack.Item>
<PrimaryButton
ariaLabel="Continue"
title="Continue"
onClick={async () => await this.acceptCodeOfConduct()}
tabIndex={0}
className="genericPaneSubmitBtn"
text="Continue"
disabled={!this.state.readCodeOfConduct}
/>
</Stack.Item>
</Stack>
);
}
}

View File

@@ -15,15 +15,17 @@ import {
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import * as Logger from "../../../Common/Logger"; import * as Logger from "../../../Common/Logger";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent"; import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less"; import "./GalleryViewerComponent.less";
import { HttpStatusCodes } from "../../../Common/Constants"; import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent";
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -60,6 +62,7 @@ interface GalleryViewerComponentState {
sortBy: SortBy; sortBy: SortBy;
searchText: string; searchText: string;
dialogProps: DialogProps; dialogProps: DialogProps;
isCodeOfConductAccepted: boolean;
} }
interface GalleryTabInfo { interface GalleryTabInfo {
@@ -86,6 +89,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private publicNotebooks: IGalleryItem[]; private publicNotebooks: IGalleryItem[];
private favoriteNotebooks: IGalleryItem[]; private favoriteNotebooks: IGalleryItem[];
private publishedNotebooks: IGalleryItem[]; private publishedNotebooks: IGalleryItem[];
private isCodeOfConductAccepted: boolean;
private columnCount: number; private columnCount: number;
private rowCount: number; private rowCount: number;
@@ -100,7 +104,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
selectedTab: props.selectedTab, selectedTab: props.selectedTab,
sortBy: props.sortBy, sortBy: props.sortBy,
searchText: props.searchText, searchText: props.searchText,
dialogProps: undefined dialogProps: undefined,
isCodeOfConductAccepted: undefined
}; };
this.sortingOptions = [ this.sortingOptions = [
@@ -134,10 +139,21 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)]; const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) { if (this.props.container?.isGalleryPublishEnabled()) {
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks)); tabs.push(
this.createPublicGalleryTab(
GalleryTab.PublicGallery,
this.state.publicNotebooks,
this.state.isCodeOfConductAccepted
)
);
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks)); tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior.
if (this.state.isCodeOfConductAccepted !== false) {
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks)); tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
} }
}
const pivotProps: IPivotProps = { const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange, onLinkClick: this.onPivotChange,
@@ -167,6 +183,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
); );
} }
private createPublicGalleryTab(
tab: GalleryTab,
data: IGalleryItem[],
acceptedCodeOfConduct: boolean
): GalleryTabInfo {
return {
tab,
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct)
};
}
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo { private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return { return {
tab, tab,
@@ -174,6 +201,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
} }
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return acceptedCodeOfConduct === false ? (
<CodeOfConductComponent
junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
) : (
this.createTabContent(data)
);
}
private createTabContent(data: IGalleryItem[]): JSX.Element { private createTabContent(data: IGalleryItem[]): JSX.Element {
return ( return (
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
@@ -187,8 +227,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}> <Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} /> <Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item> </Stack.Item>
{this.props.container?.isGalleryPublishEnabled() && (
<Stack.Item>
<InfoComponent />
</Stack.Item>
)}
</Stack> </Stack>
{data && this.createCardsTabContent(data)} {data && this.createCardsTabContent(data)}
</Stack> </Stack>
); );
@@ -254,12 +298,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> { private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) { if (!offline) {
try { try {
const response = await this.props.junoClient.getPublicNotebooks(); let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
if (this.props.container.isCodeOfConductEnabled()) {
response = await this.props.junoClient.fetchPublicNotebooks();
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
this.publicNotebooks = response.data?.notebooksData;
} else {
response = await this.props.junoClient.getPublicNotebooks();
this.publicNotebooks = response.data;
}
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading public notebooks`); throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
} }
this.publicNotebooks = response.data;
} catch (error) { } catch (error) {
const message = `Failed to load public notebooks: ${error}`; const message = `Failed to load public notebooks: ${error}`;
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks"); Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
@@ -268,7 +319,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
} }
this.setState({ this.setState({
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))] publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
isCodeOfConductAccepted: this.isCodeOfConductAccepted
}); });
} }
@@ -333,12 +385,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean { private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
const toSearch = searchText.trim().toUpperCase(); const toSearch = searchText.trim().toUpperCase();
const searchData: string[] = [ const searchData: string[] = [item.author.toUpperCase(), item.description.toUpperCase(), item.name.toUpperCase()];
item.author.toUpperCase(),
item.description.toUpperCase(), if (item.tags) {
item.name.toUpperCase(), searchData.push(...item.tags.map(tag => tag.toUpperCase()));
...item.tags?.map(tag => tag.toUpperCase()) }
];
for (const data of searchData) { for (const data of searchData) {
if (data?.indexOf(toSearch) !== -1) { if (data?.indexOf(toSearch) !== -1) {

View File

@@ -0,0 +1,26 @@
@import "../../../../../less/Common/Constants.less";
.infoPanel, .infoPanelMain {
display: flex;
align-items: center;
}
.infoPanel {
padding-left: 5px;
padding-right: 5px;
}
.infoLabel, .infoLabelMain {
padding-left: 5px
}
.infoLabel {
font-weight: 400
}
.infoIconMain {
color: @AccentMedium
}
.infoIconMain:hover {
color: @BaseMedium
}

View File

@@ -0,0 +1,10 @@
import { shallow } from "enzyme";
import React from "react";
import { InfoComponent } from "./InfoComponent";
describe("InfoComponent", () => {
it("renders", () => {
const wrapper = shallow(<InfoComponent />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,42 @@
import * as React from "react";
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fabric-react";
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
import "./InfoComponent.less";
export class InfoComponent extends React.Component {
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
return (
<Link href={url} target="_blank">
<div className="infoPanel">
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabel">{labelText}</Label>
</div>
</Link>
);
};
private onHover = (): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5, padding: 5 }}>
<Stack.Item>{this.getInfoPanel("Script", "Code of Conduct", CodeOfConductEndpoints.codeOfConduct)}</Stack.Item>
<Stack.Item>
{this.getInfoPanel("RedEye", "Privacy Statement", CodeOfConductEndpoints.privacyStatement)}
</Stack.Item>
<Stack.Item>
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
</Stack.Item>
</Stack>
);
};
public render(): JSX.Element {
return (
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
<div className="infoPanelMain">
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabelMain">Help</Label>
</div>
</HoverCard>
);
}
}

View File

@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InfoComponent renders 1`] = `
<StyledHoverCardBase
instantOpenOnClick={true}
plainCardProps={
Object {
"onRenderPlainCard": [Function],
}
}
type="PlainCard"
>
<div
className="infoPanelMain"
>
<Memo(StyledIconBase)
className="infoIconMain"
iconName="Help"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
<StyledLabelBase
className="infoLabelMain"
>
Help
</StyledLabelBase>
</div>
</StyledHoverCardBase>
`;

View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeOfConductComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StackItem>
<Text
style={
Object {
"fontSize": "20px",
"fontWeight": 500,
}
}
>
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
</Text>
</StackItem>
<StackItem>
<Text>
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
</Text>
</StackItem>
<StackItem>
<Text>
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
<StyledLinkBase
href="https://aka.ms/cosmos-code-of-conduct"
target="_blank"
>
code of conduct
</StyledLinkBase>
and
<StyledLinkBase
href="https://aka.ms/ms-privacy-policy"
target="_blank"
>
privacy statement
</StyledLinkBase>
</Text>
</StackItem>
<StackItem>
<StyledCheckboxBase
label="I have read and accepted the code of conduct and privacy statement"
onChange={[Function]}
styles={
Object {
"label": Object {
"margin": 0,
"padding": "2 0 2 0",
},
"text": Object {
"fontSize": 12,
},
}
}
/>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
ariaLabel="Continue"
className="genericPaneSubmitBtn"
disabled={true}
onClick={[Function]}
tabIndex={0}
text="Continue"
title="Continue"
/>
</StackItem>
</Stack>
`;

View File

@@ -17,7 +17,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}, },
isFavorite: false, isFavorite: false,
downloadButtonText: "Download", downloadButtonText: "Download",
@@ -45,7 +46,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}, },
isFavorite: true, isFavorite: true,
downloadButtonText: "Download", downloadButtonText: "Download",

View File

@@ -10,7 +10,7 @@ import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2"; import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper"; import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
@@ -19,6 +19,8 @@ import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComp
import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
export interface NotebookViewerComponentProps { export interface NotebookViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -85,16 +87,17 @@ export class NotebookViewerComponent extends React.Component<
} }
const notebook: Notebook = await response.json(); const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook); this.notebookComponentBootstrapper.setContent("json", notebook);
this.setState({ content: notebook, showProgressBar: false }); this.setState({ content: notebook, showProgressBar: false });
if (this.props.galleryItem) { if (this.props.galleryItem && !SessionStorageUtility.getEntry(this.props.galleryItem.id)) {
const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id); const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id);
if (!response.data) { if (!response.data) {
throw new Error(`Received HTTP ${response.status} while increasing notebook views`); throw new Error(`Received HTTP ${response.status} while increasing notebook views`);
} }
this.setState({ galleryItem: response.data }); this.setState({ galleryItem: response.data });
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
} }
} catch (error) { } catch (error) {
this.setState({ showProgressBar: false }); this.setState({ showProgressBar: false });
@@ -104,10 +107,21 @@ export class NotebookViewerComponent extends React.Component<
} }
} }
private removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;
}
const notebookV4 = notebook as NotebookV4;
if (notebookV4 && notebookV4.cells[0].source[0].search(newCellId)) {
delete notebookV4.cells[0];
notebook = notebookV4;
}
};
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="notebookViewerContainer"> <div className="notebookViewerContainer">
{this.props.backNavigationText ? ( {this.props.backNavigationText !== undefined ? (
<Link onClick={this.props.onBackClick}> <Link onClick={this.props.onBackClick}>
<Icon iconName="Back" /> {this.props.backNavigationText} <Icon iconName="Back" /> {this.props.backNavigationText}
</Link> </Link>

View File

@@ -27,9 +27,10 @@ import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/l
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png"; import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import { QueriesClient } from "../../../Common/QueriesClient";
export interface QueriesGridComponentProps { export interface QueriesGridComponentProps {
queriesClient: ViewModels.QueriesClient; queriesClient: QueriesClient;
onQuerySelect: (query: DataModels.Query) => void; onQuerySelect: (query: DataModels.Query) => void;
containerVisible: boolean; containerVisible: boolean;
saveQueryEnabled: boolean; saveQueryEnabled: boolean;

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarAction extends IToolbarDisplayable {
type: "action";
action: () => void;
}
export default IToolbarAction;

View File

@@ -1,18 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
interface IToolbarDisplayable {
id: string;
title: ko.Subscribable<string>;
displayName: ko.Subscribable<string>;
enabled: ko.Subscribable<boolean>;
visible: ko.Observable<boolean>;
focused: ko.Observable<boolean>;
icon: string;
mouseDown: (data: any, event: MouseEvent) => any;
keyUp: (data: any, event: KeyboardEvent) => any;
keyDown: (data: any, event: KeyboardEvent) => any;
}
export default IToolbarDisplayable;

View File

@@ -1,56 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarDropDown extends IToolbarDisplayable {
type: "dropdown";
subgroup: IActionConfigItem[];
expanded: ko.Observable<boolean>;
open: () => void;
}
export interface IDropdown {
type: "dropdown";
title: string;
displayName: string;
id: string;
enabled: ko.Observable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
subgroup?: IActionConfigItem[];
}
export interface ISeperator {
type: "separator";
visible?: ko.Observable<boolean>;
}
export interface IToggle {
type: "toggle";
title: string;
displayName: string;
checkedTitle: string;
checkedDisplayName: string;
id: string;
checked: ko.Observable<boolean>;
enabled: ko.Observable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
}
export interface IAction {
type: "action";
title: string;
displayName: string;
id: string;
action: () => any;
enabled: ko.Subscribable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
}
export type IActionConfigItem = ISeperator | IAction | IToggle | IDropdown;
export default IToolbarDropDown;

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarAction from "./IToolbarAction";
import IToolbarToggle from "./IToolbarToggle";
import IToolbarSeperator from "./IToolbarSeperator";
import IToolbarDropDown from "./IToolbarDropDown";
type IToolbarItem = IToolbarAction | IToolbarToggle | IToolbarSeperator | IToolbarDropDown;
export default IToolbarItem;

View File

@@ -1,10 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
interface IToolbarSeperator {
type: "separator";
visible: ko.Observable<boolean>;
}
export default IToolbarSeperator;

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarToggle extends IToolbarDisplayable {
type: "toggle";
checked: ko.Observable<boolean>;
toggle: () => void;
}
export default IToolbarToggle;

View File

@@ -1,58 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
var keyCodes = {
RightClick: 3,
Enter: 13,
Esc: 27,
Tab: 9,
LeftArrow: 37,
UpArrow: 38,
RightArrow: 39,
DownArrow: 40,
Delete: 46,
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
Period: 190,
DecimalPoint: 110,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
Dash: 189
};
export default keyCodes;

View File

@@ -1,145 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import { IDropdown } from "./IToolbarDropDown";
import { IActionConfigItem } from "./IToolbarDropDown";
import IToolbarItem from "./IToolbarItem";
import * as ko from "knockout";
import ToolbarDropDown from "./ToolbarDropDown";
import ToolbarAction from "./ToolbarAction";
import ToolbarToggle from "./ToolbarToggle";
import template from "./toolbar.html";
export default class Toolbar {
private _toolbarWidth = ko.observable<number>();
private _actionConfigs: IActionConfigItem[];
private _afterExecute: (id: string) => void;
private _hasFocus: boolean = false;
private _focusedSubscription: ko.Subscription;
constructor(actionItems: IActionConfigItem[], afterExecute?: (id: string) => void) {
this._actionConfigs = actionItems;
this._afterExecute = afterExecute;
this.toolbarItems.subscribe(this._focusFirstEnabledItem);
$(window).resize(() => {
this._toolbarWidth($(".toolbar").width());
});
setTimeout(() => {
this._toolbarWidth($(".toolbar").width());
}, 500);
}
public toolbarItems: ko.PureComputed<IToolbarItem[]> = ko.pureComputed(() => {
var remainingToolbarSpace = this._toolbarWidth();
var toolbarItems: IToolbarItem[] = [];
var moreItem: IDropdown = {
type: "dropdown",
title: "More",
displayName: "More",
id: "more-actions-toggle",
enabled: ko.observable(true),
visible: ko.observable(true),
icon: "images/ASX_More.svg",
subgroup: []
};
var showHasMoreItem = false;
var addSeparator = false;
this._actionConfigs.forEach(actionConfig => {
if (actionConfig.type === "separator") {
addSeparator = true;
} else if (remainingToolbarSpace / 60 > 2) {
if (addSeparator) {
addSeparator = false;
toolbarItems.push(Toolbar._createToolbarItemFromConfig({ type: "separator" }));
remainingToolbarSpace -= 10;
}
toolbarItems.push(Toolbar._createToolbarItemFromConfig(actionConfig));
remainingToolbarSpace -= 60;
} else {
showHasMoreItem = true;
if (addSeparator) {
addSeparator = false;
moreItem.subgroup.push({
type: "separator"
});
}
if (!!actionConfig) {
moreItem.subgroup.push(actionConfig);
}
}
});
if (showHasMoreItem) {
toolbarItems.push(
Toolbar._createToolbarItemFromConfig({ type: "separator" }),
Toolbar._createToolbarItemFromConfig(moreItem)
);
}
return toolbarItems;
});
public focus() {
this._hasFocus = true;
this._focusFirstEnabledItem(this.toolbarItems());
}
private _focusFirstEnabledItem = (items: IToolbarItem[]) => {
if (!!this._focusedSubscription) {
// no memory leaks! :D
this._focusedSubscription.dispose();
}
if (this._hasFocus) {
for (var i = 0; i < items.length; i++) {
if (items[i].type !== "separator" && (<any>items[i]).enabled()) {
(<any>items[i]).focused(true);
this._focusedSubscription = (<any>items[i]).focused.subscribe((newValue: any) => {
if (!newValue) {
this._hasFocus = false;
this._focusedSubscription.dispose();
}
});
break;
}
}
}
};
private static _createToolbarItemFromConfig(
configItem: IActionConfigItem,
afterExecute?: (id: string) => void
): IToolbarItem {
switch (configItem.type) {
case "dropdown":
return new ToolbarDropDown(configItem, afterExecute);
case "action":
return new ToolbarAction(configItem, afterExecute);
case "toggle":
return new ToolbarToggle(configItem, afterExecute);
case "separator":
return {
type: "separator",
visible: ko.observable(true)
};
}
}
}
/**
* Helper class for ko component registration
*/
export class ToolbarComponent {
constructor() {
return {
viewModel: Toolbar,
template
};
}
}

View File

@@ -1,86 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IAction } from "./IToolbarDropDown";
import IToolbarAction from "./IToolbarAction";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
export default class ToolbarAction implements IToolbarAction {
public type: "action" = "action";
public id: string;
public icon: string;
public title: ko.Observable<string>;
public displayName: ko.Observable<string>;
public enabled: ko.Subscribable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public action: () => void;
private _afterExecute: (id: string) => void;
constructor(actionItem: IAction, afterExecute?: (id: string) => void) {
this.action = actionItem.action;
this.title = ko.observable(actionItem.title);
this.displayName = ko.observable(actionItem.displayName);
this.id = actionItem.id;
this.enabled = actionItem.enabled;
this.visible = actionItem.visible ? actionItem.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = actionItem.icon;
this._afterExecute = afterExecute;
}
private _executeAction = () => {
this.action();
if (!!this._afterExecute) {
this._afterExecute(this.id);
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this._executeAction();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this._executeAction();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,167 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IDropdown } from "./IToolbarDropDown";
import { IActionConfigItem } from "./IToolbarDropDown";
import IToolbarDropDown from "./IToolbarDropDown";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
interface IMenuItem {
id?: string;
type: "normal" | "separator" | "submenu";
label?: string;
enabled?: boolean;
visible?: boolean;
submenu?: IMenuItem[];
}
export default class ToolbarDropDown implements IToolbarDropDown {
public type: "dropdown" = "dropdown";
public title: ko.Observable<string>;
public displayName: ko.Observable<string>;
public id: string;
public enabled: ko.Observable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public icon: string;
public subgroup: IActionConfigItem[] = [];
public expanded: ko.Observable<boolean> = ko.observable(false);
private _afterExecute: (id: string) => void;
constructor(dropdown: IDropdown, afterExecute?: (id: string) => void) {
this.subgroup = dropdown.subgroup;
this.title = ko.observable(dropdown.title);
this.displayName = ko.observable(dropdown.displayName);
this.id = dropdown.id;
this.enabled = dropdown.enabled;
this.visible = dropdown.visible ? dropdown.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = dropdown.icon;
this._afterExecute = afterExecute;
}
private static _convertToMenuItem = (
actionConfigs: IActionConfigItem[],
actionMap: { [id: string]: () => void } = {}
): { menuItems: IMenuItem[]; actionMap: { [id: string]: () => void } } => {
var returnValue = {
menuItems: actionConfigs.map<IMenuItem>((actionConfig: IActionConfigItem, index, array) => {
var menuItem: IMenuItem;
switch (actionConfig.type) {
case "action":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "normal",
label: actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true
};
actionMap[actionConfig.id] = actionConfig.action;
break;
case "dropdown":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "submenu",
label: actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true,
submenu: ToolbarDropDown._convertToMenuItem(actionConfig.subgroup, actionMap).menuItems
};
break;
case "toggle":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "normal",
label: actionConfig.checked() ? actionConfig.checkedDisplayName : actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true
};
actionMap[actionConfig.id] = () => {
actionConfig.checked(!actionConfig.checked());
};
break;
case "separator":
menuItem = <IMenuItem>{
type: "separator",
visible: true
};
break;
}
return menuItem;
}),
actionMap: actionMap
};
return returnValue;
};
public open = () => {
if (!!(<any>window).host) {
var convertedMenuItem = ToolbarDropDown._convertToMenuItem(this.subgroup);
(<any>window).host
.executeProviderOperation("MenuManager.showMenu", {
iFrameStack: [`#${window.frameElement.id}`],
anchor: `#${this.id}`,
menuItems: convertedMenuItem.menuItems
})
.then((id?: string) => {
if (!!id && !!convertedMenuItem.actionMap[id]) {
convertedMenuItem.actionMap[id]();
}
});
if (!!this._afterExecute) {
this._afterExecute(this.id);
}
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this.open();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this.open();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,109 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IToggle } from "./IToolbarDropDown";
import IToolbarToggle from "./IToolbarToggle";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
export default class ToolbarToggle implements IToolbarToggle {
public type: "toggle" = "toggle";
public checked: ko.Observable<boolean>;
public id: string;
public enabled: ko.Observable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public icon: string;
private _title: string;
private _displayName: string;
private _checkedTitle: string;
private _checkedDisplayName: string;
private _afterExecute: (id: string) => void;
constructor(toggleItem: IToggle, afterExecute?: (id: string) => void) {
this._title = toggleItem.title;
this._displayName = toggleItem.displayName;
this.id = toggleItem.id;
this.enabled = toggleItem.enabled;
this.visible = toggleItem.visible ? toggleItem.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = toggleItem.icon;
this.checked = toggleItem.checked;
this._checkedTitle = toggleItem.checkedTitle;
this._checkedDisplayName = toggleItem.checkedDisplayName;
this._afterExecute = afterExecute;
}
public title = ko.pureComputed(() => {
if (this.checked()) {
return this._checkedTitle;
} else {
return this._title;
}
});
public displayName = ko.pureComputed(() => {
if (this.checked()) {
return this._checkedDisplayName;
} else {
return this._displayName;
}
});
public toggle = () => {
this.checked(!this.checked());
if (this.checked() && !!this._afterExecute) {
this._afterExecute(this.id);
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this.toggle();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this.toggle();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,166 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import KeyCodes from "./KeyCodes";
export default class Utilities {
/**
* Executes an action on a keyboard event.
* Modifiers: ctrlKey - control/command key, shiftKey - shift key, altKey - alt/option key;
* pass on 'null' to ignore the modifier (default).
*/
public static onKey(
event: any,
eventKeyCode: number,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
var source: any = event.target || event.srcElement,
keyCode: number = event.keyCode,
$sourceElement = $(source),
handled: boolean = false;
if (
$sourceElement.length &&
keyCode === eventKeyCode &&
$.isFunction(action) &&
(metaKey === null || metaKey === event.metaKey) &&
(shiftKey === null || shiftKey === event.shiftKey) &&
(altKey === null || altKey === event.altKey)
) {
action($sourceElement);
handled = true;
}
return handled;
}
/**
* Executes an action on the first matched keyboard event.
*/
public static onKeys(
event: any,
eventKeyCodes: number[],
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
var handled: boolean = false,
keyCount: number,
i: number;
if ($.isArray(eventKeyCodes)) {
keyCount = eventKeyCodes.length;
for (i = 0; i < keyCount; ++i) {
handled = Utilities.onKey(event, eventKeyCodes[i], action, metaKey, shiftKey, altKey);
if (handled) {
break;
}
}
}
return handled;
}
/**
* Executes an action on an 'enter' keyboard event.
*/
public static onEnter(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Enter, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a 'tab' keyboard event.
*/
public static onTab(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Tab, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on an 'Esc' keyboard event.
*/
public static onEsc(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Esc, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on an 'UpArrow' keyboard event.
*/
public static onUpArrow(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.UpArrow, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a 'DownArrow' keyboard event.
*/
public static onDownArrow(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.DownArrow, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a mouse event.
*/
public static onButton(event: any, eventButtonCode: number, action: ($sourceElement: JQuery) => void): boolean {
var source: any = event.currentTarget;
var buttonCode: number = event.button;
var $sourceElement = $(source);
var handled: boolean = false;
if ($sourceElement.length && buttonCode === eventButtonCode && $.isFunction(action)) {
action($sourceElement);
handled = true;
}
return handled;
}
/**
* Executes an action on a 'left' mouse event.
*/
public static onLeftButton(event: any, action: ($sourceElement: JQuery) => void): boolean {
return Utilities.onButton(event, buttonCodes.Left, action);
}
}
var buttonCodes = {
None: -1,
Left: 0,
Middle: 1,
Right: 2
};

View File

@@ -1,44 +0,0 @@
<div class="toolbar">
<!-- ko template: { name: 'toolbarItemTemplate', foreach: toolbarItems } -->
<!-- /ko -->
</div>
<script type="text/html" id="toolbarItemTemplate">
<!-- ko if: type === "action" -->
<div class="toolbar-group" data-bind="visible: visible">
<button class="toolbar-group-button" data-bind="hasFocus: focused, attr: {id: id, title: title, 'aria-label': displayName}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
<!-- /ko -->
<!-- ko if: type === "toggle" -->
<div class="toolbar-group" data-bind="visible: visible">
<button class="toolbar-group-button toggle-button" data-bind="hasFocus: focused, attr: {id: id, title: title}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon" data-bind="css: { 'toggle-checked': checked }">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
<!-- /ko -->
<!-- ko if: type === "dropdown" -->
<div class="toolbar-group" data-bind="visible: visible">
<div class="dropdown" data-bind="attr: {id: (id + '-dropdown')}">
<button role="menu" class="toolbar-group-button" data-bind="hasFocus: focused, attr: {id: id, title: title, 'aria-label': displayName}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
</div>
<!-- /ko -->
<!-- ko if: type === "separator" -->
<div class="toolbar-group vertical-separator" data-bind="visible: visible"></div>
<!-- /ko -->
</script>

View File

@@ -1,12 +1,13 @@
jest.mock("../../Common/DocumentClientUtilityBase");
import * as ko from "knockout"; import * as ko from "knockout";
import * as sinon from "sinon"; import * as sinon from "sinon";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import Q from "q"; import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { CosmosClient } from "../../Common/CosmosClient"; import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext";
describe("ContainerSampleGenerator", () => { describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => { const createExplorerStub = (database: ViewModels.Database): Explorer => {
@@ -62,27 +63,33 @@ describe("ContainerSampleGenerator", () => {
const explorerStub = createExplorerStub(database); const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true); explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true);
const fakeDocumentClientUtility = sinon.createStubInstance(DocumentClientUtilityBase);
fakeDocumentClientUtility.getOrCreateDatabaseAndCollection.returns(Q.resolve(collection));
fakeDocumentClientUtility.createDocument.returns(Q.resolve());
explorerStub.documentClientUtility = fakeDocumentClientUtility;
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
await generator.createSampleContainerAsync(); await generator.createSampleContainerAsync();
expect(fakeDocumentClientUtility.createDocument.called).toBe(true); expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
}); });
it("should send gremlin queries for Graph API account", async () => { it("should send gremlin queries for Graph API account", async () => {
sinon.stub(GremlinClient.prototype, "initialize").callsFake(() => {}); sinon.stub(GremlinClient.prototype, "initialize").callsFake(() => {});
const executeStub = sinon.stub(GremlinClient.prototype, "execute").returns(Q.resolve()); const executeStub = sinon.stub(GremlinClient.prototype, "execute").returns(Q.resolve());
sinon.stub(CosmosClient, "databaseAccount").returns({ updateUserContext({
properties: {} databaseAccount: {
id: "foo",
name: "foo",
location: "foo",
type: "foo",
kind: "foo",
tags: [],
properties: {
documentEndpoint: "bar",
gremlinEndpoint: "foo",
tableEndpoint: "foo",
cassandraEndpoint: "foo"
}
}
}); });
const sampleCollectionId = "SampleCollection"; const sampleCollectionId = "SampleCollection";
@@ -109,18 +116,12 @@ describe("ContainerSampleGenerator", () => {
const explorerStub = createExplorerStub(database); const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true); explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true);
const fakeDocumentClientUtility = sinon.createStubInstance(DocumentClientUtilityBase);
fakeDocumentClientUtility.getOrCreateDatabaseAndCollection.returns(Q.resolve(collection));
fakeDocumentClientUtility.createDocument.returns(Q.resolve());
explorerStub.documentClientUtility = fakeDocumentClientUtility;
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
await generator.createSampleContainerAsync(); await generator.createSampleContainerAsync();
expect(fakeDocumentClientUtility.createDocument.called).toBe(false); expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
expect(executeStub.called).toBe(true); expect(executeStub.called).toBe(true);
}); });

View File

@@ -3,10 +3,11 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import GraphTab from ".././Tabs/GraphTab"; import GraphTab from ".././Tabs/GraphTab";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "../../Common/CosmosClient";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { createDocument, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest { interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
data: any[]; data: any[];
@@ -64,7 +65,7 @@ export class ContainerSampleGenerator {
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true; options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
} }
await this.container.documentClientUtility.getOrCreateDatabaseAndCollection(createRequest, options); await getOrCreateDatabaseAndCollection(createRequest, options);
await this.container.refreshAllDatabases(); await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId); const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) { if (!database) {
@@ -86,14 +87,14 @@ export class ContainerSampleGenerator {
if (!queries || queries.length < 1) { if (!queries || queries.length < 1) {
return; return;
} }
const account = CosmosClient.databaseAccount(); const account = userContext.databaseAccount;
const databaseId = collection.databaseId; const databaseId = collection.databaseId;
const gremlinClient = new GremlinClient(); const gremlinClient = new GremlinClient();
gremlinClient.initialize({ gremlinClient.initialize({
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`, endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
databaseId: databaseId, databaseId: databaseId,
collectionId: collection.id(), collectionId: collection.id(),
masterKey: CosmosClient.masterKey() || "", masterKey: userContext.masterKey || "",
maxResultSize: 100 maxResultSize: 100
}); });
@@ -103,7 +104,7 @@ export class ContainerSampleGenerator {
} else { } else {
// For SQL all queries are executed at the same time // For SQL all queries are executed at the same time
this.sampleDataFile.data.forEach(doc => { this.sampleDataFile.data.forEach(doc => {
const subPromise = this.container.documentClientUtility.createDocument(collection, doc); const subPromise = createDocument(collection, doc);
subPromise.catch(reason => NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, reason)); subPromise.catch(reason => NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, reason));
promises.push(subPromise); promises.push(subPromise);
}); });

View File

@@ -1,6 +1,6 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";

View File

@@ -15,13 +15,15 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase"; import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import EnvironmentUtility from "../Common/EnvironmentUtility"; import EnvironmentUtility from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import hasher from "hasher"; import hasher from "hasher";
import NewVertexPane from "./Panes/NewVertexPane"; import NewVertexPane from "./Panes/NewVertexPane";
import NotebookV2Tab from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import Q from "q"; import Q from "q";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
@@ -33,12 +35,10 @@ 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 { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import { CassandraApi } from "../Api/Apis";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { config } from "../Config"; import { configContext } from "../ConfigContext";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "../Common/CosmosClient";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter"; import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter";
@@ -52,12 +52,12 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem } from "../Juno/JunoClient";
import { LoadQueryPane } from "./Panes/LoadQueryPane"; import { LoadQueryPane } from "./Panes/LoadQueryPane";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { MessageHandler } from "../Common/MessageHandler"; import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler";
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 { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter"; import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { PlatformType } from "../PlatformType"; import { PlatformType } from "../PlatformType";
import { QueriesClient } from "../Common/QueriesClient"; import { QueriesClient } from "../Common/QueriesClient";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
@@ -82,6 +82,10 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
import UserDefinedFunction from "./Tree/UserDefinedFunction"; import UserDefinedFunction from "./Tree/UserDefinedFunction";
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 TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext";
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
@@ -92,6 +96,14 @@ enum ShareAccessToggleState {
Read Read
} }
interface ExplorerOptions {
isEmulator: boolean;
}
interface AdHocAccessData {
readWriteUrl: string;
readUrl: string;
}
export default class Explorer { export default class Explorer {
public flight: ko.Observable<string> = ko.observable<string>( public flight: ko.Observable<string> = ko.observable<string>(
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
@@ -107,7 +119,7 @@ export default class Explorer {
public hasWriteAccess: ko.Observable<boolean>; public hasWriteAccess: ko.Observable<boolean>;
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
public databaseAccount: ko.Observable<ViewModels.DatabaseAccount>; public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>; public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
public quotaId: ko.Observable<string>; public quotaId: ko.Observable<string>;
@@ -118,7 +130,7 @@ export default class Explorer {
public isPreferredApiGraph: ko.Computed<boolean>; public isPreferredApiGraph: ko.Computed<boolean>;
public isPreferredApiTable: ko.Computed<boolean>; public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isEmulator: boolean; public isServerlessEnabled: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>; public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>; public features: ko.Observable<any>;
@@ -126,9 +138,7 @@ export default class Explorer {
public extensionEndpoint: ko.Observable<string>; public extensionEndpoint: ko.Observable<string>;
public armEndpoint: ko.Observable<string>; public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>; public isTryCosmosDBSubscription: ko.Observable<boolean>;
public documentClientUtility: DocumentClientUtilityBase; public queriesClient: QueriesClient;
public notificationsClient: ViewModels.NotificationsClient;
public queriesClient: ViewModels.QueriesClient;
public tableDataClient: TableDataClient; public tableDataClient: TableDataClient;
public splitter: Splitter; public splitter: Splitter;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>(""); public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
@@ -139,7 +149,7 @@ export default class Explorer {
public isNotificationConsoleExpanded: ko.Observable<boolean>; public isNotificationConsoleExpanded: ko.Observable<boolean>;
// Panes // Panes
public contextPanes: ViewModels.ContextualPane[]; public contextPanes: ContextualPaneBase[];
// Resource Tree // Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>; public databases: ko.ObservableArray<ViewModels.Database>;
@@ -184,25 +194,29 @@ export default class Explorer {
public uploadItemsPane: UploadItemsPane; public uploadItemsPane: UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter; public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: LoadQueryPane; public loadQueryPane: LoadQueryPane;
public saveQueryPane: ViewModels.ContextualPane; public saveQueryPane: ContextualPaneBase;
public browseQueriesPane: BrowseQueriesPane; public browseQueriesPane: BrowseQueriesPane;
public uploadFilePane: UploadFilePane; public uploadFilePane: UploadFilePane;
public stringInputPane: StringInputPane; public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane; public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ViewModels.ContextualPane; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: ReactAdapter; public publishNotebookPaneAdapter: ReactAdapter;
public copyNotebookPaneAdapter: ReactAdapter;
// features // features
public isGalleryPublishEnabled: ko.Computed<boolean>; public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>; public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>; public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>; public isRightPanelV2Enabled: ko.Computed<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>; public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>; public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<ViewModels.AdHocAccessData>; public shareAccessData: ko.Observable<AdHocAccessData>;
public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise<void>; public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise<void>;
public renewTokenError: ko.Observable<string>; public renewTokenError: ko.Observable<string>;
public tokenForRenewal: ko.Observable<string>; public tokenForRenewal: ko.Observable<string>;
@@ -228,7 +242,7 @@ export default class Explorer {
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>; public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any; // This is dynamically loaded public notebookManager?: any; // This is dynamically loaded
private _panes: ViewModels.ContextualPane[] = []; private _panes: ContextualPaneBase[] = [];
private _importExplorerConfigComplete: boolean = false; private _importExplorerConfigComplete: boolean = false;
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = database => false; private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = database => false;
private _isInitializingNotebooks: boolean; private _isInitializingNotebooks: boolean;
@@ -251,7 +265,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(options: ViewModels.ExplorerOptions) { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree dataExplorerArea: Constants.Areas.ResourceTree
}); });
@@ -264,7 +278,7 @@ export default class Explorer {
this.deleteDatabaseText = ko.observable<string>("Delete Database"); this.deleteDatabaseText = ko.observable<string>("Delete Database");
this.refreshTreeTitle = ko.observable<string>("Refresh collections"); this.refreshTreeTitle = ko.observable<string>("Refresh collections");
this.databaseAccount = ko.observable<ViewModels.DatabaseAccount>(); this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>( this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
SharedConstants.CollectionCreation.DefaultSubscriptionType SharedConstants.CollectionCreation.DefaultSubscriptionType
); );
@@ -357,9 +371,6 @@ export default class Explorer {
} }
}); });
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>(); this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.documentClientUtility = options.documentClientUtility;
this.notificationsClient = options.notificationsClient;
this.isEmulator = options.isEmulator;
this.features = ko.observable(); this.features = ko.observable();
this.serverId = ko.observable<string>(); this.serverId = ko.observable<string>();
@@ -374,7 +385,7 @@ export default class Explorer {
this.resourceTokenPartitionKey = ko.observable<string>(); this.resourceTokenPartitionKey = ko.observable<string>();
this.isAuthWithResourceToken = ko.observable<boolean>(false); this.isAuthWithResourceToken = ko.observable<boolean>(false);
this.shareAccessData = ko.observable<ViewModels.AdHocAccessData>({ this.shareAccessData = ko.observable<AdHocAccessData>({
readWriteUrl: undefined, readWriteUrl: undefined,
readUrl: undefined readUrl: undefined
}); });
@@ -397,8 +408,15 @@ export default class Explorer {
this.isGalleryPublishEnabled = ko.computed<boolean>(() => this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish) this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
); );
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
);
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
this.isGitHubPaneEnabled = ko.observable<boolean>(false); this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false); this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
this.canExceedMaximumValue = ko.computed<boolean>(() => this.canExceedMaximumValue = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue) this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
@@ -459,8 +477,14 @@ export default class Explorer {
}); });
this.notificationConsoleData = ko.observableArray<ConsoleData>([]); this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
this.defaultExperience = ko.observable<string>(); this.defaultExperience = ko.observable<string>();
this.databaseAccount.subscribe((databaseAccount: ViewModels.DatabaseAccount) => { this.databaseAccount.subscribe(databaseAccount => {
this.defaultExperience(DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount)); const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
databaseAccount
);
this.defaultExperience(defaultExperience);
updateUserContext({
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience)
});
}); });
this.isPreferredApiDocumentDB = ko.computed(() => { this.isPreferredApiDocumentDB = ko.computed(() => {
@@ -509,6 +533,14 @@ export default class Explorer {
return false; return false;
}); });
this.isServerlessEnabled = ko.computed(
() =>
this.databaseAccount &&
this.databaseAccount()?.properties?.capabilities?.find(
item => item.name === Constants.CapabilityNames.EnableServerless
) !== undefined
);
this.isPreferredApiMongoDB = ko.computed(() => { this.isPreferredApiMongoDB = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) { if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) {
@@ -544,8 +576,9 @@ export default class Explorer {
defaultExperience && defaultExperience &&
defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase() defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase()
) { ) {
const api = new CassandraApi(); this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
this._isSystemDatabasePredicate = api.isSystemDatabasePredicate; return database.id() === "system";
};
} }
}); });
@@ -575,7 +608,6 @@ export default class Explorer {
}); });
this.addDatabasePane = new AddDatabasePane({ this.addDatabasePane = new AddDatabasePane({
documentClientUtility: this.documentClientUtility,
id: "adddatabasepane", id: "adddatabasepane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -584,7 +616,6 @@ export default class Explorer {
this.addCollectionPane = new AddCollectionPane({ this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
documentClientUtility: this.documentClientUtility,
id: "addcollectionpane", id: "addcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -592,7 +623,6 @@ export default class Explorer {
}); });
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({ this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({
documentClientUtility: this.documentClientUtility,
id: "deletecollectionconfirmationpane", id: "deletecollectionconfirmationpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -600,7 +630,6 @@ export default class Explorer {
}); });
this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({ this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({
documentClientUtility: this.documentClientUtility,
id: "deletedatabaseconfirmationpane", id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -608,7 +637,6 @@ export default class Explorer {
}); });
this.graphStylingPane = new GraphStylingPane({ this.graphStylingPane = new GraphStylingPane({
documentClientUtility: this.documentClientUtility,
id: "graphstylingpane", id: "graphstylingpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -616,7 +644,6 @@ export default class Explorer {
}); });
this.addTableEntityPane = new AddTableEntityPane({ this.addTableEntityPane = new AddTableEntityPane({
documentClientUtility: this.documentClientUtility,
id: "addtableentitypane", id: "addtableentitypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -624,7 +651,6 @@ export default class Explorer {
}); });
this.editTableEntityPane = new EditTableEntityPane({ this.editTableEntityPane = new EditTableEntityPane({
documentClientUtility: this.documentClientUtility,
id: "edittableentitypane", id: "edittableentitypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -632,7 +658,6 @@ export default class Explorer {
}); });
this.tableColumnOptionsPane = new TableColumnOptionsPane({ this.tableColumnOptionsPane = new TableColumnOptionsPane({
documentClientUtility: this.documentClientUtility,
id: "tablecolumnoptionspane", id: "tablecolumnoptionspane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -640,7 +665,6 @@ export default class Explorer {
}); });
this.querySelectPane = new QuerySelectPane({ this.querySelectPane = new QuerySelectPane({
documentClientUtility: this.documentClientUtility,
id: "queryselectpane", id: "queryselectpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -648,7 +672,6 @@ export default class Explorer {
}); });
this.newVertexPane = new NewVertexPane({ this.newVertexPane = new NewVertexPane({
documentClientUtility: this.documentClientUtility,
id: "newvertexpane", id: "newvertexpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -656,7 +679,6 @@ export default class Explorer {
}); });
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
documentClientUtility: this.documentClientUtility,
id: "cassandraaddcollectionpane", id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -664,7 +686,6 @@ export default class Explorer {
}); });
this.settingsPane = new SettingsPane({ this.settingsPane = new SettingsPane({
documentClientUtility: this.documentClientUtility,
id: "settingspane", id: "settingspane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -672,7 +693,6 @@ export default class Explorer {
}); });
this.executeSprocParamsPane = new ExecuteSprocParamsPane({ this.executeSprocParamsPane = new ExecuteSprocParamsPane({
documentClientUtility: this.documentClientUtility,
id: "executesprocparamspane", id: "executesprocparamspane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -680,7 +700,6 @@ export default class Explorer {
}); });
this.renewAdHocAccessPane = new RenewAdHocAccessPane({ this.renewAdHocAccessPane = new RenewAdHocAccessPane({
documentClientUtility: this.documentClientUtility,
id: "renewadhocaccesspane", id: "renewadhocaccesspane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -688,7 +707,6 @@ export default class Explorer {
}); });
this.uploadItemsPane = new UploadItemsPane({ this.uploadItemsPane = new UploadItemsPane({
documentClientUtility: this.documentClientUtility,
id: "uploaditemspane", id: "uploaditemspane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -698,7 +716,6 @@ export default class Explorer {
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.loadQueryPane = new LoadQueryPane({ this.loadQueryPane = new LoadQueryPane({
documentClientUtility: this.documentClientUtility,
id: "loadquerypane", id: "loadquerypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -706,7 +723,6 @@ export default class Explorer {
}); });
this.saveQueryPane = new SaveQueryPane({ this.saveQueryPane = new SaveQueryPane({
documentClientUtility: this.documentClientUtility,
id: "savequerypane", id: "savequerypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -714,7 +730,6 @@ export default class Explorer {
}); });
this.browseQueriesPane = new BrowseQueriesPane({ this.browseQueriesPane = new BrowseQueriesPane({
documentClientUtility: this.documentClientUtility,
id: "browsequeriespane", id: "browsequeriespane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -722,7 +737,6 @@ export default class Explorer {
}); });
this.uploadFilePane = new UploadFilePane({ this.uploadFilePane = new UploadFilePane({
documentClientUtility: this.documentClientUtility,
id: "uploadfilepane", id: "uploadfilepane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -730,7 +744,6 @@ export default class Explorer {
}); });
this.stringInputPane = new StringInputPane({ this.stringInputPane = new StringInputPane({
documentClientUtility: this.documentClientUtility,
id: "stringinputpane", id: "stringinputpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -738,7 +751,6 @@ export default class Explorer {
}); });
this.setupNotebooksPane = new SetupNotebooksPane({ this.setupNotebooksPane = new SetupNotebooksPane({
documentClientUtility: this.documentClientUtility,
id: "setupnotebookspane", id: "setupnotebookspane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -771,7 +783,6 @@ export default class Explorer {
this.setupNotebooksPane this.setupNotebooksPane
]; ];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.rebindDocumentClientUtility.bind(this);
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
document.addEventListener( document.addEventListener(
@@ -853,7 +864,7 @@ export default class Explorer {
this.editTableEntityPane.title("Edit Table Entity"); this.editTableEntityPane.title("Edit Table Entity");
this.deleteCollectionConfirmationPane.title("Delete Table"); this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new TablesAPIDataClient(this.documentClientUtility); this.tableDataClient = new TablesAPIDataClient();
break; break;
case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): case Constants.DefaultAccountExperience.Cassandra.toLowerCase():
this.addCollectionText("New Table"); this.addCollectionText("New Table");
@@ -872,7 +883,7 @@ export default class Explorer {
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.deleteDatabaseConfirmationPane.title("Delete Keyspace"); this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id"); this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.tableDataClient = new CassandraAPIDataClient(this.documentClientUtility); this.tableDataClient = new CassandraAPIDataClient();
break; break;
} }
}); });
@@ -1018,7 +1029,7 @@ export default class Explorer {
); );
try { try {
const databaseAccount: ViewModels.DatabaseAccount = await resourceProviderClient.patchAsync( const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
this.databaseAccount().id, this.databaseAccount().id,
"2019-12-12", "2019-12-12",
{ {
@@ -1057,13 +1068,6 @@ export default class Explorer {
// TODO: return result // TODO: return result
} }
public rebindDocumentClientUtility(documentClientUtility: DocumentClientUtilityBase): void {
this.documentClientUtility = documentClientUtility;
this._panes.forEach((pane: ViewModels.ContextualPane) => {
pane.documentClientUtility = documentClientUtility;
});
}
public copyUrlLink(src: any, event: MouseEvent): void { public copyUrlLink(src: any, event: MouseEvent): void {
const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement; const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement;
urlLinkInput && urlLinkInput.select(); urlLinkInput && urlLinkInput.select();
@@ -1382,7 +1386,7 @@ export default class Explorer {
} }
const deferred: Q.Deferred<void> = Q.defer(); const deferred: Q.Deferred<void> = Q.defer();
this.documentClientUtility.readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
this.selectedNode(this.resourceTokenCollection()); this.selectedNode(this.resourceTokenCollection());
deferred.resolve(); deferred.resolve();
@@ -1406,16 +1410,13 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree dataExplorerArea: Constants.Areas.ResourceTree
}); });
} }
// TODO: Refactor // TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer(); const deferred: Q.Deferred<any> = Q.defer();
const offerPromise: Q.Promise<DataModels.Offer[]> = this.documentClientUtility.readOffers();
this._setLoadingStatusText("Fetching offers...");
offerPromise.then( const refreshDatabases = (offers?: DataModels.Offer[]) => {
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
this._setLoadingStatusText("Fetching databases..."); this._setLoadingStatusText("Fetching databases...");
this.documentClientUtility.readDatabases(null /*options*/).then( readDatabases().then(
(databases: DataModels.Database[]) => { (databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases."); this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -1466,6 +1467,18 @@ export default class Explorer {
); );
} }
); );
};
if (this.isServerlessEnabled()) {
// Serverless accounts don't support offers call
refreshDatabases();
} else {
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers();
this._setLoadingStatusText("Fetching offers...");
offerPromise.then(
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
refreshDatabases(offers);
}, },
error => { error => {
this._setLoadingStatusText("Failed to fetch offers."); this._setLoadingStatusText("Failed to fetch offers.");
@@ -1487,6 +1500,7 @@ export default class Explorer {
); );
} }
); );
}
return deferred.promise.then( return deferred.promise.then(
() => { () => {
@@ -1535,7 +1549,7 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree dataExplorerArea: Constants.Areas.ResourceTree
}); });
this.isRefreshingExplorer(true); this.isRefreshingExplorer(true);
this.documentClientUtility.refreshCachedResources().then( refreshCachedResources().then(
() => { () => {
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabases, Action.LoadDatabases,
@@ -1588,7 +1602,7 @@ export default class Explorer {
public async getArcadiaToken(): Promise<string> { public async getArcadiaToken(): Promise<string> {
return new Promise<string>((resolve: (token: string) => void, reject: (error: any) => void) => { return new Promise<string>((resolve: (token: string) => void, reject: (error: any) => void) => {
MessageHandler.sendCachedDataMessage<string>(MessageTypes.GetArcadiaToken, undefined /** params **/).then( sendCachedDataMessage<string>(MessageTypes.GetArcadiaToken, undefined /** params **/).then(
(token: string) => { (token: string) => {
resolve(token); resolve(token);
}, },
@@ -1602,7 +1616,7 @@ export default class Explorer {
private async _getArcadiaWorkspaces(): Promise<ArcadiaWorkspaceItem[]> { private async _getArcadiaWorkspaces(): Promise<ArcadiaWorkspaceItem[]> {
try { try {
const workspaces = await this._arcadiaManager.listWorkspacesAsync([CosmosClient.subscriptionId()]); const workspaces = await this._arcadiaManager.listWorkspacesAsync([userContext.subscriptionId]);
let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length); let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length);
const sparkPromises: Promise<void>[] = []; const sparkPromises: Promise<void>[] = [];
workspaces.forEach((workspace, i) => { workspaces.forEach((workspace, i) => {
@@ -1626,11 +1640,11 @@ export default class Explorer {
} }
public async createWorkspace(): Promise<string> { public async createWorkspace(): Promise<string> {
return MessageHandler.sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/); return sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/);
} }
public async createSparkPool(workspaceId: string): Promise<string> { public async createSparkPool(workspaceId: string): Promise<string> {
return MessageHandler.sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]); return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]);
} }
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> { public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
@@ -1705,7 +1719,7 @@ export default class Explorer {
} }
try { try {
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount.id); const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id);
return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default"); return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default");
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace"); Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace");
@@ -1718,6 +1732,7 @@ export default class Explorer {
return; return;
} }
let clearMessage;
try { try {
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync( const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
this.databaseAccount().id, this.databaseAccount().id,
@@ -1729,10 +1744,14 @@ export default class Explorer {
notebookWorkspace.properties.status && notebookWorkspace.properties.status &&
notebookWorkspace.properties.status.toLowerCase() === "stopped" notebookWorkspace.properties.status.toLowerCase() === "stopped"
) { ) {
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
} }
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning"); Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning");
NotificationConsoleUtils.logConsoleError(`Failed to initialize notebook workspace: ${JSON.stringify(error)}`);
} finally {
clearMessage && clearMessage();
} }
} }
@@ -1807,8 +1826,8 @@ export default class Explorer {
const isRunningInPortal = window.dataExplorerPlatform == PlatformType.Portal; const isRunningInPortal = window.dataExplorerPlatform == PlatformType.Portal;
const isRunningInDevMode = process.env.NODE_ENV === "development"; const isRunningInDevMode = process.env.NODE_ENV === "development";
if (inputs && config.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) { if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
inputs.extensionEndpoint = config.PROXY_PATH; inputs.extensionEndpoint = configContext.PROXY_PATH;
} }
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q(); const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
@@ -1826,7 +1845,7 @@ export default class Explorer {
} }
} }
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
MessageHandler.handleCachedDataMessage(message); handleCachedDataMessage(message);
return; return;
} }
if (message.type) { if (message.type) {
@@ -1913,8 +1932,7 @@ export default class Explorer {
this.features(inputs.features); this.features(inputs.features);
this.serverId(inputs.serverId); this.serverId(inputs.serverId);
this.extensionEndpoint(inputs.extensionEndpoint || ""); this.extensionEndpoint(inputs.extensionEndpoint || "");
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || config.ARM_ENDPOINT)); this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.notificationsClient.setExtensionEndpoint(this.extensionEndpoint());
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType); this.subscriptionType(inputs.subscriptionType);
this.quotaId(inputs.quotaId); this.quotaId(inputs.quotaId);
@@ -1929,11 +1947,12 @@ export default class Explorer {
this._importExplorerConfigComplete = true; this._importExplorerConfigComplete = true;
CosmosClient.authorizationToken(authorizationToken); updateUserContext({
CosmosClient.masterKey(masterKey); authorizationToken,
CosmosClient.databaseAccount(databaseAccount); masterKey,
CosmosClient.subscriptionId(inputs.subscriptionId); databaseAccount
CosmosClient.resourceGroup(inputs.resourceGroup); });
updateUserContext({ resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId });
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
{ {
@@ -1963,7 +1982,7 @@ export default class Explorer {
return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => { return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => {
const openedSprocTab = this.tabsManager.getTabs( const openedSprocTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures, ViewModels.CollectionTabKind.StoredProcedures,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === storedProcedure.rid tab => tab.node && tab.node.rid === storedProcedure.rid
); );
return ( return (
storedProcedure.rid === this.selectedNode().rid || storedProcedure.rid === this.selectedNode().rid ||
@@ -1977,7 +1996,7 @@ export default class Explorer {
return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => { return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => {
const openedUdfTab = this.tabsManager.getTabs( const openedUdfTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions, ViewModels.CollectionTabKind.UserDefinedFunctions,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === userDefinedFunction.rid tab => tab.node && tab.node.rid === userDefinedFunction.rid
); );
return ( return (
userDefinedFunction.rid === this.selectedNode().rid || userDefinedFunction.rid === this.selectedNode().rid ||
@@ -1991,7 +2010,7 @@ export default class Explorer {
return _.find(selectedCollection.triggers(), (trigger: Trigger) => { return _.find(selectedCollection.triggers(), (trigger: Trigger) => {
const openedTriggerTab = this.tabsManager.getTabs( const openedTriggerTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Triggers, ViewModels.CollectionTabKind.Triggers,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === trigger.rid tab => tab.node && tab.node.rid === trigger.rid
); );
return ( return (
trigger.rid === this.selectedNode().rid || trigger.rid === this.selectedNode().rid ||
@@ -2001,7 +2020,7 @@ export default class Explorer {
} }
public closeAllPanes(): void { public closeAllPanes(): void {
this._panes.forEach((pane: ViewModels.ContextualPane) => pane.close()); this._panes.forEach((pane: ContextualPaneBase) => pane.close());
} }
public getPlatformType(): PlatformType { public getPlatformType(): PlatformType {
@@ -2016,13 +2035,13 @@ export default class Explorer {
); );
} }
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void { public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
this.commandBarComponentAdapter.onUpdateTabsButtons(buttons); this.commandBarComponentAdapter.onUpdateTabsButtons(buttons);
} }
public signInAad = () => { public signInAad = () => {
TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" }); TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" });
MessageHandler.sendMessage({ sendMessage({
type: MessageTypes.AadSignIn type: MessageTypes.AadSignIn
}); });
}; };
@@ -2033,21 +2052,21 @@ export default class Explorer {
}; };
public clickHostedAccountSwitch = () => { public clickHostedAccountSwitch = () => {
MessageHandler.sendMessage({ sendMessage({
type: MessageTypes.UpdateAccountSwitch, type: MessageTypes.UpdateAccountSwitch,
click: true click: true
}); });
}; };
public clickHostedDirectorySwitch = () => { public clickHostedDirectorySwitch = () => {
MessageHandler.sendMessage({ sendMessage({
type: MessageTypes.UpdateDirectoryControl, type: MessageTypes.UpdateDirectoryControl,
click: true click: true
}); });
}; };
public refreshDatabaseAccount = () => { public refreshDatabaseAccount = () => {
MessageHandler.sendMessage({ sendMessage({
type: MessageTypes.RefreshDatabaseAccount type: MessageTypes.RefreshDatabaseAccount
}); });
}; };
@@ -2076,9 +2095,7 @@ export default class Explorer {
if (isNewDatabase) { if (isNewDatabase) {
database.expandDatabase(); database.expandDatabase();
} }
this.tabsManager.refreshActiveTab( this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
(tab: ViewModels.Tab) => tab.collection && tab.collection.getDatabase().rid === database.rid
);
}) })
); );
}); });
@@ -2180,8 +2197,8 @@ export default class Explorer {
return undefined; return undefined;
} }
const urlPrefixWithKeyParam: string = `${config.hostedExplorerURL}?key=`; const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`;
const currentActiveTab: ViewModels.Tab = this.tabsManager.activeTab(); const currentActiveTab = this.tabsManager.activeTab();
return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`; return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`;
} }
@@ -2292,7 +2309,7 @@ export default class Explorer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId); return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
} }
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled"; const error = "Attempt to upload notebook, but notebook is not enabled";
Logger.logError(error, "Explorer/uploadFile"); Logger.logError(error, "Explorer/uploadFile");
@@ -2347,14 +2364,28 @@ export default class Explorer {
return Promise.resolve(false); return Promise.resolve(false);
} }
public publishNotebook(name: string, content: string): void { public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> {
if (this.notebookManager) { if (this.notebookManager) {
this.notebookManager.openPublishNotebookPane(name, content); await this.notebookManager.openPublishNotebookPane(
name,
content,
parentDomElement,
this.isCodeOfConductEnabled(),
this.isLinkInjectionEnabled()
);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true); this.isPublishNotebookPaneEnabled(true);
} }
} }
public copyNotebook(name: string, content: string): void {
if (this.notebookManager) {
this.notebookManager.openCopyNotebookPane(name, content);
this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter;
this.isCopyNotebookPaneEnabled(true);
}
}
public showOkModalDialog(title: string, msg: string): void { public showOkModalDialog(title: string, msg: string): void {
this._dialogProps({ this._dialogProps({
isModal: true, isModal: true,
@@ -2444,28 +2475,26 @@ export default class Explorer {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
} }
const notebookTabs: NotebookV2Tab[] = this.tabsManager.getTabs( const notebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2, ViewModels.CollectionTabKind.NotebookV2,
(tab: ViewModels.Tab) => tab =>
(tab as NotebookV2Tab).notebookPath && (tab as NotebookV2Tab).notebookPath &&
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path) FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path)
) as NotebookV2Tab[]; ) as NotebookV2Tab[];
let notebookTab: NotebookV2Tab = notebookTabs && notebookTabs[0]; let notebookTab = notebookTabs && notebookTabs[0];
if (notebookTab) { if (notebookTab) {
this.tabsManager.activateTab(notebookTab); this.tabsManager.activateTab(notebookTab);
} else { } else {
const options: ViewModels.NotebookTabOptions = { const options: NotebookTabOptions = {
account: CosmosClient.databaseAccount(), account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookV2, tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: null, node: null,
title: notebookContentItem.name, title: notebookContentItem.name,
tabPath: notebookContentItem.path, tabPath: notebookContentItem.path,
documentClientUtility: null,
collection: null, collection: null,
selfLink: null, selfLink: null,
masterKey: CosmosClient.masterKey() || "", masterKey: userContext.masterKey || "",
hashLocation: "notebooks", hashLocation: "notebooks",
isActive: ko.observable(false), isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
@@ -2521,7 +2550,7 @@ export default class Explorer {
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
}) })
.then(newNotebookFile => { .then(newNotebookFile => {
const notebookTabs: ViewModels.Tab[] = this.tabsManager.getTabs( const notebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2, ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath) (tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
); );
@@ -2609,7 +2638,11 @@ export default class Explorer {
private async _refreshNotebooksEnabledStateForAccount(): Promise<void> { private async _refreshNotebooksEnabledStateForAccount(): Promise<void> {
const authType = window.authType as AuthType; const authType = window.authType as AuthType;
if (authType === AuthType.EncryptedToken || authType === AuthType.ResourceToken) { if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
this.isNotebooksEnabledForAccount(false); this.isNotebooksEnabledForAccount(false);
return; return;
} }
@@ -2651,7 +2684,7 @@ export default class Explorer {
} }
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => { public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const subscriptionId = CosmosClient.subscriptionId(); const subscriptionId = userContext.subscriptionId;
const armEndpoint = this.armEndpoint(); const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType; const authType = window.authType as AuthType;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
@@ -2680,7 +2713,7 @@ export default class Explorer {
}; };
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => { public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
const subscriptionId = CosmosClient.subscriptionId(); const subscriptionId = userContext.subscriptionId;
const armEndpoint = this.armEndpoint(); const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType; const authType = window.authType as AuthType;
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
@@ -2709,6 +2742,7 @@ export default class Explorer {
} }
await this.resourceTree.initialize(); await this.resourceTree.initialize();
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);
} }
@@ -2891,7 +2925,7 @@ export default class Explorer {
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Terminal, ViewModels.CollectionTabKind.Terminal,
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation tab => tab.hashLocation() == hashLocation
) as TerminalTab[]; ) as TerminalTab[];
let terminalTab: TerminalTab = terminalTabs && terminalTabs[0]; let terminalTab: TerminalTab = terminalTabs && terminalTabs[0];
@@ -2899,13 +2933,11 @@ export default class Explorer {
this.tabsManager.activateTab(terminalTab); this.tabsManager.activateTab(terminalTab);
} else { } else {
const newTab = new TerminalTab({ const newTab = new TerminalTab({
account: CosmosClient.databaseAccount(), account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.Terminal, tabKind: ViewModels.CollectionTabKind.Terminal,
node: null, node: null,
title: title, title: title,
tabPath: title, tabPath: title,
documentClientUtility: null,
collection: null, collection: null,
selfLink: null, selfLink: null,
hashLocation: hashLocation, hashLocation: hashLocation,
@@ -2927,7 +2959,7 @@ export default class Explorer {
const galleryTabs = this.tabsManager.getTabs( const galleryTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Gallery, ViewModels.CollectionTabKind.Gallery,
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation tab => tab.hashLocation() == hashLocation
); );
let galleryTab = galleryTabs && galleryTabs[0]; let galleryTab = galleryTabs && galleryTabs[0];
@@ -2940,7 +2972,7 @@ export default class Explorer {
const newTab = new this.galleryTab.default({ const newTab = new this.galleryTab.default({
// GalleryTabOptions // GalleryTabOptions
account: CosmosClient.databaseAccount(), account: userContext.databaseAccount,
container: this, container: this,
junoClient: this.notebookManager?.junoClient, junoClient: this.notebookManager?.junoClient,
notebookUrl, notebookUrl,
@@ -2973,24 +3005,21 @@ export default class Explorer {
const notebookViewerTabModule = this.notebookViewerTab; const notebookViewerTabModule = this.notebookViewerTab;
let isNotebookViewerOpen = (tab: ViewModels.Tab) => { let isNotebookViewerOpen = (tab: TabsBase) => {
const notebookViewerTab = tab as typeof notebookViewerTabModule.default; const notebookViewerTab = tab as typeof notebookViewerTabModule.default;
return notebookViewerTab.notebookUrl === notebookUrl; return notebookViewerTab.notebookUrl === notebookUrl;
}; };
const notebookViewerTabs = this.tabsManager.getTabs( const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, tab => {
ViewModels.CollectionTabKind.NotebookV2,
(tab: ViewModels.Tab) => {
return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab); return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab);
} });
);
let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0]; let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0];
if (notebookViewerTab) { if (notebookViewerTab) {
this.tabsManager.activateNewTab(notebookViewerTab); this.tabsManager.activateNewTab(notebookViewerTab);
} else { } else {
notebookViewerTab = new this.notebookViewerTab.default({ notebookViewerTab = new this.notebookViewerTab.default({
account: CosmosClient.databaseAccount(), account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookViewer, tabKind: ViewModels.CollectionTabKind.NotebookViewer,
node: null, node: null,
title: title, title: title,

View File

@@ -15,7 +15,7 @@ import { GraphData, D3Node, D3Link } from "./GraphData";
import { HashMap } from "../../../Common/HashMap"; import { HashMap } from "../../../Common/HashMap";
import { BaseType } from "d3"; import { BaseType } from "d3";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab"; import { GraphConfig } from "../../Tabs/GraphTab";
import { GraphExplorer } from "./GraphExplorer"; import { GraphExplorer } from "./GraphExplorer";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";

View File

@@ -1,3 +1,4 @@
jest.mock("../../../Common/DocumentClientUtilityBase");
import React from "react"; import React from "react";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
@@ -7,11 +8,11 @@ import { GraphExplorer, GraphExplorerProps, GraphAccessor, GraphHighlightedNodeD
import * as D3ForceGraph from "./D3ForceGraph"; import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData"; import { GraphData } from "./GraphData";
import { TabComponent } from "../../Controls/Tabs/TabComponent"; import { TabComponent } from "../../Controls/Tabs/TabComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility"; import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab"; import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
describe("Check whether query result is vertex array", () => { describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => { it("should reject null as vertex array", () => {
@@ -134,7 +135,7 @@ describe("GraphExplorer", () => {
const COLLECTION_SELF_LINK = "collectionSelfLink"; const COLLECTION_SELF_LINK = "collectionSelfLink";
const gremlinRU = 789.12; const gremlinRU = 789.12;
const createMockProps = (documentClientUtility?: any): GraphExplorerProps => { const createMockProps = (): GraphExplorerProps => {
const graphConfig = GraphTab.createGraphConfig(); const graphConfig = GraphTab.createGraphConfig();
const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig); const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig);
@@ -149,7 +150,6 @@ describe("GraphExplorer", () => {
onIsValidQueryChange: (isValidQuery: boolean): void => {}, onIsValidQueryChange: (isValidQuery: boolean): void => {},
collectionPartitionKeyProperty: "collectionPartitionKeyProperty", collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
documentClientUtility: documentClientUtility,
collectionRid: COLLECTION_RID, collectionRid: COLLECTION_RID,
collectionSelfLink: COLLECTION_SELF_LINK, collectionSelfLink: COLLECTION_SELF_LINK,
graphBackendEndpoint: "graphBackendEndpoint", graphBackendEndpoint: "graphBackendEndpoint",
@@ -188,7 +188,6 @@ describe("GraphExplorer", () => {
let wrapper: ReactWrapper; let wrapper: ReactWrapper;
let connectStub: sinon.SinonSpy; let connectStub: sinon.SinonSpy;
let queryDocStub: sinon.SinonSpy;
let submitToBackendSpy: sinon.SinonSpy; let submitToBackendSpy: sinon.SinonSpy;
let renderResultAsJsonStub: sinon.SinonSpy; let renderResultAsJsonStub: sinon.SinonSpy;
let onMiddlePaneInitializedStub: sinon.SinonSpy; let onMiddlePaneInitializedStub: sinon.SinonSpy;
@@ -215,46 +214,6 @@ describe("GraphExplorer", () => {
[query: string]: AjaxResponse; [query: string]: AjaxResponse;
} }
const createDocumentClientUtilityMock = (docDBResponse: AjaxResponse) => {
const mock = {
queryDocuments: () => {},
queryDocumentsPage: (
rid: string,
iterator: any,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> => {
const qresult = {
hasMoreResults: false,
firstItemIndex: firstItemIndex,
lastItemIndex: 0,
itemCount: 0,
documents: docDBResponse.response,
activityId: "",
headers: [] as any[],
requestCharge: gVRU
};
return Q.resolve(qresult);
}
};
const fakeIterator: any = {
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
};
queryDocStub = sinon.stub(mock, "queryDocuments").callsFake(
(container: ViewModels.DocumentRequestContainer, query: string, options: any): Q.Promise<any> => {
(fakeIterator as any)._query = query;
return Q.resolve(fakeIterator);
}
);
return mock;
};
const setupMocks = ( const setupMocks = (
graphExplorer: GraphExplorer, graphExplorer: GraphExplorer,
backendResponses: BackendResponses, backendResponses: BackendResponses,
@@ -333,7 +292,29 @@ describe("GraphExplorer", () => {
done: any, done: any,
ignoreD3Update: boolean ignoreD3Update: boolean
): GraphExplorer => { ): GraphExplorer => {
const props: GraphExplorerProps = createMockProps(createDocumentClientUtilityMock(docDBResponse)); (queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
return Q.resolve({
_query: query,
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
});
});
(queryDocumentsPage as jest.Mock).mockImplementation(
(rid: string, iterator: any, firstItemIndex: number, options: any) => {
return Q.resolve({
hasMoreResults: false,
firstItemIndex: firstItemIndex,
lastItemIndex: 0,
itemCount: 0,
documents: docDBResponse.response,
activityId: "",
headers: [] as any[],
requestCharge: gVRU
});
}
);
const props: GraphExplorerProps = createMockProps();
wrapper = mount(<GraphExplorer {...props} />); wrapper = mount(<GraphExplorer {...props} />);
graphExplorerInstance = wrapper.instance() as GraphExplorer; graphExplorerInstance = wrapper.instance() as GraphExplorer;
setupMocks(graphExplorerInstance, backendResponses, done, ignoreD3Update); setupMocks(graphExplorerInstance, backendResponses, done, ignoreD3Update);
@@ -341,7 +322,7 @@ describe("GraphExplorer", () => {
}; };
const cleanUpStubsWrapper = () => { const cleanUpStubsWrapper = () => {
queryDocStub.restore(); jest.resetAllMocks();
connectStub.restore(); connectStub.restore();
submitToBackendSpy.restore(); submitToBackendSpy.restore();
renderResultAsJsonStub.restore(); renderResultAsJsonStub.restore();
@@ -378,22 +359,11 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false); expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
}); });
it("should submit g.V() as docdb query with proper query", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[2]
).toBe(DOCDB_G_DOT_V_QUERY);
});
it("should submit g.V() as docdb query with proper parameters", () => { it("should submit g.V() as docdb query with proper parameters", () => {
expect( expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0] maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
).toEqual("databaseId"); enableCrossPartitionQuery: true
expect( });
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[1]
).toEqual("collectionId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[3]
).toEqual({ maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, enableCrossPartitionQuery: true });
}); });
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => { it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {
@@ -426,22 +396,11 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false); expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
}); });
it("should submit g.V() as docdb query with proper query", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[2]
).toBe(DOCDB_G_DOT_V_QUERY);
});
it("should submit g.V() as docdb query with proper parameters", () => { it("should submit g.V() as docdb query with proper parameters", () => {
expect( expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0] maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
).toEqual("databaseId"); enableCrossPartitionQuery: true
expect( });
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[1]
).toEqual("collectionId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[3]
).toEqual({ maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, enableCrossPartitionQuery: true });
}); });
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => { it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {

View File

@@ -8,7 +8,7 @@ import * as D3ForceGraph from "./D3ForceGraph";
import { GraphVizComponentProps } from "./GraphVizComponent"; import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GraphData from "./GraphData"; import * as GraphData from "./GraphData";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphUtil } from "./GraphUtil"; import { GraphUtil } from "./GraphUtil";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
@@ -28,7 +28,7 @@ import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels"; import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase"; import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
@@ -47,7 +47,6 @@ export interface GraphExplorerProps {
onIsValidQueryChange: (isValidQuery: boolean) => void; onIsValidQueryChange: (isValidQuery: boolean) => void;
collectionPartitionKeyProperty: string; collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string; collectionRid: string;
collectionSelfLink: string; collectionSelfLink: string;
graphBackendEndpoint: string; graphBackendEndpoint: string;
@@ -697,7 +696,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param cmd * @param cmd
*/ */
public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> { public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> {
console.log("submit:", cmd);
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`); const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`);
this.setExecuteCounter(this.executeCounter + 1); this.setExecuteCounter(this.executeCounter + 1);
@@ -730,14 +728,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
*/ */
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> { public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
// TODO maxItemCount: this reduces throttling, but won't cap the # of results // TODO maxItemCount: this reduces throttling, but won't cap the # of results
return this.props.documentClientUtility return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.PAGE_ALL, maxItemCount: GraphExplorer.PAGE_ALL,
enableCrossPartitionQuery: enableCrossPartitionQuery:
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) === StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
"true" "true"
}) }).then(
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => { (iterator: QueryIterator<ItemDefinition & Resource>) => {
return iterator.fetchNext().then(response => response.resources); return iterator.fetchNext().then(response => response.resources);
}, },
@@ -1732,11 +1728,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`; query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`;
} }
return this.props.documentClientUtility return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
}) })
.then( .then(
(iterator: QueryIterator<ItemDefinition & Resource>) => { (iterator: QueryIterator<ItemDefinition & Resource>) => {
@@ -1766,8 +1760,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`; .currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`); const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return this.props.documentClientUtility return queryDocumentsPage(
.queryDocumentsPage(
this.props.collectionRid, this.props.collectionRid,
this.currentDocDBQueryInfo.iterator, this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index, this.currentDocDBQueryInfo.index,

View File

@@ -3,7 +3,6 @@ import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { GraphConfig } from "../../Tabs/GraphTab"; import { GraphConfig } from "../../Tabs/GraphTab";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { GraphExplorer, GraphAccessor } from "./GraphExplorer"; import { GraphExplorer, GraphAccessor } from "./GraphExplorer";
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
interface Parameter { interface Parameter {
onIsNewVertexDisabledChange: (isEnabled: boolean) => void; onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
@@ -18,7 +17,6 @@ interface Parameter {
graphConfig?: GraphConfig; graphConfig?: GraphConfig;
collectionPartitionKeyProperty: string; collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string; collectionRid: string;
collectionSelfLink: string; collectionSelfLink: string;
graphBackendEndpoint: string; graphBackendEndpoint: string;
@@ -51,7 +49,6 @@ export class GraphExplorerAdapter implements ReactAdapter {
onIsGraphDisplayed={this.params.onIsGraphDisplayed} onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues} onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty} collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
documentClientUtility={this.params.documentClientUtility}
collectionRid={this.params.collectionRid} collectionRid={this.params.collectionRid}
collectionSelfLink={this.params.collectionSelfLink} collectionSelfLink={this.params.collectionSelfLink}
graphBackendEndpoint={this.params.graphBackendEndpoint} graphBackendEndpoint={this.params.graphBackendEndpoint}

View File

@@ -1,6 +1,6 @@
import * as sinon from "sinon"; import * as sinon from "sinon";
import { GremlinClient, GremlinClientParameters } from "./GremlinClient"; import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import * as Logger from "../../../Common/Logger"; import * as Logger from "../../../Common/Logger";
describe("Gremlin Client", () => { describe("Gremlin Client", () => {

View File

@@ -4,7 +4,7 @@
import * as Q from "q"; import * as Q from "q";
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient"; import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../../Common/HashMap"; import { HashMap } from "../../../Common/HashMap";
import * as Logger from "../../../Common/Logger"; import * as Logger from "../../../Common/Logger";

View File

@@ -12,11 +12,12 @@ import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/Com
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import { CommandBarUtil } from "./CommandBarUtil"; import { CommandBarUtil } from "./CommandBarUtil";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentAdapter implements ReactAdapter { export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>; public parameters: ko.Observable<number>;
public container: Explorer; public container: Explorer;
private tabsButtons: ViewModels.NavbarButtonConfig[]; private tabsButtons: CommandButtonComponentProps[];
private isNotebookTabActive: ko.Computed<boolean>; private isNotebookTabActive: ko.Computed<boolean>;
constructor(container: Explorer) { constructor(container: Explorer) {
@@ -44,14 +45,15 @@ export class CommandBarComponentAdapter implements ReactAdapter {
container.isHostedDataExplorerEnabled, container.isHostedDataExplorerEnabled,
container.isSynapseLinkUpdating, container.isSynapseLinkUpdating,
container.databaseAccount, container.databaseAccount,
this.isNotebookTabActive this.isNotebookTabActive,
container.isServerlessEnabled
]; ];
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender()); ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
this.parameters = ko.observable(Date.now()); this.parameters = ko.observable(Date.now());
} }
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void { public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
this.tabsButtons = buttons; this.tabsButtons = buttons;
this.triggerRender(); this.triggerRender();
} }

View File

@@ -7,6 +7,47 @@ import Explorer from "../../Explorer";
describe("CommandBarComponentButtonFactory tests", () => { describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer; let mockExplorer: Explorer;
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link (Preview)";
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = () => false;
});
it("Account is not serverless - button should be visible", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const enableAzureSynapseLinkBtn = buttons.find(
button => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeDefined();
});
it("Account is serverless - button should be hidden", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const enableAzureSynapseLinkBtn = buttons.find(
button => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeUndefined();
});
});
describe("Enable notebook button", () => { describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)"; const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
@@ -23,6 +64,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true); mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
it("Notebooks is already enabled - button should be hidden", () => { it("Notebooks is already enabled - button should be hidden", () => {
@@ -86,6 +128,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true); mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
beforeEach(() => { beforeEach(() => {
@@ -167,6 +210,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true); mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
beforeEach(() => { beforeEach(() => {
@@ -254,6 +298,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.notebookManager = new NotebookManager(); mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined); mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
beforeEach(() => { beforeEach(() => {
@@ -305,6 +350,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true); mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
it("should only show New SQL Query and Open Query buttons", () => { it("should only show New SQL Query and Open Query buttons", () => {

View File

@@ -24,19 +24,20 @@ import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg"; import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import GitHubIcon from "../../../../images/github.svg"; import GitHubIcon from "../../../../images/github.svg";
import SynapseIcon from "../../../../images/synapse-link.svg"; import SynapseIcon from "../../../../images/synapse-link.svg";
import { config, Platform } from "../../../Config"; import { configContext, Platform } from "../../../ConfigContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentButtonFactory { export class CommandBarComponentButtonFactory {
private static counter: number = 0; private static counter: number = 0;
public static createStaticCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] { public static createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
if (container.isAuthWithResourceToken()) { if (container.isAuthWithResourceToken()) {
return CommandBarComponentButtonFactory.createStaticCommandBarButtonsForResourceToken(container); return CommandBarComponentButtonFactory.createStaticCommandBarButtonsForResourceToken(container);
} }
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container); const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
const buttons: ViewModels.NavbarButtonConfig[] = [newCollectionBtn]; const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container); const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) { if (addSynapseLink) {
@@ -112,7 +113,7 @@ export class CommandBarComponentButtonFactory {
if (CommandBarComponentButtonFactory.areScriptsSupported(container)) { if (CommandBarComponentButtonFactory.areScriptsSupported(container)) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
@@ -133,12 +134,12 @@ export class CommandBarComponentButtonFactory {
return buttons; return buttons;
} }
public static createContextCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] { public static createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: ViewModels.NavbarButtonConfig[] = []; const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) { if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
const label = "New Shell"; const label = "New Shell";
const newMongoShellBtn: ViewModels.NavbarButtonConfig = { const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
@@ -156,15 +157,15 @@ export class CommandBarComponentButtonFactory {
return buttons; return buttons;
} }
public static createControlCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] { public static createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: ViewModels.NavbarButtonConfig[] = []; const buttons: CommandButtonComponentProps[] = [];
if (window.dataExplorerPlatform === PlatformType.Hosted) { if (window.dataExplorerPlatform === PlatformType.Hosted) {
return buttons; return buttons;
} }
if (!container.isPreferredApiCassandra()) { if (!container.isPreferredApiCassandra()) {
const label = "Settings"; const label = "Settings";
const settingsPaneButton: ViewModels.NavbarButtonConfig = { const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.settingsPane.open(), onCommandClick: () => container.settingsPane.open(),
@@ -179,7 +180,7 @@ export class CommandBarComponentButtonFactory {
if (container.isHostedDataExplorerEnabled()) { if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen"; const label = "Open Full Screen";
const fullScreenButton: ViewModels.NavbarButtonConfig = { const fullScreenButton: CommandButtonComponentProps = {
iconSrc: OpenInTabIcon, iconSrc: OpenInTabIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.generateSharedAccessData(), onCommandClick: () => container.generateSharedAccessData(),
@@ -193,9 +194,9 @@ export class CommandBarComponentButtonFactory {
buttons.push(fullScreenButton); buttons.push(fullScreenButton);
} }
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) { if (configContext.platform !== Platform.Emulator) {
const label = "Feedback"; const label = "Feedback";
const feedbackButtonOptions: ViewModels.NavbarButtonConfig = { const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon, iconSrc: FeedbackIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.provideFeedbackEmail(), onCommandClick: () => container.provideFeedbackEmail(),
@@ -211,7 +212,7 @@ export class CommandBarComponentButtonFactory {
return buttons; return buttons;
} }
public static createDivider(): ViewModels.NavbarButtonConfig { public static createDivider(): CommandButtonComponentProps {
const label = `divider${CommandBarComponentButtonFactory.counter++}`; const label = `divider${CommandBarComponentButtonFactory.counter++}`;
return { return {
isDivider: true, isDivider: true,
@@ -228,7 +229,7 @@ export class CommandBarComponentButtonFactory {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
} }
private static createNewCollectionGroup(container: Explorer): ViewModels.NavbarButtonConfig { private static createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
const label = container.addCollectionText(); const label = container.addCollectionText();
return { return {
iconSrc: AddCollectionIcon, iconSrc: AddCollectionIcon,
@@ -241,10 +242,15 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createOpenSynapseLinkDialogButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (config.platform === Platform.Emulator) { if (configContext.platform === Platform.Emulator) {
return null; return null;
} }
if (container.isServerlessEnabled()) {
return null;
}
if ( if (
container.databaseAccount && container.databaseAccount &&
container.databaseAccount() && container.databaseAccount() &&
@@ -276,7 +282,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createNewDatabase(container: Explorer): ViewModels.NavbarButtonConfig { private static createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = container.addDatabaseText(); const label = container.addDatabaseText();
return { return {
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
@@ -291,7 +297,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createNewSQLQueryButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) { if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
const label = "New SQL Query"; const label = "New SQL Query";
return { return {
@@ -325,15 +331,15 @@ export class CommandBarComponentButtonFactory {
return null; return null;
} }
public static createScriptCommandButtons(container: Explorer): ViewModels.NavbarButtonConfig[] { public static createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: ViewModels.NavbarButtonConfig[] = []; const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean = const shouldEnableScriptsCommands: boolean =
!container.isDatabaseNodeOrNoneSelected() && CommandBarComponentButtonFactory.areScriptsSupported(container); !container.isDatabaseNodeOrNoneSelected() && CommandBarComponentButtonFactory.areScriptsSupported(container);
if (shouldEnableScriptsCommands) { if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
@@ -350,7 +356,7 @@ export class CommandBarComponentButtonFactory {
if (shouldEnableScriptsCommands) { if (shouldEnableScriptsCommands) {
const label = "New UDF"; const label = "New UDF";
const newUserDefinedFunctionBtn: ViewModels.NavbarButtonConfig = { const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon, iconSrc: AddUdfIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
@@ -367,7 +373,7 @@ export class CommandBarComponentButtonFactory {
if (shouldEnableScriptsCommands) { if (shouldEnableScriptsCommands) {
const label = "New Trigger"; const label = "New Trigger";
const newTriggerBtn: ViewModels.NavbarButtonConfig = { const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon, iconSrc: AddTriggerIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
@@ -385,7 +391,7 @@ export class CommandBarComponentButtonFactory {
return buttons; return buttons;
} }
private static createScaleAndSettingsButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
let isShared = false; let isShared = false;
if (container.isDatabaseNodeSelected()) { if (container.isDatabaseNodeSelected()) {
isShared = container.findSelectedDatabase().isDatabaseShared(); isShared = container.findSelectedDatabase().isDatabaseShared();
@@ -410,7 +416,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createNewNotebookButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook"; const label = "New Notebook";
return { return {
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
@@ -423,7 +429,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createuploadNotebookButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server"; const label = "Upload to Notebook Server";
return { return {
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
@@ -436,7 +442,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createOpenQueryButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query"; const label = "Open Query";
return { return {
iconSrc: BrowseQueriesIcon, iconSrc: BrowseQueriesIcon,
@@ -449,7 +455,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createOpenQueryFromDiskButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query From Disk"; const label = "Open Query From Disk";
return { return {
iconSrc: OpenQueryFromDiskIcon, iconSrc: OpenQueryFromDiskIcon,
@@ -462,8 +468,8 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createEnableNotebooksButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (config.platform === Platform.Emulator) { if (configContext.platform === Platform.Emulator) {
return null; return null;
} }
const label = "Enable Notebooks (Preview)"; const label = "Enable Notebooks (Preview)";
@@ -483,7 +489,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createOpenTerminalButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal"; const label = "Open Terminal";
return { return {
iconSrc: CosmosTerminalIcon, iconSrc: CosmosTerminalIcon,
@@ -496,7 +502,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createOpenMongoTerminalButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell"; const label = "Open Mongo Shell";
const tooltip = const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."; "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
@@ -522,7 +528,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createOpenCassandraTerminalButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Cassandra Shell"; const label = "Open Cassandra Shell";
const tooltip = const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."; "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
@@ -548,7 +554,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createNotebookWorkspaceResetButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace"; const label = "Reset Workspace";
return { return {
iconSrc: ResetWorkspaceIcon, iconSrc: ResetWorkspaceIcon,
@@ -561,7 +567,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createManageGitHubAccountButton(container: Explorer): ViewModels.NavbarButtonConfig { private static createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
let connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn(); let connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub"; const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
return { return {
@@ -584,7 +590,7 @@ export class CommandBarComponentButtonFactory {
}; };
} }
private static createStaticCommandBarButtonsForResourceToken(container: Explorer): ViewModels.NavbarButtonConfig[] { private static createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container); const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container);
const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container); const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);

View File

@@ -1,9 +1,10 @@
import { CommandBarUtil } from "./CommandBarUtil"; import { CommandBarUtil } from "./CommandBarUtil";
import * as ViewModels from "../../../Contracts/ViewModels"; 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";
describe("CommandBarUtil tests", () => { describe("CommandBarUtil tests", () => {
const createButton = (): ViewModels.NavbarButtonConfig => { const createButton = (): CommandButtonComponentProps => {
return { return {
iconSrc: "icon", iconSrc: "icon",
iconAlt: "label", iconAlt: "label",
@@ -54,7 +55,7 @@ describe("CommandBarUtil tests", () => {
}); });
it("should create buttons with unique keys", () => { it("should create buttons with unique keys", () => {
const btns: ViewModels.NavbarButtonConfig[] = []; const btns: CommandButtonComponentProps[] = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
btns.push(createButton()); btns.push(createButton());
} }

View File

@@ -1,12 +1,11 @@
import _ from "underscore"; import _ from "underscore";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Observable } from "knockout"; import { Observable } from "knockout";
import { IconType } from "office-ui-fabric-react/lib/Icon"; import { IconType } from "office-ui-fabric-react/lib/Icon";
import { IComponentAsProps } from "office-ui-fabric-react/lib/Utilities"; import { IComponentAsProps } from "office-ui-fabric-react/lib/Utilities";
import { KeyCodes, StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar"; import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { Dropdown, DropdownMenuItemType, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown"; import { Dropdown, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import ChevronDownIcon from "../../../../images/Chevron_down.svg"; import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker"; import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
@@ -21,13 +20,13 @@ export class CommandBarUtil {
* Convert our NavbarButtonConfig to UI Fabric buttons * Convert our NavbarButtonConfig to UI Fabric buttons
* @param btns * @param btns
*/ */
public static convertButton(btns: ViewModels.NavbarButtonConfig[], backgroundColor: string): ICommandBarItemProps[] { public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight; const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
return btns return btns
.filter(btn => btn) .filter(btn => btn)
.map( .map(
(btn: ViewModels.NavbarButtonConfig, index: number): ICommandBarItemProps => { (btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
if (btn.isDivider) { if (btn.isDivider) {
return CommandBarUtil.createDivider(btn.commandButtonLabel); return CommandBarUtil.createDivider(btn.commandButtonLabel);
} }

View File

@@ -3,17 +3,19 @@
*/ */
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import {
import { CommandButtonComponent } from "../../Controls/CommandButton/CommandButtonComponent"; CommandButtonComponent,
CommandButtonComponentProps
} from "../../Controls/CommandButton/CommandButtonComponent";
export interface ControlBarComponentProps { export interface ControlBarComponentProps {
buttons: ViewModels.NavbarButtonConfig[]; buttons: CommandButtonComponentProps[];
} }
export class ControlBarComponent extends React.Component<ControlBarComponentProps> { export class ControlBarComponent extends React.Component<ControlBarComponentProps> {
private static renderButtons(commandButtonOptions: ViewModels.NavbarButtonConfig[]): JSX.Element[] { private static renderButtons(commandButtonOptions: CommandButtonComponentProps[]): JSX.Element[] {
return commandButtonOptions.map( return commandButtonOptions.map(
(btn: ViewModels.NavbarButtonConfig, index: number): JSX.Element => { (btn: CommandButtonComponentProps, index: number): JSX.Element => {
// Remove label // Remove label
btn.commandButtonLabel = null; btn.commandButtonLabel = null;
return CommandButtonComponent.renderButton(btn, `${index}`); return CommandButtonComponent.renderButton(btn, `${index}`);

View File

@@ -8,12 +8,12 @@ import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { ControlBarComponent } from "./ControlBarComponent"; import { ControlBarComponent } from "./ControlBarComponent";
import * as ViewModels from "../../../Contracts/ViewModels"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class ControlBarComponentAdapter implements ReactAdapter { export class ControlBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>; public parameters: ko.Observable<number>;
constructor(private buttons: ko.ObservableArray<ViewModels.NavbarButtonConfig>) { constructor(private buttons: ko.ObservableArray<CommandButtonComponentProps>) {
this.buttons.subscribe(() => this.forceRender()); this.buttons.subscribe(() => this.forceRender());
this.parameters = ko.observable<number>(Date.now()); this.parameters = ko.observable<number>(Date.now());
} }

View File

@@ -8,6 +8,7 @@ import { actions, createContentRef, createKernelRef, selectors } from "@nteract/
import VirtualCommandBarComponent from "./VirtualCommandBarComponent"; import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
import { NotebookContentItem } from "../NotebookContentItem"; import { NotebookContentItem } from "../NotebookContentItem";
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper"; import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
import { CdbAppState } from "./types";
export interface NotebookComponentAdapterOptions { export interface NotebookComponentAdapterOptions {
contentItem: NotebookContentItem; contentItem: NotebookContentItem;
@@ -18,6 +19,7 @@ export interface NotebookComponentAdapterOptions {
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter { export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
private onUpdateKernelInfo: () => void; private onUpdateKernelInfo: () => void;
public getNotebookParentElement: () => HTMLElement;
public parameters: any; public parameters: any;
constructor(options: NotebookComponentAdapterOptions) { constructor(options: NotebookComponentAdapterOptions) {
@@ -44,6 +46,11 @@ export class NotebookComponentAdapter extends NotebookComponentBootstrapper impl
}) })
); );
} }
this.getNotebookParentElement = () => {
const cdbAppState = this.getStore().getState() as CdbAppState;
return cdbAppState.cdb.currentNotebookParentElements.get(this.contentRef);
};
} }
protected renderExtraComponent = (): JSX.Element => { protected renderExtraComponent = (): JSX.Element => {

View File

@@ -17,7 +17,7 @@ import {
} from "@nteract/core"; } from "@nteract/core";
import * as Immutable from "immutable"; import * as Immutable from "immutable";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { CellType, CellId, toJS } from "@nteract/commutable"; import { CellType, CellId, ImmutableNotebook } from "@nteract/commutable";
import { Store, AnyAction } from "redux"; import { Store, AnyAction } from "redux";
import "./NotebookComponent.less"; import "./NotebookComponent.less";
@@ -71,14 +71,14 @@ export class NotebookComponentBootstrapper {
); );
} }
public getContent(): { name: string; content: string } { public getContent(): { name: string; content: string | ImmutableNotebook } {
const record = this.getStore() const record = this.getStore()
.getState() .getState()
.core.entities.contents.byRef.get(this.contentRef); .core.entities.contents.byRef.get(this.contentRef);
let content: string; let content: string | ImmutableNotebook;
switch (record.model.type) { switch (record.model.type) {
case "notebook": case "notebook":
content = JSON.stringify(toJS(record.model.notebook)); content = record.model.notebook;
break; break;
case "file": case "file":
content = record.model.text; content = record.model.text;

View File

@@ -84,3 +84,22 @@ export const traceNotebookTelemetry = (payload: {
payload payload
}; };
}; };
export const UPDATE_NOTEBOOK_PARENT_DOM_ELTS = "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
export interface UpdateNotebookParentDomEltAction {
type: "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
};
}
export const UpdateNotebookParentDomElt = (payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
}): UpdateNotebookParentDomEltAction => {
return {
type: UPDATE_NOTEBOOK_PARENT_DOM_ELTS,
payload
};
};

View File

@@ -34,7 +34,7 @@ import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable"; import { RecordOf } from "immutable";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions"; import * as CdbActions from "./actions";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -82,6 +82,19 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
}); });
return state; return state;
} }
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
var parentEltsMap = state.get("currentNotebookParentElements");
const contentRef = typedAction.payload.contentRef;
const parentElt = typedAction.payload.parentElt;
if (parentElt) {
parentEltsMap.set(contentRef, parentElt);
} else {
parentEltsMap.delete(contentRef);
}
return state.set("currentNotebookParentElements", parentEltsMap);
}
} }
return state; return state;
}; };

View File

@@ -1,5 +1,5 @@
import * as Immutable from "immutable"; import * as Immutable from "immutable";
import { AppState } from "@nteract/core"; import { AppState, ContentRef } from "@nteract/core";
import { Notebook } from "../../../Common/Constants"; import { Notebook } from "../../../Common/Constants";
import { CellId } from "@nteract/commutable"; import { CellId } from "@nteract/commutable";
@@ -9,6 +9,7 @@ export interface CdbRecordProps {
defaultExperience: string | undefined; defaultExperience: string | undefined;
kernelRestartDelayMs: number; kernelRestartDelayMs: number;
hoveredCellId: CellId | undefined; hoveredCellId: CellId | undefined;
currentNotebookParentElements: Map<ContentRef, HTMLElement>;
} }
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>; export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
@@ -21,5 +22,6 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
databaseAccountName: undefined, databaseAccountName: undefined,
defaultExperience: undefined, defaultExperience: undefined,
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs, kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
hoveredCellId: undefined hoveredCellId: undefined,
currentNotebookParentElements: new Map<ContentRef, HTMLElement>()
}); });

View File

@@ -2,13 +2,12 @@
* Notebook container related stuff * Notebook container related stuff
*/ */
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
export class NotebookContainerClient implements ViewModels.INotebookContainerClient { export class NotebookContainerClient {
private reconnectingNotificationId: string; private reconnectingNotificationId: string;
private isResettingWorkspace: boolean; private isResettingWorkspace: boolean;

View File

@@ -1,5 +1,4 @@
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { StringUtils } from "../../Utils/StringUtils"; import { StringUtils } from "../../Utils/StringUtils";
import { FileSystemUtil } from "./FileSystemUtil"; import { FileSystemUtil } from "./FileSystemUtil";
@@ -9,7 +8,7 @@ import { ServerConfig, IContent, IContentProvider, FileType, IEmptyContent } fro
import { AjaxResponse } from "rxjs/ajax"; import { AjaxResponse } from "rxjs/ajax";
import { stringifyNotebook } from "@nteract/commutable"; import { stringifyNotebook } from "@nteract/commutable";
export class NotebookContentClient implements ViewModels.INotebookContentClient { export class NotebookContentClient {
constructor( constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>, private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
private notebookBasePath: ko.Observable<string>, private notebookBasePath: ko.Observable<string>,
@@ -90,7 +89,7 @@ export class NotebookContentClient implements ViewModels.INotebookContentClient
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
} }
const filepath = `${parent.path}/${name}`; const filepath = NotebookUtil.getFilePath(parent.path, name);
if (await this.checkIfFilepathExists(filepath)) { if (await this.checkIfFilepathExists(filepath)) {
throw new Error(`File already exists: ${filepath}`); throw new Error(`File already exists: ${filepath}`);
} }
@@ -117,12 +116,7 @@ export class NotebookContentClient implements ViewModels.INotebookContentClient
} }
private async checkIfFilepathExists(filepath: string): Promise<boolean> { private async checkIfFilepathExists(filepath: string): Promise<boolean> {
const basename = filepath.split("/").pop(); const parentDirPath = NotebookUtil.getParentPath(filepath);
let parentDirPath = filepath
.split(basename)
.shift()
.replace(/\/$/, ""); // no trailling slash
const items = await this.fetchNotebookFiles(parentDirPath); const items = await this.fetchNotebookFiles(parentDirPath);
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath)); return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
} }

View File

@@ -3,7 +3,6 @@
*/ */
import { JunoClient } from "../../Juno/JunoClient"; import { JunoClient } from "../../Juno/JunoClient";
import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { GitHubClient } from "../../GitHub/GitHubClient"; import { GitHubClient } from "../../GitHub/GitHubClient";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
@@ -23,7 +22,10 @@ import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; 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 Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
@@ -39,15 +41,16 @@ export default class NotebookManager {
public junoClient: JunoClient; public junoClient: JunoClient;
public notebookContentProvider: IContentProvider; public notebookContentProvider: IContentProvider;
public notebookClient: ViewModels.INotebookContainerClient; public notebookClient: NotebookContainerClient;
public notebookContentClient: ViewModels.INotebookContentClient; public notebookContentClient: NotebookContentClient;
private gitHubContentProvider: GitHubContentProvider; private gitHubContentProvider: GitHubContentProvider;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
private gitHubClient: GitHubClient; private gitHubClient: GitHubClient;
public gitHubReposPane: ViewModels.ContextualPane; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter; public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
public initialize(params: NotebookManagerOptions): void { public initialize(params: NotebookManagerOptions): void {
this.params = params; this.params = params;
@@ -56,7 +59,6 @@ export default class NotebookManager {
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient); this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
this.gitHubClient = new GitHubClient(this.onGitHubClientError); this.gitHubClient = new GitHubClient(this.onGitHubClientError);
this.gitHubReposPane = new GitHubReposPane({ this.gitHubReposPane = new GitHubReposPane({
documentClientUtility: this.params.container.documentClientUtility,
id: "gitHubReposPane", id: "gitHubReposPane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
container: this.params.container, container: this.params.container,
@@ -90,6 +92,12 @@ export default class NotebookManager {
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient); this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
} }
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
this.params.container,
this.junoClient,
this.gitHubOAuthService
);
this.gitHubOAuthService.getTokenObservable().subscribe(token => { this.gitHubOAuthService.getTokenObservable().subscribe(token => {
this.gitHubClient.setToken(token?.access_token); this.gitHubClient.setToken(token?.access_token);
@@ -108,8 +116,29 @@ export default class NotebookManager {
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope); this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
} }
public openPublishNotebookPane(name: string, content: string): void { public refreshPinnedRepos(): void {
this.publishNotebookPaneAdapter.open(name, getFullName(), content); this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
}
public async openPublishNotebookPane(
name: string,
content: string | ImmutableNotebook,
parentDomElement: HTMLElement,
isCodeOfConductEnabled: boolean,
isLinkInjectionEnabled: boolean
): Promise<void> {
await this.publishNotebookPaneAdapter.open(
name,
getFullName(),
content,
parentDomElement,
isCodeOfConductEnabled,
isLinkInjectionEnabled
);
}
public openCopyNotebookPane(name: string, content: string): void {
this.copyNotebookPaneAdapter.open(name, content);
} }
// Octokit's error handler uses any // Octokit's error handler uses any

View File

@@ -49,8 +49,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
editor: { editor: {
codemirror: (props: PassedEditorProps) => codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} /> this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
}, }
prompt: ({ id, contentRef }) => <></>
}} }}
</CodeCell> </CodeCell>
), ),

View File

@@ -30,11 +30,18 @@ import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less"; import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell"; import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler"; import CellLabeler from "./decorators/CellLabeler";
import * as cdbActions from "../NotebookComponent/actions";
export interface NotebookRendererProps { export interface NotebookRendererBaseProps {
contentRef: any; contentRef: any;
} }
interface NotebookRendererDispatchProps {
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => void;
}
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps;
interface PassedEditorProps { interface PassedEditorProps {
id: string; id: string;
contentRef: ContentRef; contentRef: ContentRef;
@@ -68,6 +75,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
}; };
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> { class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
private notebookRendererRef = React.createRef<HTMLDivElement>();
constructor(props: NotebookRendererProps) { constructor(props: NotebookRendererProps) {
super(props); super(props);
@@ -78,13 +87,22 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() { componentDidMount() {
loadTransform(this.props as any); loadTransform(this.props as any);
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentDidUpdate() {
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentWillUnmount() {
this.props.updateNotebookParentDomElt(this.props.contentRef, undefined);
} }
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<div className="NotebookRendererContainer"> <div className="NotebookRendererContainer">
<div className="NotebookRenderer"> <div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}> <KeyboardShortcuts contentRef={this.props.contentRef}>
<Cells contentRef={this.props.contentRef}> <Cells contentRef={this.props.contentRef}>
@@ -146,7 +164,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
} }
} }
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => { const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererBaseProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => { const mapDispatchToProps = (dispatch: Dispatch) => {
return { return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => { addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
@@ -156,6 +174,14 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
component: transform component: transform
}) })
); );
},
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => {
return dispatch(
cdbActions.UpdateNotebookParentDomElt({
contentRef,
parentElt
})
);
} }
}; };
}; };

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