mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-27 04:41:48 +00:00
Compare commits
18 Commits
genereated
...
users/srna
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d45af21996 | ||
|
|
a64109ebaa | ||
|
|
70f9b28499 | ||
|
|
78e70cc7cc | ||
|
|
f132a8546c | ||
|
|
e6acf6686f | ||
|
|
2904a1a60d | ||
|
|
8c792fd147 | ||
|
|
2a53dfabb5 | ||
|
|
14ef40029d | ||
|
|
dab6e43d0d | ||
|
|
aea168c893 | ||
|
|
fea321cd68 | ||
|
|
2e49ed45c3 | ||
|
|
6d142f16f9 | ||
|
|
6dcdacc8c4 | ||
|
|
33969581ac | ||
|
|
dc67c5f40b |
@@ -298,11 +298,9 @@ src/Utils/DatabaseAccountUtils.ts
|
||||
src/Utils/JunoUtils.ts
|
||||
src/Utils/MessageValidation.ts
|
||||
src/Utils/NotebookConfigurationUtils.ts
|
||||
src/Utils/NotificationConsoleUtils.ts
|
||||
src/Utils/OfferUtils.test.ts
|
||||
src/Utils/OfferUtils.ts
|
||||
src/Utils/PricingUtils.test.ts
|
||||
src/Utils/PricingUtils.ts
|
||||
src/Utils/QueryUtils.test.ts
|
||||
src/Utils/QueryUtils.ts
|
||||
src/Utils/StringUtils.test.ts
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
browser: true,
|
||||
es6: true
|
||||
},
|
||||
plugins: ["@typescript-eslint", "no-null"],
|
||||
plugins: ["@typescript-eslint", "no-null", "prefer-arrow"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
@@ -40,6 +40,7 @@ module.exports = {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "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
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @Azure/cosmos-explorer-owners
|
||||
* @Azure/cosmos-explorer-owners @Azure/azure-cosmos-explorer-developers
|
||||
|
||||
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -1,9 +1,13 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches:
|
||||
- master
|
||||
- hotfix/*
|
||||
- release/*
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -52,6 +56,7 @@ jobs:
|
||||
- run: npm run test
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, format, compile, unittest]
|
||||
name: "Build"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -75,6 +80,7 @@ jobs:
|
||||
path: dist/
|
||||
endtoendemulator:
|
||||
name: "End To End Tests | Emulator | SQL"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -101,6 +107,7 @@ jobs:
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
endtoendsql:
|
||||
name: "End To End Tests | SQL"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -129,6 +136,7 @@ jobs:
|
||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
endtoendmongo:
|
||||
name: "End To End Tests | Mongo"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -157,6 +165,7 @@ jobs:
|
||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
accessibility:
|
||||
name: "Accessibility | Hosted"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -179,7 +188,7 @@ jobs:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
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]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -203,7 +212,7 @@ jobs:
|
||||
path: "*.nupkg"
|
||||
nugetmpac:
|
||||
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]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
@@ -39,10 +39,10 @@ module.exports = {
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 18,
|
||||
functions: 22,
|
||||
lines: 28,
|
||||
statements: 27
|
||||
branches: 20,
|
||||
functions: 24,
|
||||
lines: 30,
|
||||
statements: 29.0
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
219
package-lock.json
generated
219
package-lock.json
generated
@@ -6024,6 +6024,16 @@
|
||||
"natural-compare": "^1.4.0",
|
||||
"pretty-format": "^24.9.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": {
|
||||
@@ -7577,11 +7587,40 @@
|
||||
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
|
||||
"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": {
|
||||
"version": "12.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz",
|
||||
"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": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||
@@ -7681,6 +7720,11 @@
|
||||
"@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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz",
|
||||
@@ -8403,7 +8447,6 @@
|
||||
"graceful-fs": "^4.1.11",
|
||||
"lru-cache": "^4.1.1",
|
||||
"mississippi": "^2.0.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"move-concurrently": "^1.0.1",
|
||||
"promise-inflight": "^1.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
@@ -9342,6 +9385,15 @@
|
||||
"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": {
|
||||
"version": "4.0.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
@@ -10656,6 +10713,14 @@
|
||||
"run-queue": "^1.0.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"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz",
|
||||
@@ -12744,6 +12817,12 @@
|
||||
"integrity": "sha1-EjaoEjkTkKGHetQAfCbnRTQclR8=",
|
||||
"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": {
|
||||
"version": "7.20.0",
|
||||
"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": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||
@@ -19754,6 +19841,16 @@
|
||||
"mkdirp": "^0.5.1",
|
||||
"slash": "^2.0.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": {
|
||||
@@ -20337,6 +20434,16 @@
|
||||
"dev": 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": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
@@ -21287,12 +21394,9 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
@@ -21338,6 +21442,14 @@
|
||||
"run-queue": "^1.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"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
@@ -21677,6 +21789,14 @@
|
||||
"tar": "^4"
|
||||
},
|
||||
"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": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
@@ -22171,11 +22291,11 @@
|
||||
"integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo="
|
||||
},
|
||||
"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,
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz",
|
||||
"integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==",
|
||||
"requires": {
|
||||
"@types/retry": "^0.12.0",
|
||||
"retry": "^0.12.0"
|
||||
}
|
||||
},
|
||||
@@ -22576,6 +22696,15 @@
|
||||
"requires": {
|
||||
"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": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
|
||||
"dev": true
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
|
||||
},
|
||||
"reusify": {
|
||||
"version": "1.0.4",
|
||||
@@ -25476,6 +25604,16 @@
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"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": {
|
||||
@@ -25833,6 +25971,14 @@
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
|
||||
"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": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
|
||||
@@ -26901,6 +27047,15 @@
|
||||
"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": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
@@ -27012,6 +27167,15 @@
|
||||
"integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==",
|
||||
"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": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
|
||||
@@ -27224,6 +27388,15 @@
|
||||
"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": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
@@ -27303,6 +27476,15 @@
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
|
||||
@@ -27531,6 +27713,17 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"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": {
|
||||
|
||||
10
package.json
10
package.json
@@ -34,6 +34,8 @@
|
||||
"@nteract/transform-vega": "7.0.6",
|
||||
"@octokit/rest": "17.9.2",
|
||||
"@phosphor/widgets": "1.9.3",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"@uifabric/react-cards": "0.109.110",
|
||||
"@uifabric/styling": "7.13.7",
|
||||
"abort-controller": "3.0.0",
|
||||
@@ -54,15 +56,18 @@
|
||||
"es6-symbol": "3.1.3",
|
||||
"eslint-plugin-jest": "23.13.2",
|
||||
"hasher": "1.2.0",
|
||||
"html2canvas": "1.0.0-rc.5",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"is-ci": "2.0.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery-typeahead": "2.10.6",
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"knockout": "3.5.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.15.6",
|
||||
"object.entries": "1.1.0",
|
||||
"office-ui-fabric-react": "7.121.10",
|
||||
"p-retry": "4.2.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"promise-polyfill": "8.1.0",
|
||||
"promise.prototype.finally": "3.1.0",
|
||||
@@ -130,6 +135,7 @@
|
||||
"eslint": "7.3.1",
|
||||
"eslint-cli": "1.1.1",
|
||||
"eslint-plugin-no-null": "1.0.2",
|
||||
"eslint-plugin-prefer-arrow": "1.2.2",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"expose-loader": "0.7.5",
|
||||
"file-loader": "2.0.0",
|
||||
@@ -147,6 +153,7 @@
|
||||
"less-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "0.4.3",
|
||||
"monaco-editor-webpack-plugin": "1.7.0",
|
||||
"node-fetch": "2.6.0",
|
||||
"prettier": "1.19.1",
|
||||
"puppeteer": "4.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
@@ -187,7 +194,8 @@
|
||||
"build:contracts": "npm run compile:contracts",
|
||||
"strictEligibleFiles": "node ./strict-migration-tools/index.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": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
}
|
||||
@@ -101,6 +101,7 @@ export class CapabilityNames {
|
||||
public static readonly EnableNotebooks: string = "EnableNotebooks";
|
||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
public static readonly EnableServerless: string = "EnableServerless";
|
||||
}
|
||||
|
||||
export class Features {
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as Cosmos from "@azure/cosmos";
|
||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { HttpHeaders, EmulatorMasterKey } from "./Constants";
|
||||
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { config, Platform } from "../Config";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,69 @@
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
|
||||
export function replaceKnownError(err: string): string {
|
||||
if (
|
||||
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
|
||||
err.indexOf("SharedOffer is Disabled for your account") >= 0
|
||||
) {
|
||||
return "Database throughput is not supported for internal subscriptions.";
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
export function parse(err: any): DataModels.ErrorDataModel[] {
|
||||
try {
|
||||
return _parse(err);
|
||||
} catch (e) {
|
||||
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
|
||||
}
|
||||
}
|
||||
|
||||
function _parse(err: any): DataModels.ErrorDataModel[] {
|
||||
var normalizedErrors: DataModels.ErrorDataModel[] = [];
|
||||
if (err.message && !err.code) {
|
||||
normalizedErrors.push(err);
|
||||
} else {
|
||||
const innerErrors: any[] = _getInnerErrors(err.message);
|
||||
normalizedErrors = innerErrors.map(innerError =>
|
||||
typeof innerError === "string" ? { message: innerError } : innerError
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedErrors;
|
||||
}
|
||||
|
||||
function _getInnerErrors(message: string): any[] {
|
||||
/*
|
||||
The backend error message has an inner-message which is a stringified object.
|
||||
|
||||
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
||||
Example:
|
||||
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
||||
For non-SQL errors the "Errors" propery is an array of string.
|
||||
Example:
|
||||
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
||||
*/
|
||||
|
||||
let innerMessage: any = null;
|
||||
|
||||
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
||||
try {
|
||||
// Multi-Partition error flavor
|
||||
const regExp = /^(.*)ActivityId: (.*)/g;
|
||||
const regString = regExp.exec(singleLineMessage);
|
||||
const innerMessageString = regString[1];
|
||||
innerMessage = JSON.parse(innerMessageString);
|
||||
} catch (e) {
|
||||
// Single-partition error flavor
|
||||
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
||||
const regString = regExp.exec(singleLineMessage);
|
||||
const innerMessageString = regString[1];
|
||||
innerMessage = JSON.parse(innerMessageString);
|
||||
}
|
||||
|
||||
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
||||
}
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
|
||||
export function replaceKnownError(err: string): string {
|
||||
if (
|
||||
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
|
||||
err.indexOf("SharedOffer is Disabled for your account") >= 0
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function parse(err: any): DataModels.ErrorDataModel[] {
|
||||
try {
|
||||
return _parse(err);
|
||||
} catch (e) {
|
||||
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
|
||||
}
|
||||
}
|
||||
|
||||
function _parse(err: any): DataModels.ErrorDataModel[] {
|
||||
var normalizedErrors: DataModels.ErrorDataModel[] = [];
|
||||
if (err.message && !err.code) {
|
||||
normalizedErrors.push(err);
|
||||
} else {
|
||||
const innerErrors: any[] = _getInnerErrors(err.message);
|
||||
normalizedErrors = innerErrors.map(innerError =>
|
||||
typeof innerError === "string" ? { message: innerError } : innerError
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedErrors;
|
||||
}
|
||||
|
||||
function _getInnerErrors(message: string): any[] {
|
||||
/*
|
||||
The backend error message has an inner-message which is a stringified object.
|
||||
|
||||
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
||||
Example:
|
||||
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
||||
For non-SQL errors the "Errors" propery is an array of string.
|
||||
Example:
|
||||
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
||||
*/
|
||||
|
||||
let innerMessage: any = null;
|
||||
|
||||
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
||||
try {
|
||||
// Multi-Partition error flavor
|
||||
const regExp = /^(.*)ActivityId: (.*)/g;
|
||||
const regString = regExp.exec(singleLineMessage);
|
||||
const innerMessageString = regString[1];
|
||||
innerMessage = JSON.parse(innerMessageString);
|
||||
} catch (e) {
|
||||
// Single-partition error flavor
|
||||
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
||||
const regString = regExp.exec(singleLineMessage);
|
||||
const innerMessageString = regString[1];
|
||||
innerMessage = JSON.parse(innerMessageString);
|
||||
}
|
||||
|
||||
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,26 @@
|
||||
jest.mock("./MessageHandler");
|
||||
import { LogEntryLevel } from "../Contracts/Diagnostics";
|
||||
import * as Logger from "./Logger";
|
||||
import { MessageHandler } from "./MessageHandler";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
describe("Logger", () => {
|
||||
let sendMessageSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
sendMessageSpy = spyOn(MessageHandler, "sendMessage");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sendMessageSpy = null;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should log info messages", () => {
|
||||
Logger.logInfo("Test info", "DocDB");
|
||||
const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
|
||||
|
||||
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
|
||||
expect(spyArgs.data).toContain(LogEntryLevel.Verbose);
|
||||
expect(spyArgs.data).toContain("DocDB");
|
||||
expect(spyArgs.data).toContain("Test info");
|
||||
expect(sendMessage).toBeCalled();
|
||||
});
|
||||
|
||||
it("should log error messages", () => {
|
||||
Logger.logError("Test error", "DocDB");
|
||||
const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
|
||||
|
||||
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
|
||||
expect(spyArgs.data).toContain(LogEntryLevel.Error);
|
||||
expect(spyArgs.data).toContain("DocDB");
|
||||
expect(spyArgs.data).toContain("Test error");
|
||||
expect(sendMessage).toBeCalled();
|
||||
});
|
||||
|
||||
it("should log warnings", () => {
|
||||
Logger.logWarning("Test warning", "DocDB");
|
||||
const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
|
||||
|
||||
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
|
||||
expect(spyArgs.data).toContain(LogEntryLevel.Warning);
|
||||
expect(spyArgs.data).toContain("DocDB");
|
||||
expect(spyArgs.data).toContain("Test warning");
|
||||
expect(sendMessage).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MessageHandler } from "./MessageHandler";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { appInsights } from "../Shared/appInsights";
|
||||
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 {
|
||||
MessageHandler.sendMessage({
|
||||
sendMessage({
|
||||
type: MessageTypes.LogInfo,
|
||||
data: JSON.stringify(entry)
|
||||
});
|
||||
|
||||
@@ -1,65 +1,29 @@
|
||||
import Q from "q";
|
||||
import { CachedDataPromise, MessageHandler } from "./MessageHandler";
|
||||
import * as MessageHandler from "./MessageHandler";
|
||||
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", () => {
|
||||
beforeEach(() => {
|
||||
MockMessageHandler.clearAllEntries();
|
||||
});
|
||||
|
||||
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> = {
|
||||
it("should handle cached message", async () => {
|
||||
let mockPromise = {
|
||||
id: "123",
|
||||
startTime: new Date(),
|
||||
deferred: Q.defer<any>()
|
||||
};
|
||||
let mockMessage = { message: { id: "123", data: "{}" } };
|
||||
|
||||
MockMessageHandler.addToMap(mockPromise.id, mockPromise);
|
||||
MockMessageHandler.handleCachedDataMessage(mockMessage);
|
||||
MessageHandler.RequestMap[mockPromise.id] = mockPromise;
|
||||
MessageHandler.handleCachedDataMessage(mockMessage);
|
||||
expect(mockPromise.deferred.promise.isFulfilled()).toBe(true);
|
||||
});
|
||||
|
||||
it("should delete fulfilled promises on running the garbage collector", () => {
|
||||
let mockPromise: CachedDataPromise<any> = {
|
||||
it("should delete fulfilled promises on running the garbage collector", async () => {
|
||||
let message = {
|
||||
id: "123",
|
||||
startTime: new Date(),
|
||||
deferred: Q.defer<any>()
|
||||
};
|
||||
|
||||
MockMessageHandler.addToMap(mockPromise.id, mockPromise);
|
||||
mockPromise.deferred.reject("some error");
|
||||
MockMessageHandler.runGarbageCollector();
|
||||
expect(MockMessageHandler.mapContainsKey(mockPromise.id)).toBe(false);
|
||||
MessageHandler.handleCachedDataMessage(message);
|
||||
MessageHandler.runGarbageCollector();
|
||||
expect(MessageHandler.RequestMap["123"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,77 +9,65 @@ export interface CachedDataPromise<T> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 const RequestMap: Record<string, CachedDataPromise<any>> = {};
|
||||
|
||||
export class MessageHandler {
|
||||
protected static RequestMap: Map = {};
|
||||
|
||||
public static handleCachedDataMessage(message: any): void {
|
||||
const messageContent = message && message.message;
|
||||
if (
|
||||
message == null ||
|
||||
messageContent == null ||
|
||||
messageContent.id == null ||
|
||||
!MessageHandler.RequestMap[messageContent.id]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedDataPromise = MessageHandler.RequestMap[messageContent.id];
|
||||
if (messageContent.error != null) {
|
||||
cachedDataPromise.deferred.reject(messageContent.error);
|
||||
} else {
|
||||
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
|
||||
}
|
||||
MessageHandler.runGarbageCollector();
|
||||
export function handleCachedDataMessage(message: any): void {
|
||||
const messageContent = message && message.message;
|
||||
if (message == null || messageContent == null || messageContent.id == null || !RequestMap[messageContent.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
public static sendCachedDataMessage<TResponseDataModel>(
|
||||
messageType: MessageTypes,
|
||||
params: Object[],
|
||||
timeoutInMs?: number
|
||||
): Q.Promise<TResponseDataModel> {
|
||||
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
||||
deferred: Q.defer<TResponseDataModel>(),
|
||||
startTime: new Date(),
|
||||
id: _.uniqueId()
|
||||
};
|
||||
MessageHandler.RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
||||
MessageHandler.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||
const cachedDataPromise = RequestMap[messageContent.id];
|
||||
if (messageContent.error != null) {
|
||||
cachedDataPromise.deferred.reject(messageContent.error);
|
||||
} else {
|
||||
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
|
||||
}
|
||||
runGarbageCollector();
|
||||
}
|
||||
|
||||
//TODO: Use telemetry to measure optimal time to resolve/reject promises
|
||||
return cachedDataPromise.deferred.promise.timeout(
|
||||
timeoutInMs || Constants.ClientDefaults.requestTimeoutMs,
|
||||
"Timed out while waiting for response from portal"
|
||||
export function sendCachedDataMessage<TResponseDataModel>(
|
||||
messageType: MessageTypes,
|
||||
params: Object[],
|
||||
timeoutInMs?: number
|
||||
): Q.Promise<TResponseDataModel> {
|
||||
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
||||
deferred: Q.defer<TResponseDataModel>(),
|
||||
startTime: new Date(),
|
||||
id: _.uniqueId()
|
||||
};
|
||||
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
||||
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||
|
||||
//TODO: Use telemetry to measure optimal time to resolve/reject promises
|
||||
return cachedDataPromise.deferred.promise.timeout(
|
||||
timeoutInMs || Constants.ClientDefaults.requestTimeoutMs,
|
||||
"Timed out while waiting for response from portal"
|
||||
);
|
||||
}
|
||||
|
||||
export function sendMessage(data: any): void {
|
||||
if (canSendMessage()) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
signature: "pcIframe",
|
||||
data: data
|
||||
},
|
||||
window.document.referrer
|
||||
);
|
||||
}
|
||||
|
||||
public static sendMessage(data: any): void {
|
||||
if (MessageHandler.canSendMessage()) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
signature: "pcIframe",
|
||||
data: data
|
||||
},
|
||||
window.document.referrer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static canSendMessage(): boolean {
|
||||
return window.parent !== window;
|
||||
}
|
||||
|
||||
protected static runGarbageCollector() {
|
||||
Object.keys(MessageHandler.RequestMap).forEach((key: string) => {
|
||||
const promise: Q.Promise<any> = MessageHandler.RequestMap[key].deferred.promise;
|
||||
if (promise.isFulfilled() || promise.isRejected()) {
|
||||
delete MessageHandler.RequestMap[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function canSendMessage(): boolean {
|
||||
return window.parent !== window;
|
||||
}
|
||||
|
||||
// TODO: This is exported just for testing. It should not be.
|
||||
export function runGarbageCollector() {
|
||||
Object.keys(RequestMap).forEach((key: string) => {
|
||||
const promise: Q.Promise<any> = RequestMap[key].deferred.promise;
|
||||
if (promise.isFulfilled() || promise.isRejected()) {
|
||||
delete RequestMap[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
updateDocument
|
||||
} from "./MongoProxyClient";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { Collection, DatabaseAccount, DocumentId } from "../Contracts/ViewModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import { config } from "../Config";
|
||||
import { CosmosClient } from "./CosmosClient";
|
||||
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
||||
|
||||
const databaseId = "testDB";
|
||||
@@ -171,7 +173,7 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("builds the correct URL", () => {
|
||||
updateDocument(databaseId, collection, documentId, {});
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
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",
|
||||
expect.any(Object)
|
||||
@@ -180,7 +182,7 @@ describe("MongoProxyClient", () => {
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
|
||||
updateDocument(databaseId, collection, documentId, {});
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
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",
|
||||
expect.any(Object)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as DataExplorerConstants from "../Common/Constants";
|
||||
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";
|
||||
@@ -12,11 +11,12 @@ 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 { sendMessage } from "./MessageHandler";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
|
||||
const defaultHeaders = {
|
||||
[HttpHeaders.apiType]: ApiType.MongoDB.toString(),
|
||||
@@ -123,7 +123,7 @@ export function queryDocuments(
|
||||
export function readDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: ViewModels.DocumentId
|
||||
documentId: DocumentId
|
||||
): Promise<DataModels.DocumentId> {
|
||||
const databaseAccount = CosmosClient.databaseAccount();
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
@@ -205,8 +205,8 @@ export function createDocument(
|
||||
export function updateDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: ViewModels.DocumentId,
|
||||
documentContent: unknown
|
||||
documentId: DocumentId,
|
||||
documentContent: string
|
||||
): Promise<DataModels.DocumentId> {
|
||||
const databaseAccount = CosmosClient.databaseAccount();
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
@@ -230,7 +230,7 @@ export function updateDocument(
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(documentContent),
|
||||
body: documentContent,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
@@ -246,11 +246,7 @@ export function updateDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: ViewModels.DocumentId
|
||||
): Promise<void> {
|
||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||
const databaseAccount = CosmosClient.databaseAccount();
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@@ -385,7 +381,7 @@ export function createMongoCollectionWithARM(
|
||||
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 extensionEndpoint = window.dataExplorer.extensionEndpoint();
|
||||
let url = config.MONGO_BACKEND_ENDPOINT
|
||||
@@ -408,7 +404,7 @@ async function errorHandling(response: Response, action: string, params: unknown
|
||||
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
|
||||
);
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||
return;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { CosmosClient } from "./CosmosClient";
|
||||
|
||||
export class NotificationsClientBase implements ViewModels.NotificationsClient {
|
||||
export class NotificationsClientBase {
|
||||
private _extensionEndpoint: string;
|
||||
private _notificationsApiSuffix: string;
|
||||
|
||||
@@ -16,7 +16,7 @@ export class NotificationsClientBase implements ViewModels.NotificationsClient {
|
||||
|
||||
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
|
||||
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
|
||||
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
|
||||
const 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}`;
|
||||
|
||||
@@ -8,11 +8,19 @@ import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/Notificat
|
||||
import { CosmosClient } from "./CosmosClient";
|
||||
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import * as Logger from "./Logger";
|
||||
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { QueryUtils } from "../Utils/QueryUtils";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import {
|
||||
getOrCreateDatabaseAndCollection,
|
||||
createDocument,
|
||||
queryDocuments,
|
||||
queryDocumentsPage,
|
||||
deleteDocument
|
||||
} from "./DocumentClientUtilityBase";
|
||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||
|
||||
export class QueriesClient implements ViewModels.QueriesClient {
|
||||
export class QueriesClient {
|
||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||
paths: [`/${SavedQueries.PartitionKeyProperty}`],
|
||||
kind: BackendDefaults.partitionKeyKind,
|
||||
@@ -33,14 +41,13 @@ export class QueriesClient implements ViewModels.QueriesClient {
|
||||
ConsoleDataType.InProgress,
|
||||
"Setting up account for saving queries"
|
||||
);
|
||||
return this.container.documentClientUtility
|
||||
.getOrCreateDatabaseAndCollection({
|
||||
collectionId: SavedQueries.CollectionName,
|
||||
databaseId: SavedQueries.DatabaseName,
|
||||
partitionKey: QueriesClient.PartitionKey,
|
||||
offerThroughput: SavedQueries.OfferThroughput,
|
||||
databaseLevelThroughput: undefined
|
||||
})
|
||||
return getOrCreateDatabaseAndCollection({
|
||||
collectionId: SavedQueries.CollectionName,
|
||||
databaseId: SavedQueries.DatabaseName,
|
||||
partitionKey: QueriesClient.PartitionKey,
|
||||
offerThroughput: SavedQueries.OfferThroughput,
|
||||
databaseLevelThroughput: undefined
|
||||
})
|
||||
.then(
|
||||
(collection: DataModels.Collection) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
@@ -89,8 +96,7 @@ export class QueriesClient implements ViewModels.QueriesClient {
|
||||
`Saving query ${query.queryName}`
|
||||
);
|
||||
query.id = query.queryName;
|
||||
return this.container.documentClientUtility
|
||||
.createDocument(queriesCollection, query)
|
||||
return createDocument(queriesCollection, query)
|
||||
.then(
|
||||
(savedQuery: DataModels.Query) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
@@ -131,17 +137,11 @@ export class QueriesClient implements ViewModels.QueriesClient {
|
||||
|
||||
const options: any = { enableCrossPartitionQuery: true };
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
|
||||
return this.container.documentClientUtility
|
||||
.queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
|
||||
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
|
||||
.then(
|
||||
(queryIterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
|
||||
this.container.documentClientUtility.queryDocumentsPage(
|
||||
queriesCollection.id(),
|
||||
queryIterator,
|
||||
firstItemIndex,
|
||||
options
|
||||
);
|
||||
queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
|
||||
return QueryUtils.queryAllPages(fetchQueries).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
|
||||
@@ -217,17 +217,16 @@ export class QueriesClient implements ViewModels.QueriesClient {
|
||||
`Deleting query ${query.queryName}`
|
||||
);
|
||||
query.id = query.queryName;
|
||||
const documentId: ViewModels.DocumentId = new DocumentId(
|
||||
const documentId = new DocumentId(
|
||||
{
|
||||
partitionKey: QueriesClient.PartitionKey,
|
||||
partitionKeyProperty: "id"
|
||||
} as ViewModels.DocumentsTab,
|
||||
} as DocumentsTab,
|
||||
query,
|
||||
query.queryName
|
||||
); // TODO: Remove DocumentId's dependency on DocumentsTab
|
||||
const options: any = { partitionKey: query.resourceId };
|
||||
return this.container.documentClientUtility
|
||||
.deleteDocument(queriesCollection, documentId)
|
||||
return deleteDocument(queriesCollection, documentId)
|
||||
.then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
@@ -250,7 +249,7 @@ export class QueriesClient implements ViewModels.QueriesClient {
|
||||
}
|
||||
|
||||
public getResourceId(): string {
|
||||
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
|
||||
const databaseAccount = CosmosClient.databaseAccount();
|
||||
const databaseAccountName: string = (databaseAccount && databaseAccount.name) || "";
|
||||
const subscriptionId: string = CosmosClient.subscriptionId() || "";
|
||||
const resourceGroup: string = CosmosClient.resourceGroup() || "";
|
||||
|
||||
16
src/Common/dataAccess/deleteCollection.test.ts
Normal file
16
src/Common/dataAccess/deleteCollection.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../MessageHandler");
|
||||
import { deleteCollection } from "./deleteCollection";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { sendCachedDataMessage } from "../MessageHandler";
|
||||
|
||||
describe("deleteCollection", () => {
|
||||
it("should call ARM if logged in with AAD", async () => {
|
||||
window.authType = AuthType.AAD;
|
||||
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
||||
await deleteCollection("database", "collection");
|
||||
expect(armRequest).toHaveBeenCalled();
|
||||
});
|
||||
// TODO: Test non-AAD case
|
||||
});
|
||||
31
src/Common/dataAccess/deleteCollection.ts
Normal file
31
src/Common/dataAccess/deleteCollection.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { CosmosClient } from "../CosmosClient";
|
||||
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
||||
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
|
||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD) {
|
||||
await deleteSqlContainer(
|
||||
CosmosClient.subscriptionId(),
|
||||
CosmosClient.resourceGroup(),
|
||||
CosmosClient.databaseAccount().name,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
} else {
|
||||
await CosmosClient.client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.delete();
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
|
||||
throw error;
|
||||
}
|
||||
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
||||
clearMessage();
|
||||
await refreshCachedResources();
|
||||
}
|
||||
16
src/Common/dataAccess/deleteDatabase.test.ts
Normal file
16
src/Common/dataAccess/deleteDatabase.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../MessageHandler");
|
||||
import { deleteDatabase } from "./deleteDatabase";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { sendCachedDataMessage } from "../MessageHandler";
|
||||
|
||||
describe("deleteDatabase", () => {
|
||||
it("should call ARM if logged in with AAD", async () => {
|
||||
window.authType = AuthType.AAD;
|
||||
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
||||
await deleteDatabase("database");
|
||||
expect(armRequest).toHaveBeenCalled();
|
||||
});
|
||||
// TODO: Test non-AAD case
|
||||
});
|
||||
34
src/Common/dataAccess/deleteDatabase.ts
Normal file
34
src/Common/dataAccess/deleteDatabase.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { CosmosClient } from "../CosmosClient";
|
||||
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
||||
import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { AuthType } from "../../AuthType";
|
||||
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 deleteSqlDatabase(
|
||||
CosmosClient.subscriptionId(),
|
||||
CosmosClient.resourceGroup(),
|
||||
CosmosClient.databaseAccount().name,
|
||||
databaseId
|
||||
);
|
||||
} else {
|
||||
await CosmosClient.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();
|
||||
}
|
||||
20
src/Common/dataAccess/sendNotificationForError.ts
Normal file
20
src/Common/dataAccess/sendNotificationForError.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,16 @@
|
||||
import * as DataModels from "./DataModels";
|
||||
import * as monaco from "monaco-editor";
|
||||
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase";
|
||||
import Q from "q";
|
||||
import { AccessibleVerticalList } from "../Explorer/Tree/AccessibleVerticalList";
|
||||
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
|
||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
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 { UploadDetails } from "../workers/upload/definitions";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import ConflictsTab from "../Explorer/Tabs/ConflictsTab";
|
||||
import Trigger from "../Explorer/Tree/Trigger";
|
||||
|
||||
export interface ExplorerOptions {
|
||||
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;
|
||||
}
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
|
||||
export interface TokenProvider {
|
||||
getAuthHeader(): Promise<Headers>;
|
||||
@@ -75,11 +50,6 @@ export interface WaitsForTemplate {
|
||||
isTemplateReady: ko.Observable<boolean>;
|
||||
}
|
||||
|
||||
export interface AdHocAccessData {
|
||||
readWriteUrl: string;
|
||||
readUrl: string;
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
nodeKind: string;
|
||||
rid: string;
|
||||
@@ -201,118 +171,15 @@ export interface Collection extends CollectionBase {
|
||||
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
|
||||
*/
|
||||
export interface PaneOptions {
|
||||
id: string;
|
||||
documentClientUtility: DocumentClientUtilityBase;
|
||||
visible: ko.Observable<boolean>;
|
||||
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
|
||||
*/
|
||||
@@ -382,19 +249,6 @@ export interface DocumentRequestContainer {
|
||||
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 {
|
||||
endpoint?: string;
|
||||
masterKey?: string;
|
||||
@@ -406,11 +260,10 @@ export interface TabOptions {
|
||||
tabKind: CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
documentClientUtility: DocumentClientUtilityBase;
|
||||
selfLink: string;
|
||||
isActive: ko.Observable<boolean>;
|
||||
hashLocation: string;
|
||||
onUpdateTabsButtons: (buttons: NavbarButtonConfig[]) => void;
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
|
||||
isTabsContentExpanded?: ko.Observable<boolean>;
|
||||
onLoadStartKey?: number;
|
||||
|
||||
@@ -423,47 +276,6 @@ export interface TabOptions {
|
||||
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 {
|
||||
partitionKey: DataModels.PartitionKey;
|
||||
documentIds: ko.ObservableArray<DocumentId>;
|
||||
@@ -491,157 +303,15 @@ export interface ScriptTabOption extends TabOptions {
|
||||
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 {
|
||||
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 {
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface MongoShellTab extends Tab {}
|
||||
|
||||
export enum DocumentExplorerState {
|
||||
noDocumentSelected,
|
||||
newDocumentValid,
|
||||
@@ -759,40 +429,8 @@ export interface AuthorizationTokenHeaderMetadata {
|
||||
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> {
|
||||
text: string;
|
||||
value: T;
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
PortalTheme
|
||||
} from "./HeatmapDatatypes";
|
||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||
import { MessageHandler } from "../../Common/MessageHandler";
|
||||
import { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler";
|
||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||
import { StyleConstants } from "../../Common/Constants";
|
||||
import "./Heatmap.less";
|
||||
@@ -209,7 +209,7 @@ export class Heatmap {
|
||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
||||
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);
|
||||
MessageHandler.sendMessage("ready");
|
||||
sendMessage("ready");
|
||||
|
||||
@@ -13,9 +13,7 @@ import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponen
|
||||
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
|
||||
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
|
||||
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("new-vertex-form", NewVertexComponent);
|
||||
ko.components.register("error-display", new ErrorDisplayComponent());
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { StringUtils } from "../../../Utils/StringUtils";
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface GalleryCardComponentProps {
|
||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
||||
public static readonly CARD_WIDTH = 256;
|
||||
private static readonly cardImageHeight = 144;
|
||||
public static readonly cardHeightToWidthRatio =
|
||||
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
|
||||
private static readonly cardDescriptionMaxChars = 88;
|
||||
private static readonly cardItemGapBig = 10;
|
||||
private static readonly cardItemGapSmall = 8;
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as React from "react";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as Logger from "../../../Common/Logger";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
@@ -19,6 +19,7 @@ import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComp
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
import Explorer from "../../Explorer";
|
||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||
|
||||
export interface NotebookViewerComponentProps {
|
||||
container?: Explorer;
|
||||
@@ -88,13 +89,13 @@ export class NotebookViewerComponent extends React.Component<
|
||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||
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);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} while increasing notebook views`);
|
||||
}
|
||||
|
||||
this.setState({ galleryItem: response.data });
|
||||
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ showProgressBar: false });
|
||||
|
||||
@@ -27,9 +27,10 @@ import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/l
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
|
||||
export interface QueriesGridComponentProps {
|
||||
queriesClient: ViewModels.QueriesClient;
|
||||
queriesClient: QueriesClient;
|
||||
onQuerySelect: (query: DataModels.Query) => void;
|
||||
containerVisible: boolean;
|
||||
saveQueryEnabled: boolean;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,10 +0,0 @@
|
||||
/*!---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*----------------------------------------------------------*/
|
||||
|
||||
interface IToolbarSeperator {
|
||||
type: "separator";
|
||||
visible: ko.Observable<boolean>;
|
||||
}
|
||||
|
||||
export default IToolbarSeperator;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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>
|
||||
@@ -1,10 +1,11 @@
|
||||
jest.mock("../../Common/DocumentClientUtilityBase");
|
||||
import * as ko from "knockout";
|
||||
import * as sinon from "sinon";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
|
||||
import Q from "q";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
import { CosmosClient } from "../../Common/CosmosClient";
|
||||
import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
|
||||
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
@@ -62,19 +63,12 @@ describe("ContainerSampleGenerator", () => {
|
||||
|
||||
const explorerStub = createExplorerStub(database);
|
||||
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);
|
||||
generator.setData(sampleData);
|
||||
|
||||
await generator.createSampleContainerAsync();
|
||||
|
||||
expect(fakeDocumentClientUtility.createDocument.called).toBe(true);
|
||||
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send gremlin queries for Graph API account", async () => {
|
||||
@@ -109,18 +103,12 @@ describe("ContainerSampleGenerator", () => {
|
||||
const explorerStub = createExplorerStub(database);
|
||||
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);
|
||||
generator.setData(sampleData);
|
||||
|
||||
await generator.createSampleContainerAsync();
|
||||
|
||||
expect(fakeDocumentClientUtility.createDocument.called).toBe(false);
|
||||
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
|
||||
expect(executeStub.called).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import GraphTab from ".././Tabs/GraphTab";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { CosmosClient } from "../../Common/CosmosClient";
|
||||
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { createDocument, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
|
||||
|
||||
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
|
||||
data: any[];
|
||||
@@ -64,7 +65,7 @@ export class ContainerSampleGenerator {
|
||||
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
|
||||
}
|
||||
|
||||
await this.container.documentClientUtility.getOrCreateDatabaseAndCollection(createRequest, options);
|
||||
await getOrCreateDatabaseAndCollection(createRequest, options);
|
||||
await this.container.refreshAllDatabases();
|
||||
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
|
||||
if (!database) {
|
||||
@@ -103,7 +104,7 @@ export class ContainerSampleGenerator {
|
||||
} else {
|
||||
// For SQL all queries are executed at the same time
|
||||
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));
|
||||
promises.push(subPromise);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
import Database from "./Tree/Database";
|
||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
||||
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase";
|
||||
import { readDatabases, readCollection, readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||
import hasher from "hasher";
|
||||
import NewVertexPane from "./Panes/NewVertexPane";
|
||||
import NotebookV2Tab from "./Tabs/NotebookV2Tab";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
import Q from "q";
|
||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||
import TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -33,7 +33,6 @@ import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
|
||||
import { CassandraApi } from "../Api/Apis";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { config } from "../Config";
|
||||
@@ -52,12 +51,12 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { LoadQueryPane } from "./Panes/LoadQueryPane";
|
||||
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 { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
|
||||
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { PlatformType } from "../PlatformType";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
|
||||
@@ -82,6 +81,10 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
|
||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import Trigger from "./Tree/Trigger";
|
||||
import { NotificationsClientBase } from "../Common/NotificationsClientBase";
|
||||
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
||||
import TabsBase from "./Tabs/TabsBase";
|
||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@@ -92,6 +95,15 @@ enum ShareAccessToggleState {
|
||||
Read
|
||||
}
|
||||
|
||||
interface ExplorerOptions {
|
||||
notificationsClient: NotificationsClientBase;
|
||||
isEmulator: boolean;
|
||||
}
|
||||
interface AdHocAccessData {
|
||||
readWriteUrl: string;
|
||||
readUrl: string;
|
||||
}
|
||||
|
||||
export default class Explorer {
|
||||
public flight: ko.Observable<string> = ko.observable<string>(
|
||||
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
|
||||
@@ -107,7 +119,7 @@ export default class Explorer {
|
||||
public hasWriteAccess: ko.Observable<boolean>;
|
||||
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||
|
||||
public databaseAccount: ko.Observable<ViewModels.DatabaseAccount>;
|
||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
|
||||
public quotaId: ko.Observable<string>;
|
||||
@@ -118,6 +130,7 @@ export default class Explorer {
|
||||
public isPreferredApiGraph: ko.Computed<boolean>;
|
||||
public isPreferredApiTable: ko.Computed<boolean>;
|
||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||
public isServerlessEnabled: ko.Computed<boolean>;
|
||||
public isEmulator: boolean;
|
||||
public isAccountReady: ko.Observable<boolean>;
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
@@ -126,9 +139,8 @@ export default class Explorer {
|
||||
public extensionEndpoint: ko.Observable<string>;
|
||||
public armEndpoint: ko.Observable<string>;
|
||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||
public documentClientUtility: DocumentClientUtilityBase;
|
||||
public notificationsClient: ViewModels.NotificationsClient;
|
||||
public queriesClient: ViewModels.QueriesClient;
|
||||
public notificationsClient: NotificationsClientBase;
|
||||
public queriesClient: QueriesClient;
|
||||
public tableDataClient: TableDataClient;
|
||||
public splitter: Splitter;
|
||||
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
|
||||
@@ -139,7 +151,7 @@ export default class Explorer {
|
||||
public isNotificationConsoleExpanded: ko.Observable<boolean>;
|
||||
|
||||
// Panes
|
||||
public contextPanes: ViewModels.ContextualPane[];
|
||||
public contextPanes: ContextualPaneBase[];
|
||||
|
||||
// Resource Tree
|
||||
public databases: ko.ObservableArray<ViewModels.Database>;
|
||||
@@ -184,12 +196,12 @@ export default class Explorer {
|
||||
public uploadItemsPane: UploadItemsPane;
|
||||
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
|
||||
public loadQueryPane: LoadQueryPane;
|
||||
public saveQueryPane: ViewModels.ContextualPane;
|
||||
public saveQueryPane: ContextualPaneBase;
|
||||
public browseQueriesPane: BrowseQueriesPane;
|
||||
public uploadFilePane: UploadFilePane;
|
||||
public stringInputPane: StringInputPane;
|
||||
public setupNotebooksPane: SetupNotebooksPane;
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public gitHubReposPane: ContextualPaneBase;
|
||||
public publishNotebookPaneAdapter: ReactAdapter;
|
||||
|
||||
// features
|
||||
@@ -202,7 +214,7 @@ export default class Explorer {
|
||||
public hasAutoPilotV2FeatureFlag: ko.Computed<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 renewTokenError: ko.Observable<string>;
|
||||
public tokenForRenewal: ko.Observable<string>;
|
||||
@@ -228,7 +240,7 @@ export default class Explorer {
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
public notebookManager?: any; // This is dynamically loaded
|
||||
|
||||
private _panes: ViewModels.ContextualPane[] = [];
|
||||
private _panes: ContextualPaneBase[] = [];
|
||||
private _importExplorerConfigComplete: boolean = false;
|
||||
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = database => false;
|
||||
private _isInitializingNotebooks: boolean;
|
||||
@@ -251,7 +263,7 @@ export default class Explorer {
|
||||
|
||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||
|
||||
constructor(options: ViewModels.ExplorerOptions) {
|
||||
constructor(options: ExplorerOptions) {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
});
|
||||
@@ -264,7 +276,7 @@ export default class Explorer {
|
||||
this.deleteDatabaseText = ko.observable<string>("Delete Database");
|
||||
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>(
|
||||
SharedConstants.CollectionCreation.DefaultSubscriptionType
|
||||
);
|
||||
@@ -357,7 +369,6 @@ export default class Explorer {
|
||||
}
|
||||
});
|
||||
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
||||
this.documentClientUtility = options.documentClientUtility;
|
||||
this.notificationsClient = options.notificationsClient;
|
||||
this.isEmulator = options.isEmulator;
|
||||
|
||||
@@ -374,7 +385,7 @@ export default class Explorer {
|
||||
this.resourceTokenPartitionKey = ko.observable<string>();
|
||||
this.isAuthWithResourceToken = ko.observable<boolean>(false);
|
||||
|
||||
this.shareAccessData = ko.observable<ViewModels.AdHocAccessData>({
|
||||
this.shareAccessData = ko.observable<AdHocAccessData>({
|
||||
readWriteUrl: undefined,
|
||||
readUrl: undefined
|
||||
});
|
||||
@@ -459,7 +470,7 @@ export default class Explorer {
|
||||
});
|
||||
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
|
||||
this.defaultExperience = ko.observable<string>();
|
||||
this.databaseAccount.subscribe((databaseAccount: ViewModels.DatabaseAccount) => {
|
||||
this.databaseAccount.subscribe(databaseAccount => {
|
||||
this.defaultExperience(DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount));
|
||||
});
|
||||
|
||||
@@ -509,6 +520,14 @@ export default class Explorer {
|
||||
return false;
|
||||
});
|
||||
|
||||
this.isServerlessEnabled = ko.computed(
|
||||
() =>
|
||||
this.databaseAccount &&
|
||||
this.databaseAccount()?.properties?.capabilities?.find(
|
||||
item => item.name === Constants.CapabilityNames.EnableServerless
|
||||
) !== undefined
|
||||
);
|
||||
|
||||
this.isPreferredApiMongoDB = ko.computed(() => {
|
||||
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
|
||||
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) {
|
||||
@@ -544,8 +563,9 @@ export default class Explorer {
|
||||
defaultExperience &&
|
||||
defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase()
|
||||
) {
|
||||
const api = new CassandraApi();
|
||||
this._isSystemDatabasePredicate = api.isSystemDatabasePredicate;
|
||||
this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
|
||||
return database.id() === "system";
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -575,7 +595,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.addDatabasePane = new AddDatabasePane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "adddatabasepane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -584,7 +603,6 @@ export default class Explorer {
|
||||
|
||||
this.addCollectionPane = new AddCollectionPane({
|
||||
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "addcollectionpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -592,7 +610,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "deletecollectionconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -600,7 +617,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "deletedatabaseconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -608,7 +624,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.graphStylingPane = new GraphStylingPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "graphstylingpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -616,7 +631,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.addTableEntityPane = new AddTableEntityPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "addtableentitypane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -624,7 +638,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.editTableEntityPane = new EditTableEntityPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "edittableentitypane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -632,7 +645,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.tableColumnOptionsPane = new TableColumnOptionsPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "tablecolumnoptionspane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -640,7 +652,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.querySelectPane = new QuerySelectPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "queryselectpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -648,7 +659,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.newVertexPane = new NewVertexPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "newvertexpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -656,7 +666,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "cassandraaddcollectionpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -664,7 +673,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.settingsPane = new SettingsPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "settingspane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -672,7 +680,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.executeSprocParamsPane = new ExecuteSprocParamsPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "executesprocparamspane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -680,7 +687,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.renewAdHocAccessPane = new RenewAdHocAccessPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "renewadhocaccesspane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -688,7 +694,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.uploadItemsPane = new UploadItemsPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "uploaditemspane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -698,7 +703,6 @@ export default class Explorer {
|
||||
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
|
||||
|
||||
this.loadQueryPane = new LoadQueryPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "loadquerypane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -706,7 +710,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.saveQueryPane = new SaveQueryPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "savequerypane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -714,7 +717,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.browseQueriesPane = new BrowseQueriesPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "browsequeriespane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -722,7 +724,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.uploadFilePane = new UploadFilePane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "uploadfilepane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -730,7 +731,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.stringInputPane = new StringInputPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "stringinputpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -738,7 +738,6 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.setupNotebooksPane = new SetupNotebooksPane({
|
||||
documentClientUtility: this.documentClientUtility,
|
||||
id: "setupnotebookspane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -771,7 +770,6 @@ export default class Explorer {
|
||||
this.setupNotebooksPane
|
||||
];
|
||||
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
|
||||
this.rebindDocumentClientUtility.bind(this);
|
||||
this.isTabsContentExpanded = ko.observable(false);
|
||||
|
||||
document.addEventListener(
|
||||
@@ -853,7 +851,7 @@ export default class Explorer {
|
||||
this.editTableEntityPane.title("Edit Table Entity");
|
||||
this.deleteCollectionConfirmationPane.title("Delete Table");
|
||||
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
|
||||
this.tableDataClient = new TablesAPIDataClient(this.documentClientUtility);
|
||||
this.tableDataClient = new TablesAPIDataClient();
|
||||
break;
|
||||
case Constants.DefaultAccountExperience.Cassandra.toLowerCase():
|
||||
this.addCollectionText("New Table");
|
||||
@@ -872,7 +870,7 @@ export default class Explorer {
|
||||
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
|
||||
this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
|
||||
this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
|
||||
this.tableDataClient = new CassandraAPIDataClient(this.documentClientUtility);
|
||||
this.tableDataClient = new CassandraAPIDataClient();
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -1018,7 +1016,7 @@ export default class Explorer {
|
||||
);
|
||||
|
||||
try {
|
||||
const databaseAccount: ViewModels.DatabaseAccount = await resourceProviderClient.patchAsync(
|
||||
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
|
||||
this.databaseAccount().id,
|
||||
"2019-12-12",
|
||||
{
|
||||
@@ -1057,13 +1055,6 @@ export default class Explorer {
|
||||
// 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 {
|
||||
const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement;
|
||||
urlLinkInput && urlLinkInput.select();
|
||||
@@ -1382,7 +1373,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
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.selectedNode(this.resourceTokenCollection());
|
||||
deferred.resolve();
|
||||
@@ -1406,87 +1397,97 @@ export default class Explorer {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Refactor
|
||||
const deferred: Q.Deferred<any> = Q.defer();
|
||||
const offerPromise: Q.Promise<DataModels.Offer[]> = this.documentClientUtility.readOffers();
|
||||
this._setLoadingStatusText("Fetching offers...");
|
||||
|
||||
offerPromise.then(
|
||||
(offers: DataModels.Offer[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched offers.");
|
||||
this._setLoadingStatusText("Fetching databases...");
|
||||
this.documentClientUtility.readDatabases(null /*options*/).then(
|
||||
(databases: DataModels.Database[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched databases.");
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
||||
this._setLoadingStatusText("Fetching databases...");
|
||||
readDatabases(null /*options*/).then(
|
||||
(databases: DataModels.Database[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched databases.");
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
},
|
||||
startKey
|
||||
);
|
||||
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
||||
const deltaDatabases = this.getDeltaDatabases(databases, offers);
|
||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
||||
this.selectedNode(currentlySelectedNode);
|
||||
this._setLoadingStatusText("Fetching containers...");
|
||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
|
||||
.then(
|
||||
() => {
|
||||
this._setLoadingStatusText("Successfully fetched containers.");
|
||||
deferred.resolve();
|
||||
},
|
||||
startKey
|
||||
);
|
||||
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
||||
const deltaDatabases = this.getDeltaDatabases(databases, offers);
|
||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
||||
this.selectedNode(currentlySelectedNode);
|
||||
this._setLoadingStatusText("Fetching containers...");
|
||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
|
||||
.then(
|
||||
() => {
|
||||
this._setLoadingStatusText("Successfully fetched containers.");
|
||||
deferred.resolve();
|
||||
},
|
||||
reason => {
|
||||
this._setLoadingStatusText("Failed to fetch containers.");
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isRefreshingExplorer(false));
|
||||
},
|
||||
error => {
|
||||
this._setLoadingStatusText("Failed to fetch databases.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
error: JSON.stringify(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
this._setLoadingStatusText("Failed to fetch offers.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
error: JSON.stringify(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
reason => {
|
||||
this._setLoadingStatusText("Failed to fetch containers.");
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isRefreshingExplorer(false));
|
||||
},
|
||||
error => {
|
||||
this._setLoadingStatusText("Failed to fetch databases.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
error: JSON.stringify(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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 => {
|
||||
this._setLoadingStatusText("Failed to fetch offers.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
error: JSON.stringify(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return deferred.promise.then(
|
||||
() => {
|
||||
@@ -1535,7 +1536,7 @@ export default class Explorer {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
});
|
||||
this.isRefreshingExplorer(true);
|
||||
this.documentClientUtility.refreshCachedResources().then(
|
||||
refreshCachedResources().then(
|
||||
() => {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabases,
|
||||
@@ -1588,7 +1589,7 @@ export default class Explorer {
|
||||
|
||||
public async getArcadiaToken(): Promise<string> {
|
||||
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) => {
|
||||
resolve(token);
|
||||
},
|
||||
@@ -1626,11 +1627,11 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
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> {
|
||||
return MessageHandler.sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]);
|
||||
return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]);
|
||||
}
|
||||
|
||||
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
|
||||
@@ -1826,7 +1827,7 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||
MessageHandler.handleCachedDataMessage(message);
|
||||
handleCachedDataMessage(message);
|
||||
return;
|
||||
}
|
||||
if (message.type) {
|
||||
@@ -1963,7 +1964,7 @@ export default class Explorer {
|
||||
return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => {
|
||||
const openedSprocTab = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.StoredProcedures,
|
||||
(tab: ViewModels.Tab) => tab.node && tab.node.rid === storedProcedure.rid
|
||||
tab => tab.node && tab.node.rid === storedProcedure.rid
|
||||
);
|
||||
return (
|
||||
storedProcedure.rid === this.selectedNode().rid ||
|
||||
@@ -1977,7 +1978,7 @@ export default class Explorer {
|
||||
return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => {
|
||||
const openedUdfTab = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.UserDefinedFunctions,
|
||||
(tab: ViewModels.Tab) => tab.node && tab.node.rid === userDefinedFunction.rid
|
||||
tab => tab.node && tab.node.rid === userDefinedFunction.rid
|
||||
);
|
||||
return (
|
||||
userDefinedFunction.rid === this.selectedNode().rid ||
|
||||
@@ -1991,7 +1992,7 @@ export default class Explorer {
|
||||
return _.find(selectedCollection.triggers(), (trigger: Trigger) => {
|
||||
const openedTriggerTab = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Triggers,
|
||||
(tab: ViewModels.Tab) => tab.node && tab.node.rid === trigger.rid
|
||||
tab => tab.node && tab.node.rid === trigger.rid
|
||||
);
|
||||
return (
|
||||
trigger.rid === this.selectedNode().rid ||
|
||||
@@ -2001,7 +2002,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public closeAllPanes(): void {
|
||||
this._panes.forEach((pane: ViewModels.ContextualPane) => pane.close());
|
||||
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
|
||||
}
|
||||
|
||||
public getPlatformType(): PlatformType {
|
||||
@@ -2016,13 +2017,13 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
|
||||
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void {
|
||||
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
|
||||
this.commandBarComponentAdapter.onUpdateTabsButtons(buttons);
|
||||
}
|
||||
|
||||
public signInAad = () => {
|
||||
TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" });
|
||||
MessageHandler.sendMessage({
|
||||
sendMessage({
|
||||
type: MessageTypes.AadSignIn
|
||||
});
|
||||
};
|
||||
@@ -2033,21 +2034,21 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
public clickHostedAccountSwitch = () => {
|
||||
MessageHandler.sendMessage({
|
||||
sendMessage({
|
||||
type: MessageTypes.UpdateAccountSwitch,
|
||||
click: true
|
||||
});
|
||||
};
|
||||
|
||||
public clickHostedDirectorySwitch = () => {
|
||||
MessageHandler.sendMessage({
|
||||
sendMessage({
|
||||
type: MessageTypes.UpdateDirectoryControl,
|
||||
click: true
|
||||
});
|
||||
};
|
||||
|
||||
public refreshDatabaseAccount = () => {
|
||||
MessageHandler.sendMessage({
|
||||
sendMessage({
|
||||
type: MessageTypes.RefreshDatabaseAccount
|
||||
});
|
||||
};
|
||||
@@ -2076,9 +2077,7 @@ export default class Explorer {
|
||||
if (isNewDatabase) {
|
||||
database.expandDatabase();
|
||||
}
|
||||
this.tabsManager.refreshActiveTab(
|
||||
(tab: ViewModels.Tab) => tab.collection && tab.collection.getDatabase().rid === database.rid
|
||||
);
|
||||
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -2181,7 +2180,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
const urlPrefixWithKeyParam: string = `${config.hostedExplorerURL}?key=`;
|
||||
const currentActiveTab: ViewModels.Tab = this.tabsManager.activeTab();
|
||||
const currentActiveTab = this.tabsManager.activeTab();
|
||||
|
||||
return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`;
|
||||
}
|
||||
@@ -2347,9 +2346,9 @@ export default class Explorer {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
public publishNotebook(name: string, content: string): void {
|
||||
public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void {
|
||||
if (this.notebookManager) {
|
||||
this.notebookManager.openPublishNotebookPane(name, content);
|
||||
this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
this.isPublishNotebookPaneEnabled(true);
|
||||
}
|
||||
@@ -2444,25 +2443,23 @@ export default class Explorer {
|
||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||
}
|
||||
|
||||
const notebookTabs: NotebookV2Tab[] = this.tabsManager.getTabs(
|
||||
const notebookTabs = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: ViewModels.Tab) =>
|
||||
tab =>
|
||||
(tab as NotebookV2Tab).notebookPath &&
|
||||
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path)
|
||||
) as NotebookV2Tab[];
|
||||
let notebookTab: NotebookV2Tab = notebookTabs && notebookTabs[0];
|
||||
let notebookTab = notebookTabs && notebookTabs[0];
|
||||
|
||||
if (notebookTab) {
|
||||
this.tabsManager.activateTab(notebookTab);
|
||||
} else {
|
||||
const options: ViewModels.NotebookTabOptions = {
|
||||
const options: NotebookTabOptions = {
|
||||
account: CosmosClient.databaseAccount(),
|
||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||
node: null,
|
||||
title: notebookContentItem.name,
|
||||
tabPath: notebookContentItem.path,
|
||||
documentClientUtility: null,
|
||||
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
masterKey: CosmosClient.masterKey() || "",
|
||||
@@ -2521,7 +2518,7 @@ export default class Explorer {
|
||||
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
|
||||
})
|
||||
.then(newNotebookFile => {
|
||||
const notebookTabs: ViewModels.Tab[] = this.tabsManager.getTabs(
|
||||
const notebookTabs = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
|
||||
);
|
||||
@@ -2609,7 +2606,11 @@ export default class Explorer {
|
||||
|
||||
private async _refreshNotebooksEnabledStateForAccount(): Promise<void> {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@@ -2891,7 +2892,7 @@ export default class Explorer {
|
||||
|
||||
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Terminal,
|
||||
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation
|
||||
tab => tab.hashLocation() == hashLocation
|
||||
) as TerminalTab[];
|
||||
let terminalTab: TerminalTab = terminalTabs && terminalTabs[0];
|
||||
|
||||
@@ -2904,8 +2905,6 @@ export default class Explorer {
|
||||
node: null,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
hashLocation: hashLocation,
|
||||
@@ -2927,7 +2926,7 @@ export default class Explorer {
|
||||
|
||||
const galleryTabs = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Gallery,
|
||||
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation
|
||||
tab => tab.hashLocation() == hashLocation
|
||||
);
|
||||
let galleryTab = galleryTabs && galleryTabs[0];
|
||||
|
||||
@@ -2973,17 +2972,14 @@ export default class Explorer {
|
||||
|
||||
const notebookViewerTabModule = this.notebookViewerTab;
|
||||
|
||||
let isNotebookViewerOpen = (tab: ViewModels.Tab) => {
|
||||
let isNotebookViewerOpen = (tab: TabsBase) => {
|
||||
const notebookViewerTab = tab as typeof notebookViewerTabModule.default;
|
||||
return notebookViewerTab.notebookUrl === notebookUrl;
|
||||
};
|
||||
|
||||
const notebookViewerTabs = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: ViewModels.Tab) => {
|
||||
return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab);
|
||||
}
|
||||
);
|
||||
const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, tab => {
|
||||
return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab);
|
||||
});
|
||||
let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0];
|
||||
|
||||
if (notebookViewerTab) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { GraphData, D3Node, D3Link } from "./GraphData";
|
||||
import { HashMap } from "../../../Common/HashMap";
|
||||
import { BaseType } from "d3";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { GraphConfig } from "../../Tabs/GraphTab";
|
||||
import { GraphExplorer } from "./GraphExplorer";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
jest.mock("../../../Common/DocumentClientUtilityBase");
|
||||
import React from "react";
|
||||
import * as sinon from "sinon";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
@@ -7,11 +8,11 @@ import { GraphExplorer, GraphExplorerProps, GraphAccessor, GraphHighlightedNodeD
|
||||
import * as D3ForceGraph from "./D3ForceGraph";
|
||||
import { GraphData } from "./GraphData";
|
||||
import { TabComponent } from "../../Controls/Tabs/TabComponent";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as StorageUtility from "../../../Shared/StorageUtility";
|
||||
import GraphTab from "../../Tabs/GraphTab";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
|
||||
|
||||
describe("Check whether query result is vertex array", () => {
|
||||
it("should reject null as vertex array", () => {
|
||||
@@ -134,7 +135,7 @@ describe("GraphExplorer", () => {
|
||||
const COLLECTION_SELF_LINK = "collectionSelfLink";
|
||||
const gremlinRU = 789.12;
|
||||
|
||||
const createMockProps = (documentClientUtility?: any): GraphExplorerProps => {
|
||||
const createMockProps = (): GraphExplorerProps => {
|
||||
const graphConfig = GraphTab.createGraphConfig();
|
||||
const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig);
|
||||
|
||||
@@ -149,7 +150,6 @@ describe("GraphExplorer", () => {
|
||||
onIsValidQueryChange: (isValidQuery: boolean): void => {},
|
||||
|
||||
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
|
||||
documentClientUtility: documentClientUtility,
|
||||
collectionRid: COLLECTION_RID,
|
||||
collectionSelfLink: COLLECTION_SELF_LINK,
|
||||
graphBackendEndpoint: "graphBackendEndpoint",
|
||||
@@ -188,7 +188,6 @@ describe("GraphExplorer", () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
let connectStub: sinon.SinonSpy;
|
||||
let queryDocStub: sinon.SinonSpy;
|
||||
let submitToBackendSpy: sinon.SinonSpy;
|
||||
let renderResultAsJsonStub: sinon.SinonSpy;
|
||||
let onMiddlePaneInitializedStub: sinon.SinonSpy;
|
||||
@@ -215,46 +214,6 @@ describe("GraphExplorer", () => {
|
||||
[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 = (
|
||||
graphExplorer: GraphExplorer,
|
||||
backendResponses: BackendResponses,
|
||||
@@ -333,7 +292,29 @@ describe("GraphExplorer", () => {
|
||||
done: any,
|
||||
ignoreD3Update: boolean
|
||||
): 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} />);
|
||||
graphExplorerInstance = wrapper.instance() as GraphExplorer;
|
||||
setupMocks(graphExplorerInstance, backendResponses, done, ignoreD3Update);
|
||||
@@ -341,7 +322,7 @@ describe("GraphExplorer", () => {
|
||||
};
|
||||
|
||||
const cleanUpStubsWrapper = () => {
|
||||
queryDocStub.restore();
|
||||
jest.resetAllMocks();
|
||||
connectStub.restore();
|
||||
submitToBackendSpy.restore();
|
||||
renderResultAsJsonStub.restore();
|
||||
@@ -378,22 +359,11 @@ describe("GraphExplorer", () => {
|
||||
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", () => {
|
||||
expect(
|
||||
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0]
|
||||
).toEqual("databaseId");
|
||||
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 });
|
||||
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
|
||||
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
|
||||
enableCrossPartitionQuery: true
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
expect(
|
||||
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0]
|
||||
).toEqual("databaseId");
|
||||
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 });
|
||||
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
|
||||
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
|
||||
enableCrossPartitionQuery: true
|
||||
});
|
||||
});
|
||||
|
||||
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as D3ForceGraph from "./D3ForceGraph";
|
||||
import { GraphVizComponentProps } from "./GraphVizComponent";
|
||||
import * as GraphData from "./GraphData";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { GraphUtil } from "./GraphUtil";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
@@ -28,7 +28,7 @@ import * as Constants from "../../../Common/Constants";
|
||||
import { InputProperty } from "../../../Contracts/ViewModels";
|
||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
|
||||
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
|
||||
|
||||
export interface GraphAccessor {
|
||||
applyFilter: () => void;
|
||||
@@ -47,7 +47,6 @@ export interface GraphExplorerProps {
|
||||
onIsValidQueryChange: (isValidQuery: boolean) => void;
|
||||
|
||||
collectionPartitionKeyProperty: string;
|
||||
documentClientUtility: DocumentClientUtilityBase;
|
||||
collectionRid: string;
|
||||
collectionSelfLink: string;
|
||||
graphBackendEndpoint: string;
|
||||
@@ -697,7 +696,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @param cmd
|
||||
*/
|
||||
public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> {
|
||||
console.log("submit:", cmd);
|
||||
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`);
|
||||
this.setExecuteCounter(this.executeCounter + 1);
|
||||
|
||||
@@ -730,26 +728,24 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
*/
|
||||
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
|
||||
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
|
||||
return this.props.documentClientUtility
|
||||
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
|
||||
maxItemCount: GraphExplorer.PAGE_ALL,
|
||||
enableCrossPartitionQuery:
|
||||
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
|
||||
"true"
|
||||
})
|
||||
.then(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
return iterator.fetchNext().then(response => response.resources);
|
||||
},
|
||||
(reason: any) => {
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to execute non-paged query ${query}. Reason:${reason}`,
|
||||
reason
|
||||
);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
|
||||
maxItemCount: GraphExplorer.PAGE_ALL,
|
||||
enableCrossPartitionQuery:
|
||||
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
|
||||
"true"
|
||||
}).then(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
return iterator.fetchNext().then(response => response.resources);
|
||||
},
|
||||
(reason: any) => {
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to execute non-paged query ${query}. Reason:${reason}`,
|
||||
reason
|
||||
);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1732,12 +1728,10 @@ 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`;
|
||||
}
|
||||
|
||||
return this.props.documentClientUtility
|
||||
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
|
||||
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
|
||||
enableCrossPartitionQuery:
|
||||
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
})
|
||||
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
|
||||
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
|
||||
enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
})
|
||||
.then(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
this.currentDocDBQueryInfo = {
|
||||
@@ -1766,16 +1760,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
|
||||
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
|
||||
|
||||
return this.props.documentClientUtility
|
||||
.queryDocumentsPage(
|
||||
this.props.collectionRid,
|
||||
this.currentDocDBQueryInfo.iterator,
|
||||
this.currentDocDBQueryInfo.index,
|
||||
{
|
||||
enableCrossPartitionQuery:
|
||||
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
}
|
||||
)
|
||||
return queryDocumentsPage(
|
||||
this.props.collectionRid,
|
||||
this.currentDocDBQueryInfo.iterator,
|
||||
this.currentDocDBQueryInfo.index,
|
||||
{
|
||||
enableCrossPartitionQuery:
|
||||
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
}
|
||||
)
|
||||
.then((results: ViewModels.QueryResults) => {
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { GraphConfig } from "../../Tabs/GraphTab";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { GraphExplorer, GraphAccessor } from "./GraphExplorer";
|
||||
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
|
||||
|
||||
interface Parameter {
|
||||
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
|
||||
@@ -18,7 +17,6 @@ interface Parameter {
|
||||
graphConfig?: GraphConfig;
|
||||
|
||||
collectionPartitionKeyProperty: string;
|
||||
documentClientUtility: DocumentClientUtilityBase;
|
||||
collectionRid: string;
|
||||
collectionSelfLink: string;
|
||||
graphBackendEndpoint: string;
|
||||
@@ -51,7 +49,6 @@ export class GraphExplorerAdapter implements ReactAdapter {
|
||||
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
|
||||
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
|
||||
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
|
||||
documentClientUtility={this.params.documentClientUtility}
|
||||
collectionRid={this.params.collectionRid}
|
||||
collectionSelfLink={this.params.collectionSelfLink}
|
||||
graphBackendEndpoint={this.params.graphBackendEndpoint}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as sinon from "sinon";
|
||||
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
|
||||
describe("Gremlin Client", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import * as Q from "q";
|
||||
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { HashMap } from "../../../Common/HashMap";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
|
||||
@@ -12,11 +12,12 @@ import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/Com
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
export class CommandBarComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
public container: Explorer;
|
||||
private tabsButtons: ViewModels.NavbarButtonConfig[];
|
||||
private tabsButtons: CommandButtonComponentProps[];
|
||||
private isNotebookTabActive: ko.Computed<boolean>;
|
||||
|
||||
constructor(container: Explorer) {
|
||||
@@ -44,14 +45,15 @@ export class CommandBarComponentAdapter implements ReactAdapter {
|
||||
container.isHostedDataExplorerEnabled,
|
||||
container.isSynapseLinkUpdating,
|
||||
container.databaseAccount,
|
||||
this.isNotebookTabActive
|
||||
this.isNotebookTabActive,
|
||||
container.isServerlessEnabled
|
||||
];
|
||||
|
||||
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
|
||||
this.parameters = ko.observable(Date.now());
|
||||
}
|
||||
|
||||
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void {
|
||||
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
|
||||
this.tabsButtons = buttons;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,47 @@ import Explorer from "../../Explorer";
|
||||
describe("CommandBarComponentButtonFactory tests", () => {
|
||||
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", () => {
|
||||
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
|
||||
|
||||
@@ -23,6 +64,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
it("Notebooks is already enabled - button should be hidden", () => {
|
||||
@@ -86,6 +128,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -167,6 +210,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -254,6 +298,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.notebookManager = new NotebookManager();
|
||||
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -305,6 +350,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
it("should only show New SQL Query and Open Query buttons", () => {
|
||||
|
||||
@@ -26,17 +26,18 @@ import GitHubIcon from "../../../../images/github.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
import { config, Platform } from "../../../Config";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
export class CommandBarComponentButtonFactory {
|
||||
private static counter: number = 0;
|
||||
|
||||
public static createStaticCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
public static createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
if (container.isAuthWithResourceToken()) {
|
||||
return CommandBarComponentButtonFactory.createStaticCommandBarButtonsForResourceToken(container);
|
||||
}
|
||||
|
||||
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [newCollectionBtn];
|
||||
const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
|
||||
|
||||
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
|
||||
if (addSynapseLink) {
|
||||
@@ -112,7 +113,7 @@ export class CommandBarComponentButtonFactory {
|
||||
|
||||
if (CommandBarComponentButtonFactory.areScriptsSupported(container)) {
|
||||
const label = "New Stored Procedure";
|
||||
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
|
||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -133,12 +134,12 @@ export class CommandBarComponentButtonFactory {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public static createContextCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [];
|
||||
public static createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
|
||||
const label = "New Shell";
|
||||
const newMongoShellBtn: ViewModels.NavbarButtonConfig = {
|
||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -156,15 +157,15 @@ export class CommandBarComponentButtonFactory {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public static createControlCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [];
|
||||
public static createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (window.dataExplorerPlatform === PlatformType.Hosted) {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
if (!container.isPreferredApiCassandra()) {
|
||||
const label = "Settings";
|
||||
const settingsPaneButton: ViewModels.NavbarButtonConfig = {
|
||||
const settingsPaneButton: CommandButtonComponentProps = {
|
||||
iconSrc: SettingsIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.settingsPane.open(),
|
||||
@@ -179,7 +180,7 @@ export class CommandBarComponentButtonFactory {
|
||||
|
||||
if (container.isHostedDataExplorerEnabled()) {
|
||||
const label = "Open Full Screen";
|
||||
const fullScreenButton: ViewModels.NavbarButtonConfig = {
|
||||
const fullScreenButton: CommandButtonComponentProps = {
|
||||
iconSrc: OpenInTabIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.generateSharedAccessData(),
|
||||
@@ -195,7 +196,7 @@ export class CommandBarComponentButtonFactory {
|
||||
|
||||
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
|
||||
const label = "Feedback";
|
||||
const feedbackButtonOptions: ViewModels.NavbarButtonConfig = {
|
||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||
iconSrc: FeedbackIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.provideFeedbackEmail(),
|
||||
@@ -211,7 +212,7 @@ export class CommandBarComponentButtonFactory {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public static createDivider(): ViewModels.NavbarButtonConfig {
|
||||
public static createDivider(): CommandButtonComponentProps {
|
||||
const label = `divider${CommandBarComponentButtonFactory.counter++}`;
|
||||
return {
|
||||
isDivider: true,
|
||||
@@ -228,7 +229,7 @@ export class CommandBarComponentButtonFactory {
|
||||
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
||||
}
|
||||
|
||||
private static createNewCollectionGroup(container: Explorer): ViewModels.NavbarButtonConfig {
|
||||
private static createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
|
||||
const label = container.addCollectionText();
|
||||
return {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (container.isServerlessEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
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();
|
||||
return {
|
||||
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()) {
|
||||
const label = "New SQL Query";
|
||||
return {
|
||||
@@ -325,15 +331,15 @@ export class CommandBarComponentButtonFactory {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static createScriptCommandButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [];
|
||||
public static createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const shouldEnableScriptsCommands: boolean =
|
||||
!container.isDatabaseNodeOrNoneSelected() && CommandBarComponentButtonFactory.areScriptsSupported(container);
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New Stored Procedure";
|
||||
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
|
||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -350,7 +356,7 @@ export class CommandBarComponentButtonFactory {
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New UDF";
|
||||
const newUserDefinedFunctionBtn: ViewModels.NavbarButtonConfig = {
|
||||
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddUdfIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -367,7 +373,7 @@ export class CommandBarComponentButtonFactory {
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New Trigger";
|
||||
const newTriggerBtn: ViewModels.NavbarButtonConfig = {
|
||||
const newTriggerBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddTriggerIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -385,7 +391,7 @@ export class CommandBarComponentButtonFactory {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static createScaleAndSettingsButton(container: Explorer): ViewModels.NavbarButtonConfig {
|
||||
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
|
||||
let isShared = false;
|
||||
if (container.isDatabaseNodeSelected()) {
|
||||
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";
|
||||
return {
|
||||
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";
|
||||
return {
|
||||
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";
|
||||
return {
|
||||
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";
|
||||
return {
|
||||
iconSrc: OpenQueryFromDiskIcon,
|
||||
@@ -462,7 +468,7 @@ export class CommandBarComponentButtonFactory {
|
||||
};
|
||||
}
|
||||
|
||||
private static createEnableNotebooksButton(container: Explorer): ViewModels.NavbarButtonConfig {
|
||||
private static createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (config.platform === Platform.Emulator) {
|
||||
return null;
|
||||
}
|
||||
@@ -483,7 +489,7 @@ export class CommandBarComponentButtonFactory {
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenTerminalButton(container: Explorer): ViewModels.NavbarButtonConfig {
|
||||
private static createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
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 tooltip =
|
||||
"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 tooltip =
|
||||
"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";
|
||||
return {
|
||||
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();
|
||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||
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 openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
describe("CommandBarUtil tests", () => {
|
||||
const createButton = (): ViewModels.NavbarButtonConfig => {
|
||||
const createButton = (): CommandButtonComponentProps => {
|
||||
return {
|
||||
iconSrc: "icon",
|
||||
iconAlt: "label",
|
||||
@@ -54,7 +55,7 @@ describe("CommandBarUtil tests", () => {
|
||||
});
|
||||
|
||||
it("should create buttons with unique keys", () => {
|
||||
const btns: ViewModels.NavbarButtonConfig[] = [];
|
||||
const btns: CommandButtonComponentProps[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
btns.push(createButton());
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import _ from "underscore";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Observable } from "knockout";
|
||||
import { IconType } from "office-ui-fabric-react/lib/Icon";
|
||||
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 { 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 ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
|
||||
@@ -21,13 +20,13 @@ export class CommandBarUtil {
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
* @param btns
|
||||
*/
|
||||
public static convertButton(btns: ViewModels.NavbarButtonConfig[], backgroundColor: string): ICommandBarItemProps[] {
|
||||
public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
|
||||
return btns
|
||||
.filter(btn => btn)
|
||||
.map(
|
||||
(btn: ViewModels.NavbarButtonConfig, index: number): ICommandBarItemProps => {
|
||||
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
|
||||
if (btn.isDivider) {
|
||||
return CommandBarUtil.createDivider(btn.commandButtonLabel);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandButtonComponent } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import {
|
||||
CommandButtonComponent,
|
||||
CommandButtonComponentProps
|
||||
} from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
export interface ControlBarComponentProps {
|
||||
buttons: ViewModels.NavbarButtonConfig[];
|
||||
buttons: CommandButtonComponentProps[];
|
||||
}
|
||||
|
||||
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(
|
||||
(btn: ViewModels.NavbarButtonConfig, index: number): JSX.Element => {
|
||||
(btn: CommandButtonComponentProps, index: number): JSX.Element => {
|
||||
// Remove label
|
||||
btn.commandButtonLabel = null;
|
||||
return CommandButtonComponent.renderButton(btn, `${index}`);
|
||||
|
||||
@@ -8,12 +8,12 @@ import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { ControlBarComponent } from "./ControlBarComponent";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
export class ControlBarComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private buttons: ko.ObservableArray<ViewModels.NavbarButtonConfig>) {
|
||||
constructor(private buttons: ko.ObservableArray<CommandButtonComponentProps>) {
|
||||
this.buttons.subscribe(() => this.forceRender());
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { actions, createContentRef, createKernelRef, selectors } from "@nteract/
|
||||
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
|
||||
import { NotebookContentItem } from "../NotebookContentItem";
|
||||
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
|
||||
import { CdbAppState } from "./types";
|
||||
|
||||
export interface NotebookComponentAdapterOptions {
|
||||
contentItem: NotebookContentItem;
|
||||
@@ -18,6 +19,7 @@ export interface NotebookComponentAdapterOptions {
|
||||
|
||||
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
||||
private onUpdateKernelInfo: () => void;
|
||||
public getNotebookParentElement: () => HTMLElement;
|
||||
public parameters: any;
|
||||
|
||||
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 => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@nteract/core";
|
||||
import * as Immutable from "immutable";
|
||||
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 "./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()
|
||||
.getState()
|
||||
.core.entities.contents.byRef.get(this.contentRef);
|
||||
let content: string;
|
||||
let content: string | ImmutableNotebook;
|
||||
switch (record.model.type) {
|
||||
case "notebook":
|
||||
content = JSON.stringify(toJS(record.model.notebook));
|
||||
content = record.model.notebook;
|
||||
break;
|
||||
case "file":
|
||||
content = record.model.text;
|
||||
|
||||
@@ -84,3 +84,22 @@ export const traceNotebookTelemetry = (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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ import { sessions, kernels } from "rx-jupyter";
|
||||
import { RecordOf } from "immutable";
|
||||
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as CdbActions from "./actions";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
@@ -82,6 +82,19 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
|
||||
});
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Immutable from "immutable";
|
||||
import { AppState } from "@nteract/core";
|
||||
import { AppState, ContentRef } from "@nteract/core";
|
||||
|
||||
import { Notebook } from "../../../Common/Constants";
|
||||
import { CellId } from "@nteract/commutable";
|
||||
@@ -9,6 +9,7 @@ export interface CdbRecordProps {
|
||||
defaultExperience: string | undefined;
|
||||
kernelRestartDelayMs: number;
|
||||
hoveredCellId: CellId | undefined;
|
||||
currentNotebookParentElements: Map<ContentRef, HTMLElement>;
|
||||
}
|
||||
|
||||
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
|
||||
@@ -21,5 +22,6 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
|
||||
databaseAccountName: undefined,
|
||||
defaultExperience: undefined,
|
||||
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
|
||||
hoveredCellId: undefined
|
||||
hoveredCellId: undefined,
|
||||
currentNotebookParentElements: new Map<ContentRef, HTMLElement>()
|
||||
});
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
|
||||
export class NotebookContainerClient implements ViewModels.INotebookContainerClient {
|
||||
export class NotebookContainerClient {
|
||||
private reconnectingNotificationId: string;
|
||||
private isResettingWorkspace: boolean;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import { StringUtils } from "../../Utils/StringUtils";
|
||||
import { FileSystemUtil } from "./FileSystemUtil";
|
||||
@@ -9,7 +8,7 @@ import { ServerConfig, IContent, IContentProvider, FileType, IEmptyContent } fro
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import { stringifyNotebook } from "@nteract/commutable";
|
||||
|
||||
export class NotebookContentClient implements ViewModels.INotebookContentClient {
|
||||
export class NotebookContentClient {
|
||||
constructor(
|
||||
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
||||
private notebookBasePath: ko.Observable<string>,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
@@ -23,7 +22,9 @@ import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import Explorer from "../Explorer";
|
||||
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
||||
|
||||
export interface NotebookManagerOptions {
|
||||
container: Explorer;
|
||||
@@ -39,14 +40,14 @@ export default class NotebookManager {
|
||||
public junoClient: JunoClient;
|
||||
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public notebookClient: ViewModels.INotebookContainerClient;
|
||||
public notebookContentClient: ViewModels.INotebookContentClient;
|
||||
public notebookClient: NotebookContainerClient;
|
||||
public notebookContentClient: NotebookContentClient;
|
||||
|
||||
private gitHubContentProvider: GitHubContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
private gitHubClient: GitHubClient;
|
||||
|
||||
public gitHubReposPane: ViewModels.ContextualPane;
|
||||
public gitHubReposPane: ContextualPaneBase;
|
||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||
|
||||
public initialize(params: NotebookManagerOptions): void {
|
||||
@@ -56,7 +57,6 @@ export default class NotebookManager {
|
||||
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
|
||||
this.gitHubClient = new GitHubClient(this.onGitHubClientError);
|
||||
this.gitHubReposPane = new GitHubReposPane({
|
||||
documentClientUtility: this.params.container.documentClientUtility,
|
||||
id: "gitHubReposPane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: this.params.container,
|
||||
@@ -108,8 +108,12 @@ export default class NotebookManager {
|
||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||
}
|
||||
|
||||
public openPublishNotebookPane(name: string, content: string): void {
|
||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content);
|
||||
public openPublishNotebookPane(
|
||||
name: string,
|
||||
content: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement
|
||||
): void {
|
||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
|
||||
}
|
||||
|
||||
// Octokit's error handler uses any
|
||||
|
||||
@@ -49,8 +49,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
||||
},
|
||||
prompt: ({ id, contentRef }) => <></>
|
||||
}
|
||||
}}
|
||||
</CodeCell>
|
||||
),
|
||||
|
||||
@@ -30,11 +30,18 @@ import { CellType } from "@nteract/commutable/src";
|
||||
import "./NotebookRenderer.less";
|
||||
import HoverableCell from "./decorators/HoverableCell";
|
||||
import CellLabeler from "./decorators/CellLabeler";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
export interface NotebookRendererBaseProps {
|
||||
contentRef: any;
|
||||
}
|
||||
|
||||
interface NotebookRendererDispatchProps {
|
||||
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => void;
|
||||
}
|
||||
|
||||
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps;
|
||||
|
||||
interface PassedEditorProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
@@ -68,6 +75,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
|
||||
};
|
||||
|
||||
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
private notebookRendererRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: NotebookRendererProps) {
|
||||
super(props);
|
||||
|
||||
@@ -78,13 +87,22 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
|
||||
componentDidMount() {
|
||||
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 {
|
||||
return (
|
||||
<>
|
||||
<div className="NotebookRendererContainer">
|
||||
<div className="NotebookRenderer">
|
||||
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<KeyboardShortcuts 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) => {
|
||||
return {
|
||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||
@@ -156,6 +174,14 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
|
||||
component: transform
|
||||
})
|
||||
);
|
||||
},
|
||||
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => {
|
||||
return dispatch(
|
||||
cdbActions.UpdateNotebookParentDomElt({
|
||||
contentRef,
|
||||
parentElt
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import {
|
||||
ImmutableNotebook,
|
||||
MediaBundle,
|
||||
CodeCellParams,
|
||||
MarkdownCellParams,
|
||||
makeCodeCell,
|
||||
makeMarkdownCell,
|
||||
makeNotebookRecord
|
||||
} from "@nteract/commutable";
|
||||
import { List, Map } from "immutable";
|
||||
|
||||
const fileName = "file";
|
||||
const notebookName = "file.ipynb";
|
||||
@@ -7,6 +17,57 @@ const filePath = `folder/${fileName}`;
|
||||
const notebookPath = `folder/${notebookName}`;
|
||||
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
||||
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
||||
const notebookRecord = makeNotebookRecord({
|
||||
cellOrder: List.of("0", "1", "2", "3"),
|
||||
cellMap: Map({
|
||||
"0": makeMarkdownCell({
|
||||
cell_type: "markdown",
|
||||
source: "abc",
|
||||
metadata: undefined
|
||||
} as MarkdownCellParams),
|
||||
"1": makeCodeCell({
|
||||
cell_type: "code",
|
||||
execution_count: undefined,
|
||||
metadata: undefined,
|
||||
source: "print(5)",
|
||||
outputs: List.of({
|
||||
name: "stdout",
|
||||
output_type: "stream",
|
||||
text: "5"
|
||||
})
|
||||
} as CodeCellParams),
|
||||
"2": makeCodeCell({
|
||||
cell_type: "code",
|
||||
execution_count: undefined,
|
||||
metadata: undefined,
|
||||
source: 'display(HTML("<h1>Sample html</h1>"))',
|
||||
outputs: List.of({
|
||||
data: Object.freeze({
|
||||
data: {
|
||||
"text/html": "<h1>Sample output</h1>",
|
||||
"text/plain": "<IPython.core.display.HTML object>"
|
||||
}
|
||||
} as MediaBundle),
|
||||
output_type: "display_data",
|
||||
metadata: undefined
|
||||
})
|
||||
} as CodeCellParams),
|
||||
"3": makeCodeCell({
|
||||
cell_type: "code",
|
||||
execution_count: undefined,
|
||||
metadata: undefined,
|
||||
source: 'print("hello world")',
|
||||
outputs: List.of({
|
||||
name: "stdout",
|
||||
output_type: "stream",
|
||||
text: "hello world"
|
||||
})
|
||||
} as CodeCellParams)
|
||||
}),
|
||||
nbformat_minor: 2,
|
||||
nbformat: 2,
|
||||
metadata: undefined
|
||||
});
|
||||
|
||||
describe("NotebookUtil", () => {
|
||||
describe("isNotebookFile", () => {
|
||||
@@ -46,4 +107,11 @@ describe("NotebookUtil", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findFirstCodeCellWithDisplay", () => {
|
||||
it("works for Notebook file", () => {
|
||||
const notebookObject = notebookRecord as ImmutableNotebook;
|
||||
expect(NotebookUtil.findFirstCodeCellWithDisplay(notebookObject)).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path";
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import { ImmutableNotebook, ImmutableCodeCell, ImmutableOutput } from "@nteract/commutable";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import { StringUtils } from "../../Utils/StringUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
@@ -100,4 +100,30 @@ export class NotebookUtil {
|
||||
const basePath = path.split(contentName).shift();
|
||||
return `${basePath}${newName}`;
|
||||
}
|
||||
|
||||
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
|
||||
let codeCellCount = -1;
|
||||
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
|
||||
const cellId = notebookObject.cellOrder.get(i);
|
||||
if (cellId) {
|
||||
const cell = notebookObject.cellMap.get(cellId);
|
||||
if (cell && cell.cell_type === "code") {
|
||||
codeCellCount++;
|
||||
const codeCell = cell as ImmutableCodeCell;
|
||||
if (codeCell.outputs) {
|
||||
const displayOutput = codeCell.outputs.find((output: ImmutableOutput) => {
|
||||
if (output.output_type === "display_data" || output.output_type === "execute_result") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (displayOutput) {
|
||||
return codeCellCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Output does not exist for any of the cells.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<!-- Add collection errors - End -->
|
||||
|
||||
<!-- upsell message - start -->
|
||||
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: formErrors && !formErrors()">
|
||||
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()">
|
||||
<div class="infoBoxContent">
|
||||
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo"></span>
|
||||
<span class="infoBoxDetails">
|
||||
@@ -112,7 +112,9 @@
|
||||
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
|
||||
<option data-bind="value: $data">
|
||||
</datalist>
|
||||
|
||||
<!-- Database provisioned throughput - Start -->
|
||||
<!-- ko if: canConfigureThroughput -->
|
||||
<div class="databaseProvision" aria-label="New database provision support"
|
||||
data-bind="visible: databaseCreateNew">
|
||||
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
|
||||
@@ -187,6 +189,7 @@
|
||||
</throughput-input>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
<!-- Database provisioned throughput - End -->
|
||||
</div>
|
||||
|
||||
@@ -414,7 +417,10 @@
|
||||
more</a></p>
|
||||
</div>
|
||||
<!-- large parition key - end -->
|
||||
<!-- Provision collection throughput checkox - start -->
|
||||
|
||||
<!-- Provision collection throughput - start -->
|
||||
<!-- ko if: canConfigureThroughput -->
|
||||
<!-- Provision collection throughput checkbox - start -->
|
||||
<div class="pkPadding" data-bind="visible: databaseHasSharedOffer() && !databaseCreateNew()">
|
||||
<input type="checkbox" id="collectionSharedThroughput"
|
||||
data-bind="checked: collectionWithThroughputInShared, attr: {title:collectionWithThroughputInSharedTitle}" />
|
||||
@@ -434,7 +440,8 @@
|
||||
level.</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Provision collection throughput checkox - end -->
|
||||
<!-- Provision collection throughput checkbox - end -->
|
||||
|
||||
<!-- Provision collection throughput spinner - start -->
|
||||
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
|
||||
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
|
||||
@@ -467,6 +474,7 @@
|
||||
</throughput-input-autopilot-v3>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
|
||||
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
|
||||
<!-- 3 -->
|
||||
@@ -499,8 +507,10 @@
|
||||
</throughput-input>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- Provision collection throughput spinner - end -->
|
||||
<!-- /ko -->
|
||||
<!-- Provision collection throughput - end -->
|
||||
|
||||
<!-- Enable analytical storage - start -->
|
||||
<div class="enableAnalyticalStorage pkPadding" aria-label="Enable Analytical Store"
|
||||
data-bind="visible: isSynapseLinkSupported">
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import AddCollectionPane from "./AddCollectionPane";
|
||||
import Explorer from "../Explorer";
|
||||
import ko from "knockout";
|
||||
import { AutopilotTier } from "../../Contracts/DataModels";
|
||||
import { AutopilotTier, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
|
||||
describe("Add Collection Pane", () => {
|
||||
describe("isValid()", () => {
|
||||
let explorer: Explorer;
|
||||
const mockDatabaseAccount: ViewModels.DatabaseAccount = {
|
||||
const mockDatabaseAccount: DatabaseAccount = {
|
||||
id: "mock",
|
||||
kind: "DocumentDB",
|
||||
location: "",
|
||||
@@ -24,7 +23,7 @@ describe("Add Collection Pane", () => {
|
||||
tags: []
|
||||
};
|
||||
|
||||
const mockFreeTierDatabaseAccount: ViewModels.DatabaseAccount = {
|
||||
const mockFreeTierDatabaseAccount: DatabaseAccount = {
|
||||
id: "mock",
|
||||
kind: "DocumentDB",
|
||||
location: "",
|
||||
@@ -41,7 +40,7 @@ describe("Add Collection Pane", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ import { createMongoCollectionWithARM, createMongoCollectionWithProxy } from "..
|
||||
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { refreshCachedResources, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
|
||||
|
||||
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
||||
isPreferredApiTable: ko.Computed<boolean>;
|
||||
databaseId?: string;
|
||||
databaseSelfLink?: string;
|
||||
}
|
||||
|
||||
export default class AddCollectionPane extends ContextualPaneBase {
|
||||
public defaultExperience: ko.Computed<string>;
|
||||
@@ -93,15 +100,19 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
public canExceedMaximumValue: ko.PureComputed<boolean>;
|
||||
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
|
||||
public ruToolTipText: ko.Computed<string>;
|
||||
public canConfigureThroughput: ko.PureComputed<boolean>;
|
||||
public showUpsellMessage: ko.PureComputed<boolean>;
|
||||
|
||||
private _databaseOffers: HashMap<DataModels.Offer>;
|
||||
private _isSynapseLinkEnabled: ko.Computed<boolean>;
|
||||
|
||||
constructor(options: ViewModels.AddCollectionPaneOptions) {
|
||||
constructor(options: AddCollectionPaneOptions) {
|
||||
super(options);
|
||||
this._databaseOffers = new HashMap<DataModels.Offer>();
|
||||
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
|
||||
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
|
||||
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.formWarnings = ko.observable<string>();
|
||||
this.collectionId = ko.observable<string>();
|
||||
this.databaseId = ko.observable<string>();
|
||||
@@ -591,6 +602,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
if (config.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.container.isServerlessEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.container.isPreferredApiDocumentDB()) {
|
||||
return true;
|
||||
}
|
||||
@@ -609,7 +625,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
this._isSynapseLinkEnabled = ko.computed(() => {
|
||||
const databaseAccount =
|
||||
(this.container && this.container.databaseAccount && this.container.databaseAccount()) ||
|
||||
({} as ViewModels.DatabaseAccount);
|
||||
({} as DataModels.DatabaseAccount);
|
||||
const properties = databaseAccount.properties || ({} as DataModels.DatabaseAccountExtendedProperties);
|
||||
|
||||
// TODO: remove check for capability once all accounts have been migrated
|
||||
@@ -723,10 +739,19 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
private _computeOfferThroughput(): number {
|
||||
if (this.databaseCreateNewShared()) {
|
||||
return this.isSharedAutoPilotSelected() ? undefined : this._getThroughput();
|
||||
if (!this.canConfigureThroughput()) {
|
||||
return undefined;
|
||||
}
|
||||
return this.isAutoPilotSelected() ? undefined : this._getThroughput();
|
||||
|
||||
if (this.isAutoPilotSelected()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._getThroughput();
|
||||
}
|
||||
|
||||
public submit() {
|
||||
@@ -923,8 +948,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
)
|
||||
);
|
||||
} else {
|
||||
createCollectionFunc = () =>
|
||||
this.container.documentClientUtility.getOrCreateDatabaseAndCollection(createRequest, options);
|
||||
createCollectionFunc = () => getOrCreateDatabaseAndCollection(createRequest, options);
|
||||
}
|
||||
|
||||
createCollectionFunc().then(
|
||||
@@ -960,7 +984,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
};
|
||||
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
|
||||
this.resetData();
|
||||
return this.container.documentClientUtility.refreshCachedResources().then(() => {
|
||||
return refreshCachedResources().then(() => {
|
||||
this.container.refreshAllDatabases();
|
||||
});
|
||||
},
|
||||
@@ -1248,10 +1272,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
: SharedConstants.CollectionCreation.NumberOfPartitionsInUnlimitedCollection;
|
||||
}
|
||||
|
||||
private _convertShardKeyToPartitionKey(
|
||||
shardKey: string,
|
||||
configurationOverrides: ViewModels.ConfigurationOverrides
|
||||
): string {
|
||||
private _convertShardKeyToPartitionKey(shardKey: string): string {
|
||||
if (!shardKey) {
|
||||
return shardKey;
|
||||
}
|
||||
@@ -1313,10 +1334,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
};
|
||||
if (this.container.isPreferredApiMongoDB()) {
|
||||
transform = (value: string) => {
|
||||
return this._convertShardKeyToPartitionKey(
|
||||
value,
|
||||
this.container.databaseAccount().properties.configurationOverrides
|
||||
);
|
||||
return this._convertShardKeyToPartitionKey(value);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<!-- Add database errors - End -->
|
||||
|
||||
<!-- upsell message - start -->
|
||||
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: formErrors && !formErrors()">
|
||||
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()">
|
||||
<div class="infoBoxContent">
|
||||
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo"></span>
|
||||
<span class="infoBoxDetails">
|
||||
@@ -76,6 +76,9 @@
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'" placeholder="Type a new database id"
|
||||
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus"
|
||||
aria-label="Database id" autofocus>
|
||||
|
||||
<!-- Database provisioned throughput - Start -->
|
||||
<!-- ko if: canConfigureThroughput -->
|
||||
<div class="databaseProvision" aria-label="New database provision support">
|
||||
<input tabindex="0" type="checkbox" id="addDatabasePane-databaseSharedThroughput"
|
||||
title="Provision shared throughput" data-bind="checked: databaseCreateNewShared" />
|
||||
@@ -156,6 +159,7 @@
|
||||
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
<!-- Database provisioned throughput - End -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,12 @@ import * as Constants from "../../Common/Constants";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer";
|
||||
import AddDatabasePane from "./AddDatabasePane";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
|
||||
describe("Add Database Pane", () => {
|
||||
describe("getSharedThroughputDefault()", () => {
|
||||
let explorer: Explorer;
|
||||
const mockDatabaseAccount: ViewModels.DatabaseAccount = {
|
||||
const mockDatabaseAccount: DatabaseAccount = {
|
||||
id: "mock",
|
||||
kind: "DocumentDB",
|
||||
location: "",
|
||||
@@ -22,7 +23,7 @@ describe("Add Database Pane", () => {
|
||||
tags: []
|
||||
};
|
||||
|
||||
const mockFreeTierDatabaseAccount: ViewModels.DatabaseAccount = {
|
||||
const mockFreeTierDatabaseAccount: DatabaseAccount = {
|
||||
id: "mock",
|
||||
kind: "DocumentDB",
|
||||
location: "",
|
||||
@@ -40,7 +41,6 @@ describe("Add Database Pane", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({
|
||||
documentClientUtility: null,
|
||||
notificationsClient: null,
|
||||
isEmulator: false
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { CosmosClient } from "../../Common/CosmosClient";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
|
||||
|
||||
export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public defaultExperience: ko.Computed<string>;
|
||||
@@ -49,6 +50,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
|
||||
public ruToolTipText: ko.Computed<string>;
|
||||
public isFreeTierAccount: ko.Computed<boolean>;
|
||||
public canConfigureThroughput: ko.PureComputed<boolean>;
|
||||
public showUpsellMessage: ko.PureComputed<boolean>;
|
||||
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
@@ -56,6 +59,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
this.databaseId = ko.observable<string>();
|
||||
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
|
||||
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
|
||||
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
|
||||
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
|
||||
|
||||
@@ -330,10 +335,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
) {
|
||||
AddDbUtilities.createSqlDatabase(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings).then(
|
||||
() => {
|
||||
Promise.all([
|
||||
this.container.documentClientUtility.refreshCachedOffers(),
|
||||
this.container.documentClientUtility.refreshCachedResources()
|
||||
]).then(() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
||||
});
|
||||
}
|
||||
@@ -350,10 +352,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
createDatabaseParameters,
|
||||
autoPilotSettings
|
||||
).then(() => {
|
||||
Promise.all([
|
||||
this.container.documentClientUtility.refreshCachedOffers(),
|
||||
this.container.documentClientUtility.refreshCachedResources()
|
||||
]).then(() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
||||
});
|
||||
});
|
||||
@@ -369,10 +368,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
createDatabaseParameters,
|
||||
autoPilotSettings
|
||||
).then(() => {
|
||||
Promise.all([
|
||||
this.container.documentClientUtility.refreshCachedOffers(),
|
||||
this.container.documentClientUtility.refreshCachedResources()
|
||||
]).then(() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
||||
});
|
||||
});
|
||||
@@ -409,7 +405,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
autoPilot,
|
||||
hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag()
|
||||
};
|
||||
this.container.documentClientUtility.createDatabase(createRequest).then(
|
||||
createDatabase(createRequest).then(
|
||||
(database: DataModels.Database) => {
|
||||
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
|
||||
},
|
||||
@@ -460,10 +456,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
startKey: number
|
||||
): void {
|
||||
AddDbUtilities.createCassandraKeyspace(armEndpoint, createKeyspaceParameters, autoPilotSettings).then(() => {
|
||||
Promise.all([
|
||||
this.container.documentClientUtility.refreshCachedOffers(),
|
||||
this.container.documentClientUtility.refreshCachedResources()
|
||||
]).then(() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createKeyspaceParameters.offerThroughput, startKey);
|
||||
});
|
||||
});
|
||||
@@ -522,7 +515,15 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
private _computeOfferThroughput(): number {
|
||||
return this.isAutoPilotSelected() ? undefined : this._getThroughput();
|
||||
if (!this.canConfigureThroughput()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.isAutoPilotSelected()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._getThroughput();
|
||||
}
|
||||
|
||||
private _isValid(): boolean {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import QueryTab from "../Tabs/QueryTab";
|
||||
|
||||
export class BrowseQueriesPane extends ContextualPaneBase {
|
||||
public queriesGridComponentAdapter: QueriesGridComponentAdapter;
|
||||
@@ -87,7 +88,7 @@ export class BrowseQueriesPane extends ContextualPaneBase {
|
||||
} else {
|
||||
selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
}
|
||||
const queryTab: ViewModels.QueryTab = this.container.tabsManager.activeTab() as ViewModels.QueryTab;
|
||||
const queryTab = this.container.tabsManager.activeTab() as QueryTab;
|
||||
queryTab.tabTitle(savedQuery.queryName);
|
||||
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
|
||||
queryTab.initialEditorContent(savedQuery.query);
|
||||
|
||||
@@ -118,6 +118,8 @@
|
||||
<option data-bind="value: $data.id"> </option>
|
||||
</datalist>
|
||||
|
||||
<!-- Database provisioned throughput - Start -->
|
||||
<!-- ko if: canConfigureThroughput -->
|
||||
<div
|
||||
class="databaseProvision"
|
||||
aria-label="New database provision support"
|
||||
@@ -202,6 +204,8 @@
|
||||
</throughput-input>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
<!-- Database provisioned throughput - End -->
|
||||
</div>
|
||||
<div class="seconddivpadding">
|
||||
<p>
|
||||
@@ -231,6 +235,9 @@
|
||||
style="height:125px; width: calc(100% - 80px); resize: vertical;"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Provision table throughput - start -->
|
||||
<!-- ko if: canConfigureThroughput -->
|
||||
<div class="seconddivpadding" data-bind="visible: keyspaceHasSharedOffer() && !keyspaceCreateNew()">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -315,6 +322,8 @@
|
||||
</throughput-input>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
<!-- Provision table throughput - end -->
|
||||
</div>
|
||||
<div class="paneFooter">
|
||||
<div class="leftpanel-okbut">
|
||||
|
||||
@@ -51,6 +51,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
|
||||
public isFreeTierAccount: ko.Computed<boolean>;
|
||||
public ruToolTipText: ko.Computed<string>;
|
||||
public canConfigureThroughput: ko.PureComputed<boolean>;
|
||||
|
||||
private keyspaceOffers: HashMap<DataModels.Offer>;
|
||||
|
||||
@@ -61,6 +62,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
this.keyspaceCreateNew = ko.observable<boolean>(true);
|
||||
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
|
||||
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
|
||||
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.keyspaceOffers = new HashMap<DataModels.Offer>();
|
||||
this.keyspaceIds = ko.observableArray<string>();
|
||||
this.keyspaceHasSharedOffer = ko.observable<boolean>(false);
|
||||
@@ -365,7 +367,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`;
|
||||
let createTableQuery: string;
|
||||
|
||||
if (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer()) {
|
||||
if (this.canConfigureThroughput() && (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer())) {
|
||||
if (this.isAutoPilotSelected() && this.selectedAutoPilotThroughput()) {
|
||||
createTableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${this.selectedAutoPilotThroughput()};`;
|
||||
} else {
|
||||
|
||||
@@ -5,11 +5,10 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import { KeyCodes } from "../../Common/Constants";
|
||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
// TODO: Use specific actions for logging telemetry data
|
||||
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel implements ViewModels.ContextualPane {
|
||||
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
|
||||
public id: string;
|
||||
public container: Explorer;
|
||||
public firstFieldHasFocus: ko.Observable<boolean>;
|
||||
@@ -17,14 +16,12 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel imple
|
||||
public formErrors: ko.Observable<string>;
|
||||
public title: ko.Observable<string>;
|
||||
public visible: ko.Observable<boolean>;
|
||||
public documentClientUtility: DocumentClientUtilityBase;
|
||||
public isExecuting: ko.Observable<boolean>;
|
||||
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super();
|
||||
this.id = options.id;
|
||||
this.container = options.container;
|
||||
this.documentClientUtility = options.documentClientUtility;
|
||||
this.visible = options.visible || ko.observable(false);
|
||||
this.firstFieldHasFocus = ko.observable<boolean>(false);
|
||||
this.formErrors = ko.observable<string>();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
jest.mock("../../Common/dataAccess/deleteCollection");
|
||||
import * as ko from "knockout";
|
||||
import * as sinon from "sinon";
|
||||
import Q from "q";
|
||||
@@ -6,17 +7,17 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import DeleteCollectionConfirmationPane from "./DeleteCollectionConfirmationPane";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
|
||||
import Explorer from "../Explorer";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { TreeNode } from "../../Contracts/ViewModels";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
|
||||
describe("Delete Collection Confirmation Pane", () => {
|
||||
describe("Explorer.isLastCollection()", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
});
|
||||
|
||||
it("should be true if 1 database and 1 collection", () => {
|
||||
@@ -55,15 +56,11 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
let fakeDocumentClientUtility = sinon.createStubInstance<DocumentClientUtilityBase>(
|
||||
DocumentClientUtilityBase as any
|
||||
);
|
||||
let fakeExplorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
|
||||
let fakeExplorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
let pane = new DeleteCollectionConfirmationPane({
|
||||
documentClientUtility: fakeDocumentClientUtility as any,
|
||||
id: "deletecollectionconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: fakeExplorer
|
||||
@@ -87,6 +84,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
let telemetryProcessorSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
(deleteCollection as jest.Mock).mockResolvedValue(undefined);
|
||||
telemetryProcessorSpy = sinon.spy(TelemetryProcessor, "trace");
|
||||
});
|
||||
|
||||
@@ -96,8 +94,6 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
|
||||
it("it should log feedback if last collection and database is not shared", () => {
|
||||
let selectedCollectionId = "testCol";
|
||||
let fakeDocumentClientUtility = {} as DocumentClientUtilityBase;
|
||||
fakeDocumentClientUtility.deleteCollection = () => Q(null);
|
||||
let fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.findSelectedCollection = () => {
|
||||
return {
|
||||
@@ -110,24 +106,22 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
const SubscriptionId = "testId";
|
||||
const AccountName = "testAccount";
|
||||
fakeExplorer.databaseAccount = ko.observable<ViewModels.DatabaseAccount>({
|
||||
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
|
||||
id: SubscriptionId,
|
||||
name: AccountName
|
||||
} as ViewModels.DatabaseAccount);
|
||||
} as DataModels.DatabaseAccount);
|
||||
|
||||
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
|
||||
fakeExplorer.isPreferredApiCassandra = ko.computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
fakeExplorer.documentClientUtility = fakeDocumentClientUtility;
|
||||
fakeExplorer.selectedNode = ko.observable<TreeNode>();
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
let pane = new DeleteCollectionConfirmationPane({
|
||||
documentClientUtility: fakeDocumentClientUtility as any,
|
||||
id: "deletecollectionconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: fakeExplorer as any
|
||||
|
||||
@@ -9,8 +9,9 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
|
||||
export default class DeleteCollectionConfirmationPane extends ContextualPaneBase {
|
||||
public collectionIdConfirmationText: ko.Observable<string>;
|
||||
@@ -28,7 +29,7 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
||||
this.resetData();
|
||||
}
|
||||
|
||||
public submit(): Q.Promise<any> {
|
||||
public submit(): Promise<any> {
|
||||
if (!this._isValid()) {
|
||||
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
|
||||
this.formErrors("Input collection name does not match the selected collection");
|
||||
@@ -36,7 +37,7 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting collection ${selectedCollection && selectedCollection.id()}: ${this.formErrors()}`
|
||||
);
|
||||
return Q.resolve();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.formErrors("");
|
||||
@@ -49,25 +50,23 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: this.title()
|
||||
});
|
||||
let promise: Q.Promise<any>;
|
||||
let promise: Promise<any>;
|
||||
if (this.container.isPreferredApiCassandra()) {
|
||||
promise = (<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
|
||||
promise = ((<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
|
||||
this.container.databaseAccount().properties.cassandraEndpoint,
|
||||
this.container.databaseAccount().id,
|
||||
`DROP TABLE ${selectedCollection.databaseId}.${selectedCollection.id()};`,
|
||||
this.container
|
||||
);
|
||||
) as unknown) as Promise<any>;
|
||||
} else {
|
||||
promise = this.container.documentClientUtility.deleteCollection(selectedCollection);
|
||||
promise = deleteCollection(selectedCollection.databaseId, selectedCollection.id());
|
||||
}
|
||||
return promise.then(
|
||||
() => {
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
this.container.selectedNode(selectedCollection.database);
|
||||
this.container.tabsManager?.closeTabsByComparator(
|
||||
(tab: ViewModels.Tab) => tab.node && tab.node.rid === selectedCollection.rid
|
||||
);
|
||||
this.container.tabsManager?.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedCollection.rid);
|
||||
this.container.refreshAllDatabases();
|
||||
this.resetData();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
jest.mock("../../Common/dataAccess/deleteDatabase");
|
||||
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
|
||||
import * as ko from "knockout";
|
||||
import * as sinon from "sinon";
|
||||
import Q from "q";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import DeleteDatabaseConfirmationPane from "./DeleteDatabaseConfirmationPane";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
|
||||
import Explorer from "../Explorer";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { TreeNode } from "../../Contracts/ViewModels";
|
||||
import { TabsManager } from "../Tabs/TabsManager";
|
||||
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
|
||||
|
||||
describe("Delete Database Confirmation Pane", () => {
|
||||
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeAll(() => {
|
||||
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
});
|
||||
|
||||
it("should be true if only 1 database", () => {
|
||||
@@ -49,12 +54,10 @@ describe("Delete Database Confirmation Pane", () => {
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
|
||||
let fakeDocumentClientUtility = {} as DocumentClientUtilityBase;
|
||||
let fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||
|
||||
let pane = new DeleteDatabaseConfirmationPane({
|
||||
documentClientUtility: fakeDocumentClientUtility as any,
|
||||
id: "deletedatabaseconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: fakeExplorer as any
|
||||
@@ -78,20 +81,8 @@ describe("Delete Database Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
describe("submit()", () => {
|
||||
let telemetryProcessorSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
telemetryProcessorSpy = sinon.spy(TelemetryProcessor, "trace");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
telemetryProcessorSpy.restore();
|
||||
});
|
||||
|
||||
it("on submit() it should log feedback if last non empty database or is last database that has shared throughput", () => {
|
||||
let selectedDatabaseId = "testDB";
|
||||
let fakeDocumentClientUtility = {} as DocumentClientUtilityBase;
|
||||
fakeDocumentClientUtility.deleteDatabase = () => Q.resolve(null);
|
||||
let fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.findSelectedDatabase = () => {
|
||||
return {
|
||||
@@ -106,21 +97,19 @@ describe("Delete Database Confirmation Pane", () => {
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
const SubscriptionId = "testId";
|
||||
const AccountName = "testAccount";
|
||||
fakeExplorer.databaseAccount = ko.observable<ViewModels.DatabaseAccount>({
|
||||
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
|
||||
id: SubscriptionId,
|
||||
name: AccountName
|
||||
} as ViewModels.DatabaseAccount);
|
||||
} as DataModels.DatabaseAccount);
|
||||
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
|
||||
fakeExplorer.isPreferredApiCassandra = ko.computed(() => {
|
||||
return false;
|
||||
});
|
||||
fakeExplorer.documentClientUtility = fakeDocumentClientUtility;
|
||||
fakeExplorer.selectedNode = ko.observable<TreeNode>();
|
||||
fakeExplorer.tabsManager = new TabsManager();
|
||||
fakeExplorer.isLastNonEmptyDatabase = () => true;
|
||||
|
||||
let pane = new DeleteDatabaseConfirmationPane({
|
||||
documentClientUtility: fakeDocumentClientUtility as any,
|
||||
id: "deletedatabaseconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: fakeExplorer as any
|
||||
@@ -130,15 +119,12 @@ describe("Delete Database Confirmation Pane", () => {
|
||||
pane.databaseDeleteFeedback(Feedback);
|
||||
|
||||
return pane.submit().then(() => {
|
||||
expect(telemetryProcessorSpy.called).toBe(true);
|
||||
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
|
||||
expect(
|
||||
telemetryProcessorSpy.calledWith(
|
||||
Action.DeleteDatabase,
|
||||
ActionModifiers.Mark,
|
||||
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
||||
)
|
||||
).toBe(true);
|
||||
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(
|
||||
Action.DeleteDatabase,
|
||||
ActionModifiers.Mark,
|
||||
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,9 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
|
||||
|
||||
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
||||
public databaseIdConfirmationText: ko.Observable<string>;
|
||||
@@ -50,6 +51,7 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: this.title()
|
||||
});
|
||||
// TODO: Should not be a Q promise anymore, but the Cassandra code requires it
|
||||
let promise: Q.Promise<any>;
|
||||
if (this.container.isPreferredApiCassandra()) {
|
||||
promise = (<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
|
||||
@@ -59,23 +61,19 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
||||
this.container
|
||||
);
|
||||
} else {
|
||||
promise = this.container.documentClientUtility.deleteDatabase(selectedDatabase);
|
||||
promise = Q(deleteDatabase(selectedDatabase.id()));
|
||||
}
|
||||
return promise.then(
|
||||
() => {
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
this.container.refreshAllDatabases();
|
||||
this.container.tabsManager.closeTabsByComparator(
|
||||
(tab: ViewModels.Tab) => tab.node && tab.node.rid === selectedDatabase.rid
|
||||
);
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedDatabase.rid);
|
||||
this.container.selectedNode(null);
|
||||
selectedDatabase
|
||||
.collections()
|
||||
.forEach((collection: ViewModels.Collection) =>
|
||||
this.container.tabsManager.closeTabsByComparator(
|
||||
(tab: ViewModels.Tab) => tab.node && tab.node.rid === collection.rid
|
||||
)
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === collection.rid)
|
||||
);
|
||||
this.resetData();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { JunoUtils } from "../../Utils/JunoUtils";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
|
||||
import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent";
|
||||
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
|
||||
@@ -16,6 +16,11 @@ import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
|
||||
interface GitHubReposPaneOptions extends ViewModels.PaneOptions {
|
||||
gitHubClient: GitHubClient;
|
||||
junoClient: JunoClient;
|
||||
}
|
||||
|
||||
export class GitHubReposPane extends ContextualPaneBase {
|
||||
private static readonly PageSize = 30;
|
||||
|
||||
@@ -33,7 +38,7 @@ export class GitHubReposPane extends ContextualPaneBase {
|
||||
private allGitHubReposLastPageInfo?: IGitHubPageInfo;
|
||||
private pinnedReposUpdated: boolean;
|
||||
|
||||
constructor(options: ViewModels.GitHubReposPaneOptions) {
|
||||
constructor(options: GitHubReposPaneOptions) {
|
||||
super(options);
|
||||
|
||||
this.gitHubClient = options.gitHubClient;
|
||||
|
||||
@@ -5,7 +5,8 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import QueryTab from "../Tabs/QueryTab";
|
||||
|
||||
export class LoadQueryPane extends ContextualPaneBase {
|
||||
public selectedFilesTitle: ko.Observable<string>;
|
||||
@@ -111,7 +112,7 @@ export class LoadQueryPane extends ContextualPaneBase {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt: any): void => {
|
||||
const fileData: string = evt.target.result;
|
||||
const queryTab: ViewModels.QueryTab = this.container.tabsManager.activeTab() as ViewModels.QueryTab;
|
||||
const queryTab = this.container.tabsManager.activeTab() as QueryTab;
|
||||
queryTab.initialEditorContent(fileData);
|
||||
queryTab.sqlQueryEditorContent(fileData);
|
||||
deferred.resolve();
|
||||
|
||||
@@ -1,156 +1,178 @@
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||
|
||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
parameters: ko.Observable<number>;
|
||||
private isOpened: boolean;
|
||||
private isExecuting: boolean;
|
||||
private formError: string;
|
||||
private formErrorDetail: string;
|
||||
|
||||
private name: string;
|
||||
private author: string;
|
||||
private content: string;
|
||||
private description: string;
|
||||
private tags: string;
|
||||
private imageSrc: string;
|
||||
|
||||
constructor(private container: Explorer, private junoClient: JunoClient) {
|
||||
this.parameters = ko.observable(Date.now());
|
||||
this.reset();
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (!this.isOpened) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const props: GenericRightPaneProps = {
|
||||
container: this.container,
|
||||
content: this.createContent(),
|
||||
formError: this.formError,
|
||||
formErrorDetail: this.formErrorDetail,
|
||||
id: "publishnotebookpane",
|
||||
isExecuting: this.isExecuting,
|
||||
title: "Publish to gallery",
|
||||
submitButtonText: "Publish",
|
||||
onClose: () => this.close(),
|
||||
onSubmit: () => this.submit()
|
||||
};
|
||||
|
||||
return <GenericRightPaneComponent {...props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
|
||||
public open(name: string, author: string, content: string): void {
|
||||
this.name = name;
|
||||
this.author = author;
|
||||
this.content = content;
|
||||
|
||||
this.isOpened = true;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.reset();
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public async submit(): Promise<void> {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Publishing ${this.name} to gallery`
|
||||
);
|
||||
this.isExecuting = true;
|
||||
this.triggerRender();
|
||||
|
||||
try {
|
||||
if (!this.name || !this.description || !this.author) {
|
||||
throw new Error("Name, description, and author are required");
|
||||
}
|
||||
|
||||
const response = await this.junoClient.publishNotebook(
|
||||
this.name,
|
||||
this.description,
|
||||
this.tags?.split(","),
|
||||
this.author,
|
||||
this.imageSrc,
|
||||
this.content
|
||||
);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||
} catch (error) {
|
||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||
this.formErrorDetail = `${error}`;
|
||||
|
||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
return;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
this.isExecuting = false;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
|
||||
this.formError = formError;
|
||||
this.formErrorDetail = formErrorDetail;
|
||||
|
||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||
Logger.logError(message, area);
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
this.triggerRender();
|
||||
};
|
||||
|
||||
private clearFormError = (): void => {
|
||||
this.formError = undefined;
|
||||
this.formErrorDetail = undefined;
|
||||
this.triggerRender();
|
||||
};
|
||||
|
||||
private createContent = (): JSX.Element => {
|
||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||
notebookName: this.name,
|
||||
notebookDescription: "",
|
||||
notebookTags: "",
|
||||
notebookAuthor: this.author,
|
||||
notebookCreatedDate: new Date().toISOString(),
|
||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
||||
onError: this.createFormErrorForLargeImageSelection,
|
||||
clearFormError: this.clearFormError
|
||||
};
|
||||
|
||||
return <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
||||
};
|
||||
|
||||
private reset = (): void => {
|
||||
this.isOpened = false;
|
||||
this.isExecuting = false;
|
||||
this.formError = undefined;
|
||||
this.formErrorDetail = undefined;
|
||||
this.name = undefined;
|
||||
this.author = undefined;
|
||||
this.content = undefined;
|
||||
};
|
||||
}
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import Explorer from "../Explorer";
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||
import { ImmutableNotebook } from "@nteract/commutable/src";
|
||||
import { toJS } from "@nteract/commutable";
|
||||
|
||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
parameters: ko.Observable<number>;
|
||||
private isOpened: boolean;
|
||||
private isExecuting: boolean;
|
||||
private formError: string;
|
||||
private formErrorDetail: string;
|
||||
|
||||
private name: string;
|
||||
private author: string;
|
||||
private content: string;
|
||||
private description: string;
|
||||
private tags: string;
|
||||
private imageSrc: string;
|
||||
private notebookObject: ImmutableNotebook;
|
||||
private parentDomElement: HTMLElement;
|
||||
|
||||
constructor(private container: Explorer, private junoClient: JunoClient) {
|
||||
this.parameters = ko.observable(Date.now());
|
||||
this.reset();
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (!this.isOpened) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const props: GenericRightPaneProps = {
|
||||
container: this.container,
|
||||
content: this.createContent(),
|
||||
formError: this.formError,
|
||||
formErrorDetail: this.formErrorDetail,
|
||||
id: "publishnotebookpane",
|
||||
isExecuting: this.isExecuting,
|
||||
title: "Publish to gallery",
|
||||
submitButtonText: "Publish",
|
||||
onClose: () => this.close(),
|
||||
onSubmit: () => this.submit()
|
||||
};
|
||||
|
||||
return <GenericRightPaneComponent {...props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
|
||||
public open(
|
||||
name: string,
|
||||
author: string,
|
||||
notebookContent: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement
|
||||
): void {
|
||||
this.name = name;
|
||||
this.author = author;
|
||||
if (typeof notebookContent === "string") {
|
||||
this.content = notebookContent as string;
|
||||
} else {
|
||||
this.content = JSON.stringify(toJS(notebookContent as ImmutableNotebook));
|
||||
this.notebookObject = notebookContent;
|
||||
}
|
||||
this.parentDomElement = parentDomElement;
|
||||
|
||||
this.isOpened = true;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.reset();
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public async submit(): Promise<void> {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Publishing ${this.name} to gallery`
|
||||
);
|
||||
this.isExecuting = true;
|
||||
this.triggerRender();
|
||||
|
||||
try {
|
||||
if (!this.name || !this.description || !this.author) {
|
||||
throw new Error("Name, description, and author are required");
|
||||
}
|
||||
|
||||
const response = await this.junoClient.publishNotebook(
|
||||
this.name,
|
||||
this.description,
|
||||
this.tags?.split(","),
|
||||
this.author,
|
||||
this.imageSrc,
|
||||
this.content
|
||||
);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||
} catch (error) {
|
||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||
this.formErrorDetail = `${error}`;
|
||||
|
||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
return;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
this.isExecuting = false;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
|
||||
this.formError = formError;
|
||||
this.formErrorDetail = formErrorDetail;
|
||||
|
||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||
Logger.logError(message, area);
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
this.triggerRender();
|
||||
};
|
||||
|
||||
private clearFormError = (): void => {
|
||||
this.formError = undefined;
|
||||
this.formErrorDetail = undefined;
|
||||
this.triggerRender();
|
||||
};
|
||||
|
||||
private createContent = (): JSX.Element => {
|
||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||
notebookName: this.name,
|
||||
notebookDescription: "",
|
||||
notebookTags: "",
|
||||
notebookAuthor: this.author,
|
||||
notebookCreatedDate: new Date().toISOString(),
|
||||
notebookObject: this.notebookObject,
|
||||
notebookParentDomElement: this.parentDomElement,
|
||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
||||
onError: this.createFormErrorForLargeImageSelection,
|
||||
clearFormError: this.clearFormError
|
||||
};
|
||||
|
||||
return <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
||||
};
|
||||
|
||||
private reset = (): void => {
|
||||
this.isOpened = false;
|
||||
this.isExecuting = false;
|
||||
this.formError = undefined;
|
||||
this.formErrorDetail = undefined;
|
||||
this.name = undefined;
|
||||
this.author = undefined;
|
||||
this.content = undefined;
|
||||
this.description = undefined;
|
||||
this.tags = undefined;
|
||||
this.imageSrc = undefined;
|
||||
this.notebookObject = undefined;
|
||||
this.parentDomElement = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ describe("PublishNotebookPaneComponent", () => {
|
||||
notebookTags: "tag1, tag2",
|
||||
notebookAuthor: "CosmosDB",
|
||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||
notebookObject: undefined,
|
||||
notebookParentDomElement: undefined,
|
||||
onChangeDescription: undefined,
|
||||
onChangeTags: undefined,
|
||||
onChangeImageSrc: undefined,
|
||||
|
||||
@@ -3,6 +3,9 @@ import * as React from "react";
|
||||
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
|
||||
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
|
||||
import "./PublishNotebookPaneComponent.less";
|
||||
import Html2Canvas from "html2canvas";
|
||||
import { ImmutableNotebook } from "@nteract/commutable/src";
|
||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||
|
||||
export interface PublishNotebookPaneProps {
|
||||
notebookName: string;
|
||||
@@ -10,6 +13,8 @@ export interface PublishNotebookPaneProps {
|
||||
notebookTags: string;
|
||||
notebookAuthor: string;
|
||||
notebookCreatedDate: string;
|
||||
notebookObject: ImmutableNotebook;
|
||||
notebookParentDomElement: HTMLElement;
|
||||
onChangeDescription: (newValue: string) => void;
|
||||
onChangeTags: (newValue: string) => void;
|
||||
onChangeImageSrc: (newValue: string) => void;
|
||||
@@ -24,9 +29,15 @@ interface PublishNotebookPaneState {
|
||||
imageSrc: string;
|
||||
}
|
||||
|
||||
enum ImageTypes {
|
||||
Url = "URL",
|
||||
CustomImage = "Custom Image",
|
||||
TakeScreenshot = "Take Screenshot",
|
||||
UseFirstDisplayOutput = "Use First Display Output"
|
||||
}
|
||||
|
||||
export class PublishNotebookPaneComponent extends React.Component<PublishNotebookPaneProps, PublishNotebookPaneState> {
|
||||
private static readonly maxImageSizeInMib = 1.5;
|
||||
private static readonly ImageTypes = ["URL", "Custom Image"];
|
||||
private descriptionPara1: string;
|
||||
private descriptionPara2: string;
|
||||
private descriptionProps: ITextFieldProps;
|
||||
@@ -34,12 +45,13 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
private thumbnailUrlProps: ITextFieldProps;
|
||||
private thumbnailSelectorProps: IDropdownProps;
|
||||
private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void;
|
||||
private takeScreenshot: (target: HTMLElement, onError: (error: Error) => void) => void;
|
||||
|
||||
constructor(props: PublishNotebookPaneProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
type: PublishNotebookPaneComponent.ImageTypes[0],
|
||||
type: ImageTypes.Url,
|
||||
notebookDescription: "",
|
||||
notebookTags: "",
|
||||
imageSrc: undefined
|
||||
@@ -48,12 +60,12 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
this.imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function() {
|
||||
reader.onload = () => {
|
||||
updateImageSrc(reader.result.toString());
|
||||
};
|
||||
|
||||
const onError = this.props.onError;
|
||||
reader.onerror = function(error) {
|
||||
reader.onerror = error => {
|
||||
const formError = `Failed to convert ${file.name} to base64 format`;
|
||||
const formErrorDetail = `${error}`;
|
||||
const area = "PublishNotebookPaneComponent/selectImageFile";
|
||||
@@ -61,6 +73,38 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
};
|
||||
};
|
||||
|
||||
this.takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => {
|
||||
const updateImageSrcWithScreenshot = (canvasUrl: string): void => {
|
||||
this.props.onChangeImageSrc(canvasUrl);
|
||||
this.setState({ imageSrc: canvasUrl });
|
||||
};
|
||||
|
||||
target.scrollIntoView();
|
||||
Html2Canvas(target, {
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
scale: 1,
|
||||
logging: true
|
||||
})
|
||||
.then(canvas => {
|
||||
//redraw canvas to fit Card Cover Image dimensions
|
||||
const originalImageData = canvas.toDataURL();
|
||||
const requiredHeight =
|
||||
parseInt(canvas.style.width.split("px")[0]) * GalleryCardComponent.cardHeightToWidthRatio;
|
||||
canvas.height = requiredHeight;
|
||||
const context = canvas.getContext("2d");
|
||||
const image = new Image();
|
||||
image.src = originalImageData;
|
||||
image.onload = () => {
|
||||
context.drawImage(image, 0, 0);
|
||||
updateImageSrcWithScreenshot(canvas.toDataURL());
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
this.descriptionPara1 =
|
||||
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
|
||||
|
||||
@@ -78,12 +122,45 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
}
|
||||
};
|
||||
|
||||
const screenshotErrorHandler = (error: Error) => {
|
||||
const formError = "Failed to take screen shot";
|
||||
const formErrorDetail = `${error}`;
|
||||
const area = "PublishNotebookPaneComponent/takeScreenshot";
|
||||
this.props.onError(formError, formErrorDetail, area);
|
||||
};
|
||||
|
||||
const firstOutputErrorHandler = (error: Error) => {
|
||||
const formError = "Failed to capture first output";
|
||||
const formErrorDetail = `${error}`;
|
||||
const area = "PublishNotebookPaneComponent/UseFirstOutput";
|
||||
this.props.onError(formError, formErrorDetail, area);
|
||||
};
|
||||
|
||||
this.thumbnailSelectorProps = {
|
||||
label: "Cover image",
|
||||
defaultSelectedKey: PublishNotebookPaneComponent.ImageTypes[0],
|
||||
defaultSelectedKey: ImageTypes.Url,
|
||||
ariaLabel: "Cover image",
|
||||
options: PublishNotebookPaneComponent.ImageTypes.map((value: string) => ({ text: value, key: value })),
|
||||
onChange: (event, options) => {
|
||||
options: [
|
||||
ImageTypes.Url,
|
||||
ImageTypes.CustomImage,
|
||||
ImageTypes.TakeScreenshot,
|
||||
ImageTypes.UseFirstDisplayOutput
|
||||
].map((value: string) => ({ text: value, key: value })),
|
||||
onChange: async (event, options) => {
|
||||
this.props.clearFormError();
|
||||
if (options.text === ImageTypes.TakeScreenshot) {
|
||||
try {
|
||||
await this.takeScreenshot(this.props.notebookParentDomElement, screenshotErrorHandler);
|
||||
} catch (error) {
|
||||
screenshotErrorHandler(error);
|
||||
}
|
||||
} else if (options.text === ImageTypes.UseFirstDisplayOutput) {
|
||||
try {
|
||||
await this.takeScreenshot(this.findFirstOutput(), firstOutputErrorHandler);
|
||||
} catch (error) {
|
||||
firstOutputErrorHandler(error);
|
||||
}
|
||||
}
|
||||
this.setState({ type: options.text });
|
||||
}
|
||||
};
|
||||
@@ -111,6 +188,51 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
};
|
||||
}
|
||||
|
||||
private renderThumbnailSelectors(type: string) {
|
||||
switch (type) {
|
||||
case ImageTypes.Url:
|
||||
return <TextField {...this.thumbnailUrlProps} />;
|
||||
case ImageTypes.CustomImage:
|
||||
return (
|
||||
<input
|
||||
id="selectImageFile"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={event => {
|
||||
const file = event.target.files[0];
|
||||
if (file.size / 1024 ** 2 > PublishNotebookPaneComponent.maxImageSizeInMib) {
|
||||
event.target.value = "";
|
||||
const formError = `Failed to upload ${file.name}`;
|
||||
const formErrorDetail = `Image is larger than ${PublishNotebookPaneComponent.maxImageSizeInMib} MiB. Please Choose a different image.`;
|
||||
const area = "PublishNotebookPaneComponent/selectImageFile";
|
||||
|
||||
this.props.onError(formError, formErrorDetail, area);
|
||||
this.props.onChangeImageSrc(undefined);
|
||||
this.setState({ imageSrc: undefined });
|
||||
return;
|
||||
} else {
|
||||
this.props.clearFormError();
|
||||
}
|
||||
this.imageToBase64(file, (result: string) => {
|
||||
this.props.onChangeImageSrc(result);
|
||||
this.setState({ imageSrc: result });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
private findFirstOutput(): HTMLElement {
|
||||
const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(this.props.notebookObject);
|
||||
const cellOutputDomElements = this.props.notebookParentDomElement.querySelectorAll<HTMLElement>(
|
||||
".nteract-cell-outputs"
|
||||
);
|
||||
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="publishNotebookPanelContent">
|
||||
@@ -135,39 +257,8 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
<Dropdown {...this.thumbnailSelectorProps} />
|
||||
</Stack.Item>
|
||||
|
||||
{this.state.type === PublishNotebookPaneComponent.ImageTypes[0] ? (
|
||||
<Stack.Item>
|
||||
<TextField {...this.thumbnailUrlProps} />
|
||||
</Stack.Item>
|
||||
) : (
|
||||
<Stack.Item>
|
||||
<input
|
||||
id="selectImageFile"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={event => {
|
||||
const file = event.target.files[0];
|
||||
if (file.size / 1024 ** 2 > PublishNotebookPaneComponent.maxImageSizeInMib) {
|
||||
event.target.value = "";
|
||||
const formError = `Failed to upload ${file.name}`;
|
||||
const formErrorDetail = `Image is larger than ${PublishNotebookPaneComponent.maxImageSizeInMib} MiB. Please Choose a different image.`;
|
||||
const area = "PublishNotebookPaneComponent/selectImageFile";
|
||||
<Stack.Item>{this.renderThumbnailSelectors(this.state.type)}</Stack.Item>
|
||||
|
||||
this.props.onError(formError, formErrorDetail, area);
|
||||
this.props.onChangeImageSrc(undefined);
|
||||
this.setState({ imageSrc: undefined });
|
||||
return;
|
||||
} else {
|
||||
this.props.clearFormError();
|
||||
}
|
||||
this.imageToBase64(file, (result: string) => {
|
||||
this.props.onChangeImageSrc(result);
|
||||
this.setState({ imageSrc: result });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<Text>Preview</Text>
|
||||
</Stack.Item>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ConnectionStringParser } from "../../Platform/Hosted/Helpers/Connection
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export class RenewAdHocAccessPane extends ContextualPaneBase {
|
||||
public accessKey: ko.Observable<string>;
|
||||
|
||||
@@ -5,8 +5,9 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import QueryTab from "../Tabs/QueryTab";
|
||||
|
||||
export class SaveQueryPane extends ContextualPaneBase {
|
||||
public queryName: ko.Observable<string>;
|
||||
@@ -34,8 +35,7 @@ export class SaveQueryPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
const queryName: string = this.queryName();
|
||||
const queryTab: ViewModels.QueryTab =
|
||||
this.container && (this.container.tabsManager.activeTab() as ViewModels.QueryTab);
|
||||
const queryTab = this.container && (this.container.tabsManager.activeTab() as QueryTab);
|
||||
const query: string = queryTab && queryTab.sqlQueryEditorContent();
|
||||
if (!queryName || queryName.length === 0) {
|
||||
this.formErrors("No query name specified");
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("Settings Pane", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
});
|
||||
|
||||
it("should be true for SQL API", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { StringUtility } from "../../Shared/StringUtility";
|
||||
import { config } from "../../Config";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user