mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-25 20:01:45 +00:00
Compare commits
28 Commits
hosted-msa
...
hosted-msa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35213a77e2 | ||
|
|
d1ac8eb077 | ||
|
|
d9156c47d0 | ||
|
|
a844042580 | ||
|
|
1238d30f95 | ||
|
|
684cbfe4a0 | ||
|
|
5b6b4d3583 | ||
|
|
e05a78e96a | ||
|
|
33f7ae1e6d | ||
|
|
2089a8881d | ||
|
|
2e1665f093 | ||
|
|
f6d6222e5c | ||
|
|
2147b10361 | ||
|
|
09ac1d1552 | ||
|
|
b81cef2a03 | ||
|
|
731999c4e8 | ||
|
|
aba583abd8 | ||
|
|
2e10b96678 | ||
|
|
5652f29d03 | ||
|
|
15cb4a8fc4 | ||
|
|
bf30c3190a | ||
|
|
585f75bc91 | ||
|
|
7116f25ce4 | ||
|
|
5f5d9176af | ||
|
|
ac2d645fda | ||
|
|
12a44fdd42 | ||
|
|
13dbcb6453 | ||
|
|
cc63cdc1fd |
@@ -241,9 +241,6 @@ src/Platform/Hosted/Authorization.ts
|
||||
src/Platform/Hosted/DataAccessUtility.ts
|
||||
src/Platform/Hosted/ExplorerFactory.ts
|
||||
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
|
||||
src/Platform/Hosted/Helpers/ConnectionStringParser.ts
|
||||
src/Platform/Hosted/HostedUtils.test.ts
|
||||
src/Platform/Hosted/HostedUtils.ts
|
||||
src/Platform/Hosted/Main.ts
|
||||
src/Platform/Hosted/Maint.test.ts
|
||||
src/Platform/Hosted/NotificationsClient.ts
|
||||
|
||||
@@ -43,6 +43,7 @@ module.exports = {
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
|
||||
eqeqeq: "error",
|
||||
"react/display-name": "off",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
|
||||
7
canvas/README.md
Normal file
7
canvas/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Why?
|
||||
|
||||
This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract.
|
||||
|
||||
Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution.
|
||||
|
||||
Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved
|
||||
1
canvas/index.js
Normal file
1
canvas/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {}
|
||||
11
canvas/package.json
Normal file
11
canvas/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "canvas",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
3772
less/documentDB.less
3772
less/documentDB.less
File diff suppressed because it is too large
Load Diff
249
package-lock.json
generated
249
package-lock.json
generated
@@ -5414,11 +5414,6 @@
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
|
||||
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
@@ -5656,6 +5651,7 @@
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
|
||||
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
@@ -6898,14 +6894,7 @@
|
||||
"dev": true
|
||||
},
|
||||
"canvas": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
|
||||
"integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
|
||||
"requires": {
|
||||
"nan": "^2.14.0",
|
||||
"node-pre-gyp": "^0.11.0",
|
||||
"simple-get": "^3.0.3"
|
||||
}
|
||||
"version": "file:canvas"
|
||||
},
|
||||
"capture-exit": {
|
||||
"version": "2.0.0",
|
||||
@@ -7469,7 +7458,8 @@
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"optional": true
|
||||
},
|
||||
"constants-browserify": {
|
||||
"version": "1.0.0",
|
||||
@@ -8450,6 +8440,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
|
||||
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"mimic-response": "^2.0.0"
|
||||
}
|
||||
@@ -8475,7 +8466,8 @@
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"optional": true
|
||||
},
|
||||
"deep-is": {
|
||||
"version": "0.1.3",
|
||||
@@ -8667,7 +8659,8 @@
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
||||
"optional": true
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
@@ -8703,7 +8696,8 @@
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
|
||||
"optional": true
|
||||
},
|
||||
"detect-newline": {
|
||||
"version": "2.1.0",
|
||||
@@ -10689,14 +10683,6 @@
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
|
||||
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
|
||||
"requires": {
|
||||
"minipass": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"fs-observable": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/fs-observable/-/fs-observable-4.1.14.tgz",
|
||||
@@ -10838,6 +10824,7 @@
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
@@ -10852,12 +10839,14 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"optional": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -10866,6 +10855,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -10876,6 +10866,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -11365,7 +11356,8 @@
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
|
||||
"optional": true
|
||||
},
|
||||
"has-value": {
|
||||
"version": "1.0.0",
|
||||
@@ -11847,14 +11839,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
|
||||
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
@@ -15344,6 +15328,15 @@
|
||||
"tinyqueue": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"match-sorter": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.0.2.tgz",
|
||||
"integrity": "sha512-SDRLNlWof9GnAUEyhKP0O5525MMGXUGt+ep4MrrqQ2StAh3zjvICVZseiwg7Zijn3GazpJDiwuRr/mFDHd92NQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"remove-accents": "0.4.2"
|
||||
}
|
||||
},
|
||||
"matchdep": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
|
||||
@@ -15559,7 +15552,8 @@
|
||||
"mimic-response": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
|
||||
"optional": true
|
||||
},
|
||||
"min-document": {
|
||||
"version": "2.19.0",
|
||||
@@ -15616,15 +15610,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minipass-collect": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
|
||||
@@ -15694,14 +15679,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
|
||||
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
|
||||
"requires": {
|
||||
"minipass": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
@@ -15838,9 +15815,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"msal": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/msal/-/msal-1.4.3.tgz",
|
||||
"integrity": "sha512-C90MhgzcBuTSR2BOQ/LQryY1CZVESQLJDdmRDWSsaVde+zwZ2iXD0fWw7zeBd5TzfUCiJEXZVs4lFJ8d/IGbiQ==",
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/msal/-/msal-1.4.4.tgz",
|
||||
"integrity": "sha512-aOBD/L6jAsizDFzKxxvXxH0FEDjp6Inr3Ufi/Y2o7KCFKN+akoE2sLeszEb/0Y3VxHxK0F0ea7xQ/HHTomKivw==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
@@ -15876,7 +15853,8 @@
|
||||
"nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"optional": true
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
@@ -15926,26 +15904,6 @@
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz",
|
||||
"integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
@@ -16114,41 +16072,6 @@
|
||||
"which": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
|
||||
"integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"needle": "^2.2.1",
|
||||
"nopt": "^4.0.1",
|
||||
"npm-packlist": "^1.1.6",
|
||||
"npmlog": "^4.0.2",
|
||||
"rc": "^1.2.7",
|
||||
"rimraf": "^2.6.1",
|
||||
"semver": "^5.3.0",
|
||||
"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",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.66",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
|
||||
@@ -16161,15 +16084,6 @@
|
||||
"integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=",
|
||||
"optional": true
|
||||
},
|
||||
"nopt": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
||||
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
|
||||
"requires": {
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
@@ -16194,29 +16108,6 @@
|
||||
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
|
||||
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
|
||||
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
|
||||
"requires": {
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
|
||||
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
|
||||
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1",
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
@@ -16229,6 +16120,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
@@ -16620,7 +16512,8 @@
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
||||
"dev": true
|
||||
},
|
||||
"os-locale": {
|
||||
"version": "1.4.0",
|
||||
@@ -16639,20 +16532,6 @@
|
||||
"windows-release": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||
},
|
||||
"osenv": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
|
||||
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
||||
"requires": {
|
||||
"os-homedir": "^1.0.0",
|
||||
"os-tmpdir": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-defer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
|
||||
@@ -17705,6 +17584,7 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
@@ -17904,6 +17784,15 @@
|
||||
"warning": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"react-query": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.5.5.tgz",
|
||||
"integrity": "sha512-WYZcHcAs5K5lPGT6CI8fz3lU62S8IfZhvB1K4aZH27wg9T6CWei+y7IRyZwti9X18LX134O4olgEuNth9LEX+w==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"match-sorter": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"react-redux": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz",
|
||||
@@ -18296,6 +18185,11 @@
|
||||
"superagent-proxy": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"remove-accents": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
|
||||
"integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U="
|
||||
},
|
||||
"remove-trailing-separator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
||||
@@ -19131,12 +19025,14 @@
|
||||
"simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"optional": true
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
|
||||
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"decompress-response": "^4.2.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -20128,30 +20024,6 @@
|
||||
"integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
|
||||
"dev": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
|
||||
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.8.6",
|
||||
"minizlib": "^1.2.1",
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
@@ -22207,6 +22079,7 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"string-width": "^1.0.2 || 2"
|
||||
},
|
||||
@@ -22214,12 +22087,14 @@
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"optional": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
@@ -22229,6 +22104,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
@@ -22398,7 +22274,8 @@
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "13.3.2",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"applicationinsights": "1.8.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "2.6.1",
|
||||
"canvas": "file:./canvas",
|
||||
"clean-webpack-plugin": "0.1.19",
|
||||
"copy-webpack-plugin": "6.0.2",
|
||||
"crossroads": "0.12.2",
|
||||
@@ -71,6 +71,7 @@
|
||||
"knockout": "3.5.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.18.1",
|
||||
"msal": "1.4.4",
|
||||
"object.entries": "1.1.0",
|
||||
"office-ui-fabric-react": "7.134.1",
|
||||
"p-retry": "4.2.0",
|
||||
@@ -85,6 +86,7 @@
|
||||
"react-dom": "16.9.0",
|
||||
"react-hotkeys": "2.0.0",
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-query": "3.5.5",
|
||||
"react-redux": "7.1.3",
|
||||
"redux": "4.0.4",
|
||||
"rx-jupyter": "5.5.12",
|
||||
|
||||
@@ -2,5 +2,6 @@ export enum AuthType {
|
||||
AAD = "aad",
|
||||
EncryptedToken = "encryptedtoken",
|
||||
MasterKey = "masterkey",
|
||||
ResourceToken = "resourcetoken"
|
||||
ResourceToken = "resourcetoken",
|
||||
ConnectionString = "connectionstring"
|
||||
}
|
||||
|
||||
@@ -587,11 +587,3 @@ export interface MemoryUsageInfo {
|
||||
freeKB: number;
|
||||
totalKB: number;
|
||||
}
|
||||
|
||||
export interface resourceTokenConnectionStringProperties {
|
||||
accountEndpoint: string;
|
||||
collectionId: string;
|
||||
databaseId: string;
|
||||
partitionKey?: string;
|
||||
resourceToken: string;
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels";
|
||||
import { AccountKind } from "../../../Common/Constants";
|
||||
|
||||
const createBlankProps = (): AccountSwitchComponentProps => {
|
||||
return {
|
||||
authType: null,
|
||||
displayText: "",
|
||||
accounts: [],
|
||||
selectedAccountName: null,
|
||||
isLoadingAccounts: false,
|
||||
onAccountChange: jest.fn(),
|
||||
subscriptions: [],
|
||||
selectedSubscriptionId: null,
|
||||
isLoadingSubscriptions: false,
|
||||
onSubscriptionChange: jest.fn()
|
||||
};
|
||||
};
|
||||
|
||||
const createBlankAccount = (): DatabaseAccount => {
|
||||
return {
|
||||
id: "",
|
||||
kind: AccountKind.Default,
|
||||
name: "",
|
||||
properties: null,
|
||||
location: "",
|
||||
tags: null,
|
||||
type: ""
|
||||
};
|
||||
};
|
||||
|
||||
const createBlankSubscription = (): Subscription => {
|
||||
return {
|
||||
subscriptionId: "",
|
||||
displayName: "",
|
||||
authorizationSource: "",
|
||||
state: "",
|
||||
subscriptionPolicies: null,
|
||||
tenantId: "",
|
||||
uniqueDisplayName: ""
|
||||
};
|
||||
};
|
||||
|
||||
const createFullProps = (): AccountSwitchComponentProps => {
|
||||
const props = createBlankProps();
|
||||
props.authType = AuthType.AAD;
|
||||
const account1 = createBlankAccount();
|
||||
account1.name = "account1";
|
||||
const account2 = createBlankAccount();
|
||||
account2.name = "account2";
|
||||
const account3 = createBlankAccount();
|
||||
account3.name = "superlongaccountnamestringtest";
|
||||
props.accounts = [account1, account2, account3];
|
||||
props.selectedAccountName = "account2";
|
||||
|
||||
const sub1 = createBlankSubscription();
|
||||
sub1.displayName = "sub1";
|
||||
sub1.subscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297";
|
||||
const sub2 = createBlankSubscription();
|
||||
sub2.displayName = "subsubsubsubsubsubsub2";
|
||||
sub2.subscriptionId = "b20b3e93-0185-4326-8a9c-d44bac276b6b";
|
||||
props.subscriptions = [sub1, sub2];
|
||||
props.selectedSubscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297";
|
||||
|
||||
return props;
|
||||
};
|
||||
|
||||
describe("test render", () => {
|
||||
it("renders no auth type -> handle error in code", () => {
|
||||
const props = createBlankProps();
|
||||
|
||||
const wrapper = shallow(<AccountSwitchComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// Encrypted Token
|
||||
it("renders auth security token, with selected account name", () => {
|
||||
const props = createBlankProps();
|
||||
props.authType = AuthType.EncryptedToken;
|
||||
props.selectedAccountName = "testaccount";
|
||||
|
||||
const wrapper = shallow(<AccountSwitchComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// AAD
|
||||
it("renders auth aad, with all information", () => {
|
||||
const props = createFullProps();
|
||||
const wrapper = shallow(<AccountSwitchComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders auth aad all dropdown menus", () => {
|
||||
const props = createFullProps();
|
||||
const wrapper = mount(<AccountSwitchComponent {...props} />);
|
||||
|
||||
expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false);
|
||||
wrapper.find("button.accountSwitchButton").simulate("click");
|
||||
expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(true);
|
||||
|
||||
expect(wrapper.exists("div.accountSwitchSubscriptionDropdown")).toBe(true);
|
||||
wrapper.find("DropdownBase.accountSwitchSubscriptionDropdown").simulate("click");
|
||||
// Click will dismiss the first contextual menu in enzyme. Need to dig deeper to achieve below test
|
||||
|
||||
// expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(true);
|
||||
// expect(wrapper.find("button.ms-Dropdown-item").length).toBe(2);
|
||||
// wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click");
|
||||
// expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(false);
|
||||
|
||||
// expect(wrapper.exists("div.accountSwitchAccountDropdown")).toBe(true);
|
||||
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
|
||||
// expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(true);
|
||||
// expect(wrapper.find("button.ms-Dropdown-item").length).toBe(3);
|
||||
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
|
||||
// expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(false);
|
||||
|
||||
// wrapper.find("button.accountSwitchButton").simulate("click");
|
||||
// expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
// describe("test function", () => {
|
||||
// it("switch subscription function", () => {
|
||||
// const props = createFullProps();
|
||||
// const wrapper = mount(<AccountSwitchComponent {...props} />);
|
||||
|
||||
// wrapper.find("button.accountSwitchButton").simulate("click");
|
||||
// wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click");
|
||||
// wrapper
|
||||
// .find("button.ms-Dropdown-item")
|
||||
// .at(1)
|
||||
// .simulate("click");
|
||||
// expect(props.onSubscriptionChange).toBeCalled();
|
||||
// expect(props.onSubscriptionChange).toHaveBeenCalled();
|
||||
|
||||
// wrapper.unmount();
|
||||
// });
|
||||
|
||||
// it("switch account", () => {
|
||||
// const props = createFullProps();
|
||||
// const wrapper = mount(<AccountSwitchComponent {...props} />);
|
||||
|
||||
// wrapper.find("button.accountSwitchButton").simulate("click");
|
||||
// wrapper.find("div.accountSwitchAccountDropdown").simulate("click");
|
||||
// wrapper
|
||||
// .find("button.ms-Dropdown-item")
|
||||
// .at(0)
|
||||
// .simulate("click");
|
||||
// expect(props.onAccountChange).toBeCalled();
|
||||
// expect(props.onAccountChange).toHaveBeenCalled();
|
||||
|
||||
// wrapper.unmount();
|
||||
// });
|
||||
// });
|
||||
@@ -1,177 +0,0 @@
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels";
|
||||
|
||||
import * as React from "react";
|
||||
import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
||||
|
||||
export interface AccountSwitchComponentProps {
|
||||
authType: AuthType;
|
||||
selectedAccountName: string;
|
||||
accounts: DatabaseAccount[];
|
||||
isLoadingAccounts: boolean;
|
||||
onAccountChange: (newAccount: DatabaseAccount) => void;
|
||||
selectedSubscriptionId: string;
|
||||
subscriptions: Subscription[];
|
||||
isLoadingSubscriptions: boolean;
|
||||
onSubscriptionChange: (newSubscription: Subscription) => void;
|
||||
displayText?: string;
|
||||
}
|
||||
|
||||
export class AccountSwitchComponent extends React.Component<AccountSwitchComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return this.props.authType === AuthType.AAD ? this._renderSwitchDropDown() : this._renderAccountName();
|
||||
}
|
||||
|
||||
private _renderSwitchDropDown(): JSX.Element {
|
||||
const { displayText, selectedAccountName } = this.props;
|
||||
|
||||
const menuProps: IContextualMenuProps = {
|
||||
directionalHintFixed: true,
|
||||
className: "accountSwitchContextualMenu",
|
||||
items: [
|
||||
{
|
||||
key: "switchSubscription",
|
||||
onRender: this._renderSubscriptionDropdown.bind(this)
|
||||
},
|
||||
{
|
||||
key: "switchAccount",
|
||||
onRender: this._renderAccountDropDown.bind(this)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const buttonStyles: IButtonStyles = {
|
||||
root: {
|
||||
fontSize: StyleConstants.DefaultFontSize,
|
||||
height: 40,
|
||||
padding: 0,
|
||||
paddingLeft: 10,
|
||||
marginRight: 5,
|
||||
backgroundColor: StyleConstants.BaseDark,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootHovered: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootFocused: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootPressed: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootExpanded: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
textContainer: {
|
||||
flexGrow: "initial"
|
||||
}
|
||||
};
|
||||
|
||||
const buttonProps: IButtonProps = {
|
||||
text: displayText || selectedAccountName,
|
||||
menuProps: menuProps,
|
||||
styles: buttonStyles,
|
||||
className: "accountSwitchButton",
|
||||
id: "accountSwitchButton"
|
||||
};
|
||||
|
||||
return <DefaultButton {...buttonProps} />;
|
||||
}
|
||||
|
||||
private _renderSubscriptionDropdown(): JSX.Element {
|
||||
const { subscriptions, selectedSubscriptionId, isLoadingSubscriptions } = this.props;
|
||||
const options: IDropdownOption[] = subscriptions.map(sub => {
|
||||
return {
|
||||
key: sub.subscriptionId,
|
||||
text: sub.displayName,
|
||||
data: sub
|
||||
};
|
||||
});
|
||||
|
||||
const placeHolderText = isLoadingSubscriptions
|
||||
? "Loading subscriptions"
|
||||
: !options || !options.length
|
||||
? "No subscriptions found in current directory"
|
||||
: "Select subscription from list";
|
||||
|
||||
const dropdownProps: IDropdownProps = {
|
||||
label: "Subscription",
|
||||
className: "accountSwitchSubscriptionDropdown",
|
||||
options: options,
|
||||
onChange: this._onSubscriptionDropdownChange,
|
||||
defaultSelectedKey: selectedSubscriptionId,
|
||||
placeholder: placeHolderText,
|
||||
styles: {
|
||||
callout: "accountSwitchSubscriptionDropdownMenu"
|
||||
}
|
||||
};
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
}
|
||||
|
||||
private _onSubscriptionDropdownChange = (e: React.FormEvent<HTMLDivElement>, option: IDropdownOption): void => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubscriptionChange(option.data);
|
||||
};
|
||||
|
||||
private _renderAccountDropDown(): JSX.Element {
|
||||
const { accounts, selectedAccountName, isLoadingAccounts } = this.props;
|
||||
const options: IDropdownOption[] = accounts.map(account => {
|
||||
return {
|
||||
key: account.name,
|
||||
text: account.name,
|
||||
data: account
|
||||
};
|
||||
});
|
||||
// Fabric UI will also try to select the first non-disabled option from dropdown.
|
||||
// Add a option to prevent pop the message when user click on dropdown on first time.
|
||||
options.unshift({
|
||||
key: "select from list",
|
||||
text: "Select Cosmos DB account from list",
|
||||
data: undefined
|
||||
});
|
||||
|
||||
const placeHolderText = isLoadingAccounts
|
||||
? "Loading Cosmos DB accounts"
|
||||
: !options || !options.length
|
||||
? "No Cosmos DB accounts found"
|
||||
: "Select Cosmos DB account from list";
|
||||
|
||||
const dropdownProps: IDropdownProps = {
|
||||
label: "Cosmos DB Account Name",
|
||||
className: "accountSwitchAccountDropdown",
|
||||
options: options,
|
||||
onChange: this._onAccountDropdownChange,
|
||||
defaultSelectedKey: selectedAccountName,
|
||||
placeholder: placeHolderText,
|
||||
styles: {
|
||||
callout: "accountSwitchAccountDropdownMenu"
|
||||
}
|
||||
};
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
}
|
||||
|
||||
private _onAccountDropdownChange = (e: React.FormEvent<HTMLDivElement>, option: IDropdownOption): void => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onAccountChange(option.data);
|
||||
};
|
||||
|
||||
private _renderAccountName(): JSX.Element {
|
||||
const { displayText, selectedAccountName } = this.props;
|
||||
return <span className="accountNameHeader">{displayText || selectedAccountName}</span>;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent";
|
||||
|
||||
export class AccountSwitchComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<AccountSwitchComponentProps>;
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <AccountSwitchComponent {...this.parameters()} />;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`test render renders auth aad, with all information 1`] = `
|
||||
<CustomizedDefaultButton
|
||||
className="accountSwitchButton"
|
||||
id="accountSwitchButton"
|
||||
menuProps={
|
||||
Object {
|
||||
"className": "accountSwitchContextualMenu",
|
||||
"directionalHintFixed": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"key": "switchSubscription",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"key": "switchAccount",
|
||||
"onRender": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"backgroundColor": undefined,
|
||||
"color": undefined,
|
||||
"fontSize": undefined,
|
||||
"height": 40,
|
||||
"marginRight": 5,
|
||||
"padding": 0,
|
||||
"paddingLeft": 10,
|
||||
},
|
||||
"rootExpanded": Object {
|
||||
"backgroundColor": undefined,
|
||||
"color": undefined,
|
||||
},
|
||||
"rootFocused": Object {
|
||||
"backgroundColor": undefined,
|
||||
"color": undefined,
|
||||
},
|
||||
"rootHovered": Object {
|
||||
"backgroundColor": undefined,
|
||||
"color": undefined,
|
||||
},
|
||||
"rootPressed": Object {
|
||||
"backgroundColor": undefined,
|
||||
"color": undefined,
|
||||
},
|
||||
"textContainer": Object {
|
||||
"flexGrow": "initial",
|
||||
},
|
||||
}
|
||||
}
|
||||
text="account2"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`test render renders auth security token, with selected account name 1`] = `
|
||||
<span
|
||||
className="accountNameHeader"
|
||||
>
|
||||
testaccount
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`test render renders no auth type -> handle error in code 1`] = `
|
||||
<span
|
||||
className="accountNameHeader"
|
||||
/>
|
||||
`;
|
||||
@@ -1260,16 +1260,6 @@ export default class Explorer {
|
||||
$("#contextSwitchPrompt").dialog("open");
|
||||
}
|
||||
|
||||
public displayConnectExplorerForm(): void {
|
||||
$("#divExplorer").hide();
|
||||
$("#connectExplorer").css("display", "flex");
|
||||
}
|
||||
|
||||
public hideConnectExplorerForm(): void {
|
||||
$("#connectExplorer").hide();
|
||||
$("#divExplorer").show();
|
||||
}
|
||||
|
||||
public isReadWriteToggled: () => boolean = (): boolean => {
|
||||
return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite;
|
||||
};
|
||||
@@ -1843,7 +1833,7 @@ export default class Explorer {
|
||||
if (inputs != null) {
|
||||
// In development mode, save the iframe message from the portal in session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (process.env.NODE_ENV === "development" && configContext.platform === Platform.Portal) {
|
||||
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
|
||||
}
|
||||
|
||||
@@ -1878,16 +1868,6 @@ export default class Explorer {
|
||||
subscriptionType: inputs.subscriptionType,
|
||||
quotaId: inputs.quotaId
|
||||
});
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabaseAccount,
|
||||
{
|
||||
resourceId: this.databaseAccount && this.databaseAccount().id,
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
databaseAccount: this.databaseAccount && this.databaseAccount()
|
||||
},
|
||||
inputs.loadDatabaseAccountTimestamp
|
||||
);
|
||||
|
||||
this.isAccountReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ConnectionStringParser } from "../../Platform/Hosted/Helpers/ConnectionStringParser";
|
||||
import { parseConnectionString } from "../../Platform/Hosted/Helpers/ConnectionStringParser";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
@@ -48,9 +48,7 @@ export class RenewAdHocAccessPane extends ContextualPaneBase {
|
||||
};
|
||||
|
||||
private _shouldShowContextSwitchPrompt(): boolean {
|
||||
const inputMetadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
this.accessKey()
|
||||
);
|
||||
const inputMetadata: DataModels.AccessInputMetadata = parseConnectionString(this.accessKey());
|
||||
const apiKind: DataModels.ApiKind =
|
||||
this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience());
|
||||
const hasOpenedTabs: boolean =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,242 +1,140 @@
|
||||
import { Configuration, PublicClientApplication } from "@azure/msal-browser";
|
||||
import { AuthenticatedTemplate, MsalProvider, UnauthenticatedTemplate } from "@azure/msal-react";
|
||||
import { useBoolean } from "@uifabric/react-hooks";
|
||||
import {
|
||||
DefaultButton,
|
||||
DirectionalHint,
|
||||
FocusZone,
|
||||
initializeIcons,
|
||||
Panel,
|
||||
Persona,
|
||||
PersonaInitialsColor,
|
||||
PersonaSize
|
||||
} from "office-ui-fabric-react";
|
||||
import { initializeIcons } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
import FeedbackIcon from "../images/Feedback.svg";
|
||||
import ConnectIcon from "../images/HostedConnectwhite.svg";
|
||||
import ChevronRight from "../images/chevron-right.svg";
|
||||
import "../less/hostedexplorer.less";
|
||||
import { CommandButtonComponent } from "./Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { DefaultDirectoryDropdownComponent } from "./Explorer/Controls/Directory/DefaultDirectoryDropdownComponent";
|
||||
import { DirectoryListComponent } from "./Explorer/Controls/Directory/DirectoryListComponent";
|
||||
import { AuthType } from "./AuthType";
|
||||
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
|
||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
|
||||
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
|
||||
import "./Explorer/Menus/NavBar/MeControlComponent.less";
|
||||
import { useGraphProfile } from "./hooks/useGraphProfile";
|
||||
import { usePortalAccessToken } from "./hooks/usePortalAccessToken";
|
||||
import { MeControl } from "./Platform/Hosted/Components/MeControl";
|
||||
import "./Platform/Hosted/ConnectScreen.less";
|
||||
import "./Shared/appInsights";
|
||||
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
||||
import { useAADAuth } from "./hooks/useAADAuth";
|
||||
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton";
|
||||
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
||||
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
// MSAL configuration
|
||||
const configuration: Configuration = {
|
||||
auth: {
|
||||
clientId: "e8ae3d28-de2a-4dc8-8fa3-2d2998b1c38f",
|
||||
redirectUri: "https://localhost:1234/hostedExplorer.html",
|
||||
authority: "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47"
|
||||
}
|
||||
};
|
||||
|
||||
const application = new PublicClientApplication(configuration);
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
// For handling encrypted portal tokens sent via query paramter
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const [encryptedToken, setEncryptedToken] = React.useState<string>(params && params.get("key"));
|
||||
const encryptedTokenMetadata = usePortalAccessToken(encryptedToken);
|
||||
|
||||
// For showing/hiding panel
|
||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||
const { graphData, photo } = useGraphProfile();
|
||||
|
||||
const menuProps = {
|
||||
className: "mecontrolContextualMenu",
|
||||
isBeakVisible: false,
|
||||
directionalHintFixed: true,
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
calloutProps: {
|
||||
minPagePadding: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
key: "Persona",
|
||||
onRender: () => <Persona />
|
||||
},
|
||||
{
|
||||
key: "SwitchDirectory",
|
||||
onRender: () => (
|
||||
<div className="switchDirectoryLink" onClick={() => openPanel}>
|
||||
Switch Directory
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "SignOut",
|
||||
onRender: () => (
|
||||
<div
|
||||
className="signOutLink"
|
||||
onClick={() => {
|
||||
instance.logout();
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</div>
|
||||
)
|
||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth();
|
||||
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||
const [connectionString, setConnectionString] = React.useState<string>();
|
||||
|
||||
const ref = React.useRef<HTMLIFrameElement>();
|
||||
|
||||
React.useEffect(() => {
|
||||
// If ref.current is undefined no iframe has been rendered
|
||||
if (ref.current) {
|
||||
// In hosted mode, we can set global properties directly on the child iframe.
|
||||
// This is not possible in the portal where the iframes have different origins
|
||||
const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame;
|
||||
// AAD authenticated uses ALWAYS using AAD authType
|
||||
if (isLoggedIn) {
|
||||
frameWindow.hostedConfig = {
|
||||
authType: AuthType.AAD,
|
||||
databaseAccount,
|
||||
authorizationToken: armToken
|
||||
};
|
||||
} else if (authType === AuthType.EncryptedToken) {
|
||||
frameWindow.hostedConfig = {
|
||||
authType: AuthType.EncryptedToken,
|
||||
encryptedToken,
|
||||
encryptedTokenMetadata
|
||||
};
|
||||
} else if (authType === AuthType.ConnectionString) {
|
||||
frameWindow.hostedConfig = {
|
||||
authType: AuthType.ConnectionString,
|
||||
encryptedToken,
|
||||
encryptedTokenMetadata,
|
||||
masterKey: extractMasterKeyfromConnectionString(connectionString)
|
||||
};
|
||||
} else if (authType === AuthType.ResourceToken) {
|
||||
frameWindow.hostedConfig = {
|
||||
authType: AuthType.ResourceToken,
|
||||
resourceToken: connectionString
|
||||
};
|
||||
}
|
||||
]
|
||||
};
|
||||
const personaProps = {};
|
||||
|
||||
// {
|
||||
// id: "commandbutton-settings",
|
||||
// iconSrc: SettingsIcon,
|
||||
// iconAlt: "setting button",
|
||||
// onCommandClick: () => {},
|
||||
// commandButtonLabel: undefined,
|
||||
// ariaLabel: "setting button",
|
||||
// tooltipText: "Global settings",
|
||||
// hasPopup: true,
|
||||
// disabled: false
|
||||
// },
|
||||
// {
|
||||
// id: "commandbutton-feedback",
|
||||
// iconSrc: FeedbackIcon,
|
||||
// iconAlt: "feeback button",
|
||||
// onCommandClick: () =>
|
||||
// window.open(
|
||||
// "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"
|
||||
// ),
|
||||
// commandButtonLabel: undefined,
|
||||
// ariaLabel: "feeback button",
|
||||
// tooltipText: "Send feedback",
|
||||
// hasPopup: true,
|
||||
// disabled: false
|
||||
// }
|
||||
|
||||
const buttonProps = {
|
||||
id: "mecontrolHeader",
|
||||
className: "mecontrolHeaderButton",
|
||||
menuProps: menuProps,
|
||||
onRenderMenuIcon: () => <span />,
|
||||
styles: {
|
||||
rootHovered: { backgroundColor: "#393939" },
|
||||
rootFocused: { backgroundColor: "#393939" },
|
||||
rootPressed: { backgroundColor: "#393939" },
|
||||
rootExpanded: { backgroundColor: "#393939" }
|
||||
}
|
||||
};
|
||||
}, [ref, encryptedToken, encryptedTokenMetadata, isLoggedIn, databaseAccount]);
|
||||
|
||||
const showAccount = (isLoggedIn && databaseAccount) || (encryptedTokenMetadata && encryptedTokenMetadata);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<header>
|
||||
<div className="items" role="menubar">
|
||||
<div className="cosmosDBTitle">
|
||||
<span
|
||||
className="title"
|
||||
data-bind="click: openAzurePortal, event: { keypress: onOpenAzurePortalKeyPress }"
|
||||
onClick={() => window.open("https://portal.azure.com", "_blank")}
|
||||
tabIndex={0}
|
||||
title="Go to Azure Portal"
|
||||
>
|
||||
Microsoft Azure
|
||||
</span>
|
||||
<span className="accontSplitter" /> <span className="serviceTitle">Cosmos DB</span>
|
||||
<img
|
||||
className="chevronRight"
|
||||
src="/chevron-right.svg"
|
||||
alt="account separator"
|
||||
data-bind="visible: isAccountActive"
|
||||
/>
|
||||
<span
|
||||
className="accountSwitchComponentContainer"
|
||||
data-bind="react: accountSwitchComponentAdapter, visible: isAccountActive"
|
||||
/>
|
||||
</div>
|
||||
<div className="feedbackConnectSettingIcons">
|
||||
<AuthenticatedTemplate>
|
||||
<CommandButtonComponent
|
||||
id="commandbutton-connect"
|
||||
iconSrc={ConnectIcon}
|
||||
iconAlt="connect button"
|
||||
onCommandClick={() => {}}
|
||||
ariaLabel="connect button"
|
||||
tooltipText="Connect to a Cosmos DB account"
|
||||
hasPopup={true}
|
||||
disabled={false}
|
||||
/>
|
||||
</AuthenticatedTemplate>
|
||||
<UnauthenticatedTemplate>
|
||||
<CommandButtonComponent
|
||||
id="commandbutton-feedback"
|
||||
iconSrc={FeedbackIcon}
|
||||
iconAlt="feeback button"
|
||||
onCommandClick={() =>
|
||||
window.open(
|
||||
"https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"
|
||||
)
|
||||
}
|
||||
ariaLabel="feeback button"
|
||||
tooltipText="Send feedback"
|
||||
hasPopup={true}
|
||||
disabled={false}
|
||||
/>
|
||||
</UnauthenticatedTemplate>
|
||||
{(isLoggedIn || encryptedTokenMetadata?.accountName) && (
|
||||
<img className="chevronRight" src={ChevronRight} alt="account separator" />
|
||||
)}
|
||||
{isLoggedIn && (
|
||||
<span className="accountSwitchComponentContainer">
|
||||
<AccountSwitcher armToken={armToken} setDatabaseAccount={setDatabaseAccount} />
|
||||
</span>
|
||||
)}
|
||||
{!isLoggedIn && encryptedTokenMetadata?.accountName && (
|
||||
<span className="accountSwitchComponentContainer">
|
||||
<span className="accountNameHeader">{encryptedTokenMetadata?.accountName}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<FeedbackCommandButton />
|
||||
<div className="meControl">
|
||||
<AuthenticatedTemplate>
|
||||
<FocusZone>
|
||||
<DefaultButton {...buttonProps}>
|
||||
<Persona
|
||||
imageUrl={photo}
|
||||
text={graphData?.displayName}
|
||||
secondaryText={graphData?.displayName}
|
||||
showSecondaryText={true}
|
||||
showInitialsUntilImageLoads={true}
|
||||
initialsColor={PersonaInitialsColor.teal}
|
||||
size={PersonaSize.size28}
|
||||
className="mecontrolHeaderPersona"
|
||||
/>
|
||||
</DefaultButton>
|
||||
</FocusZone>
|
||||
</AuthenticatedTemplate>
|
||||
<UnauthenticatedTemplate>
|
||||
<DefaultButton
|
||||
className="mecontrolSigninButton"
|
||||
text="Sign In"
|
||||
onClick={() => {
|
||||
instance.loginPopup();
|
||||
}}
|
||||
styles={{
|
||||
rootHovered: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootFocused: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootPressed: { backgroundColor: "#393939", color: "#fff" }
|
||||
}}
|
||||
/>
|
||||
</UnauthenticatedTemplate>
|
||||
{isLoggedIn ? (
|
||||
<MeControl {...{ graphToken, openPanel, logout, account }} />
|
||||
) : (
|
||||
<SignInButton {...{ login }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{/* <iframe
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
className="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||
data-bind="visible: navigationSelection() === 'explorer'"
|
||||
></iframe> */}
|
||||
<div data-bind="react: firewallWarningComponentAdapter" />
|
||||
<div data-bind="react: dialogComponentAdapter" />
|
||||
<Panel
|
||||
headerText="Select Directory"
|
||||
isOpen={!isOpen}
|
||||
onDismiss={dismissPanel}
|
||||
// You MUST provide this prop! Otherwise screen readers will just say "button" with no label.
|
||||
closeButtonAriaLabel="Close"
|
||||
>
|
||||
<div className="directoryDropdownContainer">
|
||||
<DefaultDirectoryDropdownComponent />
|
||||
</div>
|
||||
<div className="directoryDivider" />
|
||||
<div className="directoryListContainer">
|
||||
<DirectoryListComponent />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
{showAccount && (
|
||||
// Ideally we would import and render data explorer like any other React component, however
|
||||
// because it still has a significant amount of Knockout code, this would lead to memory leaks.
|
||||
// Knockout does not have a way to tear down all of its binding and listeners with a single method.
|
||||
// It's possible this can be changed once all knockout code has been removed.
|
||||
<iframe
|
||||
// Setting key is needed so React will re-render this element on any account change
|
||||
key={databaseAccount?.id || encryptedTokenMetadata?.accountName}
|
||||
ref={ref}
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
className="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||
></iframe>
|
||||
)}
|
||||
{!isLoggedIn && !encryptedTokenMetadata && (
|
||||
<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />
|
||||
)}
|
||||
{isLoggedIn && <DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<MsalProvider instance={application}>
|
||||
<App />
|
||||
</MsalProvider>,
|
||||
document.body
|
||||
);
|
||||
render(<App />, document.getElementById("App"));
|
||||
|
||||
32
src/HostedExplorerChildFrame.ts
Normal file
32
src/HostedExplorerChildFrame.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { AuthType } from "./AuthType";
|
||||
import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels";
|
||||
|
||||
export interface HostedExplorerChildFrame extends Window {
|
||||
hostedConfig: AAD | ConnectionString | EncryptedToken | ResourceToken;
|
||||
}
|
||||
|
||||
interface AAD {
|
||||
authType: AuthType.AAD;
|
||||
databaseAccount: DatabaseAccount;
|
||||
authorizationToken: string;
|
||||
}
|
||||
|
||||
interface ConnectionString {
|
||||
authType: AuthType.ConnectionString;
|
||||
// Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy
|
||||
encryptedToken: string;
|
||||
encryptedTokenMetadata: AccessInputMetadata;
|
||||
// Master key is currently only used by Graph API. All other APIs use encrypted tokens and proxy with connection string
|
||||
masterKey?: string;
|
||||
}
|
||||
|
||||
interface EncryptedToken {
|
||||
authType: AuthType.EncryptedToken;
|
||||
encryptedToken: string;
|
||||
encryptedTokenMetadata: AccessInputMetadata;
|
||||
}
|
||||
|
||||
interface ResourceToken {
|
||||
authType: AuthType.ResourceToken;
|
||||
resourceToken: string;
|
||||
}
|
||||
220
src/Main.tsx
220
src/Main.tsx
@@ -55,19 +55,11 @@ import "url-polyfill/url-polyfill.min";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
import * as ko from "knockout";
|
||||
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
|
||||
import * as Emulator from "./Platform/Emulator/Main";
|
||||
import Hosted from "./Platform/Hosted/Main";
|
||||
import * as Portal from "./Platform/Portal/Main";
|
||||
import { AuthType } from "./AuthType";
|
||||
|
||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||
import { applyExplorerBindings } from "./applyExplorerBindings";
|
||||
import { initializeConfiguration, Platform } from "./ConfigContext";
|
||||
import { configContext, initializeConfiguration, Platform } from "./ConfigContext";
|
||||
import Explorer from "./Explorer/Explorer";
|
||||
import React, { useEffect } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
@@ -77,49 +69,195 @@ import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import refreshImg from "../images/refresh-cosmos.svg";
|
||||
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||
import { updateUserContext } from "./UserContext";
|
||||
import AuthHeadersUtil from "./Platform/Hosted/Authorization";
|
||||
import { CollectionCreation } from "./Shared/Constants";
|
||||
import { extractFeatures } from "./Platform/Hosted/extractFeatures";
|
||||
import { emulatorAccount } from "./Platform/Emulator/emulatorAccount";
|
||||
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
||||
import {
|
||||
getDatabaseAccountKindFromExperience,
|
||||
getDatabaseAccountPropertiesFromMetadata
|
||||
} from "./Platform/Hosted/HostedUtils";
|
||||
import { DefaultExperienceUtility } from "./Shared/DefaultExperienceUtility";
|
||||
import { parseResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||
import { AccountKind, DefaultAccountExperience } from "./Common/Constants";
|
||||
|
||||
// TODO: Encapsulate and reuse all global variables as environment variables
|
||||
window.authType = AuthType.AAD;
|
||||
// const accountResourceId =
|
||||
// authType === AuthType.EncryptedToken
|
||||
// ? Main._databaseAccountId
|
||||
// : authType === AuthType.AAD && account
|
||||
// ? account.id
|
||||
// : "";
|
||||
// const subscriptionId: string =
|
||||
// accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
||||
// const resourceGroup: string =
|
||||
// accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
useEffect(() => {
|
||||
initializeConfiguration().then(config => {
|
||||
let explorer: Explorer;
|
||||
if (config.platform === Platform.Hosted) {
|
||||
try {
|
||||
Hosted.initializeExplorer().then(
|
||||
(explorer: Explorer) => {
|
||||
applyExplorerBindings(explorer);
|
||||
Hosted.configureTokenValidationDisplayPrompt(explorer);
|
||||
},
|
||||
(error: unknown) => {
|
||||
try {
|
||||
const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
|
||||
window.dataExplorer = uninitializedExplorer;
|
||||
ko.applyBindings(uninitializedExplorer);
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
if (window.authType !== AuthType.AAD) {
|
||||
uninitializedExplorer.isRefreshingExplorer(false);
|
||||
uninitializedExplorer.displayConnectExplorerForm();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
console.error(error);
|
||||
}
|
||||
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||
explorer = new Explorer();
|
||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
||||
// TODO: Remove window.authType
|
||||
window.authType = AuthType.EncryptedToken;
|
||||
// Impossible to tell if this is a try cosmos sub using an encrypted token
|
||||
explorer.isTryCosmosDBSubscription(false);
|
||||
updateUserContext({
|
||||
accessToken: encodeURIComponent(win.hostedConfig.encryptedToken)
|
||||
});
|
||||
|
||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||
win.hostedConfig.encryptedTokenMetadata.apiKind
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
explorer.initDataExplorerWithFrameInputs({
|
||||
databaseAccount: {
|
||||
id: "",
|
||||
// id: Main._databaseAccountId,
|
||||
name: win.hostedConfig.encryptedTokenMetadata.accountName,
|
||||
kind: getDatabaseAccountKindFromExperience(apiExperience),
|
||||
properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata),
|
||||
tags: []
|
||||
},
|
||||
subscriptionId: undefined,
|
||||
resourceGroup: undefined,
|
||||
masterKey: undefined,
|
||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||
authorizationToken: undefined,
|
||||
features: extractFeatures(),
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: undefined,
|
||||
serverId: AuthHeadersUtil.serverId,
|
||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||
quotaId: undefined,
|
||||
addCollectionDefaultFlight: explorer.flight(),
|
||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
|
||||
});
|
||||
explorer.isAccountReady(true);
|
||||
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
||||
window.authType = AuthType.ResourceToken;
|
||||
// Resource tokens can only be used with SQL API
|
||||
const apiExperience: string = DefaultAccountExperience.DocumentDB;
|
||||
const parsedResourceToken = parseResourceTokenConnectionString(win.hostedConfig.resourceToken);
|
||||
updateUserContext({
|
||||
resourceToken: parsedResourceToken.resourceToken
|
||||
});
|
||||
return explorer.initDataExplorerWithFrameInputs({
|
||||
databaseAccount: {
|
||||
id: "",
|
||||
name: parsedResourceToken.accountEndpoint,
|
||||
kind: AccountKind.GlobalDocumentDB,
|
||||
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
|
||||
tags: { defaultExperience: apiExperience }
|
||||
},
|
||||
subscriptionId: undefined,
|
||||
resourceGroup: undefined,
|
||||
masterKey: undefined,
|
||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||
authorizationToken: undefined,
|
||||
features: extractFeatures(),
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: undefined,
|
||||
serverId: AuthHeadersUtil.serverId,
|
||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||
quotaId: undefined,
|
||||
addCollectionDefaultFlight: explorer.flight(),
|
||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
||||
isAuthWithresourceToken: true
|
||||
});
|
||||
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
||||
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
|
||||
window.authType = AuthType.EncryptedToken;
|
||||
// Impossible to tell if this is a try cosmos sub using an encrypted token
|
||||
explorer.isTryCosmosDBSubscription(false);
|
||||
updateUserContext({
|
||||
accessToken: encodeURIComponent(win.hostedConfig.encryptedToken)
|
||||
});
|
||||
|
||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||
win.hostedConfig.encryptedTokenMetadata.apiKind
|
||||
);
|
||||
explorer.initDataExplorerWithFrameInputs({
|
||||
databaseAccount: {
|
||||
id: "",
|
||||
// id: Main._databaseAccountId,
|
||||
name: win.hostedConfig.encryptedTokenMetadata.accountName,
|
||||
kind: getDatabaseAccountKindFromExperience(apiExperience),
|
||||
properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata),
|
||||
tags: []
|
||||
},
|
||||
subscriptionId: undefined,
|
||||
resourceGroup: undefined,
|
||||
masterKey: win.hostedConfig.masterKey,
|
||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||
authorizationToken: undefined,
|
||||
features: extractFeatures(),
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: undefined,
|
||||
serverId: AuthHeadersUtil.serverId,
|
||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||
quotaId: undefined,
|
||||
addCollectionDefaultFlight: explorer.flight(),
|
||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
|
||||
});
|
||||
explorer.isAccountReady(true);
|
||||
} else if (win.hostedConfig.authType === AuthType.AAD) {
|
||||
window.authType = AuthType.AAD;
|
||||
const account = win.hostedConfig.databaseAccount;
|
||||
const accountResourceId = account.id;
|
||||
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
||||
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
||||
updateUserContext({
|
||||
databaseAccount: win.hostedConfig.databaseAccount
|
||||
});
|
||||
explorer.initDataExplorerWithFrameInputs({
|
||||
databaseAccount: account,
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
masterKey: "",
|
||||
hasWriteAccess: true, //TODO: 425017 - support read access
|
||||
authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`,
|
||||
features: extractFeatures(),
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: undefined,
|
||||
serverId: AuthHeadersUtil.serverId,
|
||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||
quotaId: undefined,
|
||||
addCollectionDefaultFlight: explorer.flight(),
|
||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
|
||||
});
|
||||
explorer.isAccountReady(true);
|
||||
}
|
||||
} else if (config.platform === Platform.Emulator) {
|
||||
window.authType = AuthType.MasterKey;
|
||||
const explorer = Emulator.initializeExplorer();
|
||||
applyExplorerBindings(explorer);
|
||||
explorer = new Explorer();
|
||||
explorer.databaseAccount(emulatorAccount);
|
||||
explorer.isAccountReady(true);
|
||||
} else if (config.platform === Platform.Portal) {
|
||||
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
|
||||
const explorer = Portal.initializeExplorer();
|
||||
TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
|
||||
applyExplorerBindings(explorer);
|
||||
explorer = new Explorer();
|
||||
|
||||
// In development mode, try to load the iframe message from session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
|
||||
if (initMessage) {
|
||||
const message = JSON.parse(initMessage);
|
||||
console.warn("Loaded cached portal iframe message from session storage");
|
||||
console.dir(message);
|
||||
explorer.initDataExplorerWithFrameInputs(message);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||
}
|
||||
applyExplorerBindings(explorer);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -176,7 +314,7 @@ const App: React.FunctionComponent = () => {
|
||||
aria-label="Share url link"
|
||||
className="shareLink"
|
||||
type="text"
|
||||
read-only
|
||||
read-only={true}
|
||||
data-bind="value: shareAccessUrl"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import Explorer from "../../Explorer/Explorer";
|
||||
import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants";
|
||||
|
||||
export function initializeExplorer(): Explorer {
|
||||
const explorer = new Explorer();
|
||||
explorer.databaseAccount({
|
||||
name: "",
|
||||
id: "",
|
||||
location: "",
|
||||
type: "",
|
||||
kind: AccountKind.DocumentDB,
|
||||
tags: {
|
||||
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
|
||||
},
|
||||
properties: {
|
||||
documentEndpoint: "",
|
||||
tableEndpoint: "",
|
||||
gremlinEndpoint: "",
|
||||
cassandraEndpoint: ""
|
||||
}
|
||||
});
|
||||
|
||||
explorer.isAccountReady(true);
|
||||
return explorer;
|
||||
}
|
||||
18
src/Platform/Emulator/emulatorAccount.tsx
Normal file
18
src/Platform/Emulator/emulatorAccount.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants";
|
||||
|
||||
export const emulatorAccount = {
|
||||
name: "",
|
||||
id: "",
|
||||
location: "",
|
||||
type: "",
|
||||
kind: AccountKind.DocumentDB,
|
||||
tags: {
|
||||
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
|
||||
},
|
||||
properties: {
|
||||
documentEndpoint: "",
|
||||
tableEndpoint: "",
|
||||
gremlinEndpoint: "",
|
||||
cassandraEndpoint: ""
|
||||
}
|
||||
};
|
||||
@@ -1,180 +0,0 @@
|
||||
import AuthHeadersUtil from "./Authorization";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
// TODO: 421864 - add a fetch wrapper
|
||||
export abstract class ArmResourceUtils {
|
||||
private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT;
|
||||
private static readonly _armApiVersion: string = configContext.ARM_API_VERSION;
|
||||
private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA;
|
||||
|
||||
// TODO: 422867 - return continuation token instead of read through
|
||||
public static async listTenants(): Promise<Array<Tenant>> {
|
||||
let tenants: Array<Tenant> = [];
|
||||
|
||||
try {
|
||||
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea);
|
||||
let nextLink = `${ArmResourceUtils._armEndpoint}/tenants?api-version=2017-08-01`;
|
||||
|
||||
while (nextLink) {
|
||||
const response: Response = await fetch(nextLink, { headers: fetchHeaders });
|
||||
const result: TenantListResult =
|
||||
response.status === 204 || response.status === 304 ? null : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
tenants = [...tenants, ...result.value];
|
||||
}
|
||||
return tenants;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "ArmResourceUtils/listTenants");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 422867 - return continuation token instead of read through
|
||||
public static async listSubscriptions(tenantId?: string): Promise<Array<Subscription>> {
|
||||
let subscriptions: Array<Subscription> = [];
|
||||
|
||||
try {
|
||||
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
|
||||
let nextLink = `${ArmResourceUtils._armEndpoint}/subscriptions?api-version=${ArmResourceUtils._armApiVersion}`;
|
||||
|
||||
while (nextLink) {
|
||||
const response: Response = await fetch(nextLink, { headers: fetchHeaders });
|
||||
const result: SubscriptionListResult =
|
||||
response.status === 204 || response.status === 304 ? null : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
const validSubscriptions = result.value.filter(
|
||||
sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue"
|
||||
);
|
||||
subscriptions = [...subscriptions, ...validSubscriptions];
|
||||
}
|
||||
return subscriptions;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "ArmResourceUtils/listSubscriptions");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 422867 - return continuation token instead of read through
|
||||
public static async listCosmosdbAccounts(
|
||||
subscriptionIds: string[],
|
||||
tenantId?: string
|
||||
): Promise<Array<DatabaseAccount>> {
|
||||
if (!subscriptionIds || !subscriptionIds.length) {
|
||||
return Promise.reject("No subscription passed in");
|
||||
}
|
||||
|
||||
let accounts: Array<DatabaseAccount> = [];
|
||||
|
||||
try {
|
||||
const subscriptionFilter = "subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'";
|
||||
const urlFilter = `$filter=(${subscriptionFilter}) and (resourceType eq 'microsoft.documentdb/databaseaccounts')`;
|
||||
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
|
||||
let nextLink = `${ArmResourceUtils._armEndpoint}/resources?api-version=${ArmResourceUtils._armApiVersion}&${urlFilter}`;
|
||||
|
||||
while (nextLink) {
|
||||
const response: Response = await fetch(nextLink, { headers: fetchHeaders });
|
||||
const result: AccountListResult =
|
||||
response.status === 204 || response.status === 304 ? null : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
accounts = [...accounts, ...result.value];
|
||||
}
|
||||
return accounts;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "ArmResourceUtils/listAccounts");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getCosmosdbAccount(cosmosdbResourceId: string, tenantId?: string): Promise<DatabaseAccount> {
|
||||
if (!cosmosdbResourceId) {
|
||||
return Promise.reject("No Cosmos DB resource id passed in");
|
||||
}
|
||||
try {
|
||||
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
|
||||
const url = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}?api-version=${Constants.ArmApiVersions.documentDB}`;
|
||||
|
||||
const response: Response = await fetch(url, { headers: fetchHeaders });
|
||||
const result: DatabaseAccount = response.status === 204 || response.status === 304 ? null : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getCosmosdbKeys(cosmosdbResourceId: string, tenantId?: string): Promise<AccountKeys> {
|
||||
if (!cosmosdbResourceId) {
|
||||
return Promise.reject("No Cosmos DB resource id passed in");
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId);
|
||||
const readWriteKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/listKeys?api-version=${Constants.ArmApiVersions.documentDB}`;
|
||||
const readOnlyKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/readOnlyKeys?api-version=${Constants.ArmApiVersions.documentDB}`;
|
||||
let response: Response = await fetch(readWriteKeysUrl, { headers: fetchHeaders, method: "POST" });
|
||||
if (response.status === Constants.HttpStatusCodes.Forbidden) {
|
||||
// fetch read only keys for readers
|
||||
response = await fetch(readOnlyKeysUrl, { headers: fetchHeaders, method: "POST" });
|
||||
}
|
||||
const result: AccountKeys =
|
||||
response.status === Constants.HttpStatusCodes.NoContent ||
|
||||
response.status === Constants.HttpStatusCodes.NotModified
|
||||
? null
|
||||
: await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAccountKeys");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getAuthToken(tenantId?: string): Promise<string> {
|
||||
try {
|
||||
const token = await AuthHeadersUtil.getAccessToken(ArmResourceUtils._armAuthArea, tenantId);
|
||||
return token;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAuthToken");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async _getAuthHeader(authArea: string, tenantId?: string): Promise<Headers> {
|
||||
const token = await AuthHeadersUtil.getAccessToken(authArea, tenantId);
|
||||
let fetchHeaders = new Headers();
|
||||
fetchHeaders.append("authorization", `Bearer ${token}`);
|
||||
return fetchHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
interface TenantListResult {
|
||||
nextLink: string;
|
||||
value: Tenant[];
|
||||
}
|
||||
|
||||
interface SubscriptionListResult {
|
||||
nextLink: string;
|
||||
value: Subscription[];
|
||||
}
|
||||
|
||||
interface AccountListResult {
|
||||
nextLink: string;
|
||||
value: DatabaseAccount[];
|
||||
}
|
||||
@@ -12,29 +12,6 @@ import { userContext } from "../../UserContext";
|
||||
export default class AuthHeadersUtil {
|
||||
public static serverId: string = Constants.ServerIds.productionPortal;
|
||||
|
||||
private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d";
|
||||
private static readonly _aadEndpoint: string = configContext.AAD_ENDPOINT;
|
||||
private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT;
|
||||
private static readonly _arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT;
|
||||
private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA;
|
||||
private static readonly _graphEndpoint: string = configContext.GRAPH_ENDPOINT;
|
||||
private static readonly _graphApiVersion: string = configContext.GRAPH_API_VERSION;
|
||||
|
||||
private static _authContext: AuthenticationContext = new AuthenticationContext({
|
||||
instance: AuthHeadersUtil._aadEndpoint,
|
||||
clientId: AuthHeadersUtil._firstPartyAppId,
|
||||
postLogoutRedirectUri: window.location.origin,
|
||||
endpoints: {
|
||||
aad: AuthHeadersUtil._aadEndpoint,
|
||||
graph: AuthHeadersUtil._graphEndpoint,
|
||||
armAuthArea: AuthHeadersUtil._armAuthArea,
|
||||
armEndpoint: AuthHeadersUtil._armEndpoint,
|
||||
arcadiaEndpoint: AuthHeadersUtil._arcadiaEndpoint
|
||||
},
|
||||
tenant: undefined,
|
||||
cacheLocation: window.navigator.userAgent.indexOf("Edge") > -1 ? "localStorage" : undefined
|
||||
});
|
||||
|
||||
public static getAccessInputMetadata(accessInput: string): Q.Promise<DataModels.AccessInputMetadata> {
|
||||
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>();
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`;
|
||||
@@ -116,154 +93,6 @@ export default class AuthHeadersUtil {
|
||||
});
|
||||
}
|
||||
|
||||
public static isUserSignedIn(): boolean {
|
||||
const user = AuthHeadersUtil._authContext.getCachedUser();
|
||||
return !!user;
|
||||
}
|
||||
|
||||
public static getCachedUser(): AuthenticationContext.UserInfo {
|
||||
if (this.isUserSignedIn()) {
|
||||
return AuthHeadersUtil._authContext.getCachedUser();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public static signIn() {
|
||||
if (!AuthHeadersUtil.isUserSignedIn()) {
|
||||
AuthHeadersUtil._authContext.login();
|
||||
}
|
||||
}
|
||||
|
||||
public static signOut() {
|
||||
AuthHeadersUtil._authContext.logOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process token from oauth after login or get cached
|
||||
*/
|
||||
public static processTokenResponse() {
|
||||
const isCallback = AuthHeadersUtil._authContext.isCallback(window.location.hash);
|
||||
if (isCallback && !AuthHeadersUtil._authContext.getLoginError()) {
|
||||
AuthHeadersUtil._authContext.handleWindowCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auth token to access apis (Graph, ARM)
|
||||
*
|
||||
* @param authEndpoint Default to ARM endpoint
|
||||
* @param tenantId if tenant id provided, tenant id will set at global. Can be reset with 'common'
|
||||
*/
|
||||
public static async getAccessToken(
|
||||
authEndpoint: string = AuthHeadersUtil._armAuthArea,
|
||||
tenantId?: string
|
||||
): Promise<string> {
|
||||
const AuthorizationType: string = (<any>window).authType;
|
||||
if (AuthorizationType === AuthType.EncryptedToken) {
|
||||
// setting authorization header to an undefined value causes the browser to exclude
|
||||
// the header, which is expected here
|
||||
throw new Error("auth type is encrypted token, should not get access token");
|
||||
}
|
||||
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
if (tenantId) {
|
||||
// if tenant id passed in, we will use this tenant id for all the rest calls until next tenant id passed in
|
||||
AuthHeadersUtil._authContext.config.tenant = tenantId;
|
||||
}
|
||||
|
||||
AuthHeadersUtil._authContext.acquireToken(
|
||||
authEndpoint,
|
||||
AuthHeadersUtil._authContext.config.tenant,
|
||||
(errorResponse: any, token: any) => {
|
||||
if (errorResponse && typeof errorResponse === "string") {
|
||||
if (errorResponse.indexOf("login is required") >= 0 || errorResponse.indexOf("AADSTS50058") === 0) {
|
||||
// Handle error AADSTS50058: A silent sign-in request was sent but no user is signed in.
|
||||
// The user's cached token is invalid, hence we let the user login again.
|
||||
AuthHeadersUtil._authContext.login();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._isMultifactorAuthRequired(errorResponse) ||
|
||||
errorResponse.indexOf("AADSTS53000") > -1 ||
|
||||
errorResponse.indexOf("AADSTS65001") > -1
|
||||
) {
|
||||
// Handle error AADSTS50079 and AADSTS50076: User needs to use multifactor authentication and acquireToken fails silent. Redirect
|
||||
// Handle error AADSTS53000: User needs to use compliant device to access resource when Conditional Access Policy is set up for user.
|
||||
AuthHeadersUtil._authContext.acquireTokenRedirect(
|
||||
authEndpoint,
|
||||
AuthHeadersUtil._authContext.config.tenant
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (errorResponse || !token) {
|
||||
Logger.logError(errorResponse, "Hosted/Authorization/_getAuthHeader");
|
||||
reject(errorResponse);
|
||||
return;
|
||||
}
|
||||
resolve(token);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getPhotoFromGraphAPI(): Promise<Blob> {
|
||||
const token = await this.getAccessToken(AuthHeadersUtil._graphEndpoint);
|
||||
const headers = new Headers();
|
||||
headers.append("Authorization", `Bearer ${token}`);
|
||||
|
||||
try {
|
||||
const response: Response = await fetch(
|
||||
`${AuthHeadersUtil._graphEndpoint}/me/thumbnailPhoto?api-version=${AuthHeadersUtil._graphApiVersion}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: headers
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
return response.blob();
|
||||
} catch (err) {
|
||||
return new Blob();
|
||||
}
|
||||
}
|
||||
|
||||
private static async _getTenant(subId: string): Promise<string | undefined> {
|
||||
if (subId) {
|
||||
try {
|
||||
// Follow https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/azure-resource-manager/resource-manager-api-authentication.md
|
||||
// TenantId will be returned in the header of the response.
|
||||
const response: Response = await fetch(
|
||||
`https://management.core.windows.net/subscriptions/${subId}?api-version=2015-01-01`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
} catch (reason) {
|
||||
if (reason.status === 401) {
|
||||
const authUrl: string = reason.headers
|
||||
.get("www-authenticate")
|
||||
.split(",")[0]
|
||||
.split("=")[1];
|
||||
// Fetch the tenant GUID ID and the length should be 36.
|
||||
const tenantId: string = authUrl.substring(authUrl.lastIndexOf("/") + 1, authUrl.lastIndexOf("/") + 37);
|
||||
return Promise.resolve(tenantId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private static _isMultifactorAuthRequired(errorResponse: string): boolean {
|
||||
for (const code of ["AADSTS50079", "AADSTS50076"]) {
|
||||
if (errorResponse.indexOf(code) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static _generateResourceUrl(): string {
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const subscriptionId: string = userContext.subscriptionId;
|
||||
|
||||
139
src/Platform/Hosted/Components/AccountSwitcher.tsx
Normal file
139
src/Platform/Hosted/Components/AccountSwitcher.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
// TODO: Renable this rule for the file or turn it off everywhere
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as React from "react";
|
||||
import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
|
||||
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
||||
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
||||
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
|
||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
|
||||
const buttonStyles: IButtonStyles = {
|
||||
root: {
|
||||
fontSize: StyleConstants.DefaultFontSize,
|
||||
height: 40,
|
||||
padding: 0,
|
||||
paddingLeft: 10,
|
||||
marginRight: 5,
|
||||
backgroundColor: StyleConstants.BaseDark,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootHovered: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootFocused: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootPressed: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
rootExpanded: {
|
||||
backgroundColor: StyleConstants.BaseHigh,
|
||||
color: StyleConstants.BaseLight
|
||||
},
|
||||
textContainer: {
|
||||
flexGrow: "initial"
|
||||
}
|
||||
};
|
||||
|
||||
const cachedSubscriptionId = localStorage.getItem("cachedSubscriptionId");
|
||||
const cachedDatabaseAccountName = localStorage.getItem("cachedDatabaseAccountName");
|
||||
|
||||
interface Props {
|
||||
armToken: string;
|
||||
setDatabaseAccount: (account: DatabaseAccount) => void;
|
||||
}
|
||||
|
||||
export const AccountSwitcher: React.FunctionComponent<Props> = ({ armToken, setDatabaseAccount }: Props) => {
|
||||
const subscriptions = useSubscriptions(armToken);
|
||||
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>(cachedSubscriptionId);
|
||||
const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
|
||||
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>(cachedDatabaseAccountName);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (accounts && selectedAccountName) {
|
||||
const account = accounts.find(account => account.name === selectedAccountName);
|
||||
// Only set a new account if one is found
|
||||
if (account) {
|
||||
setDatabaseAccount(account);
|
||||
}
|
||||
}
|
||||
}, [accounts, selectedAccountName]);
|
||||
|
||||
const menuProps: IContextualMenuProps = {
|
||||
directionalHintFixed: true,
|
||||
className: "accountSwitchContextualMenu",
|
||||
items: [
|
||||
{
|
||||
key: "switchSubscription",
|
||||
onRender: () => {
|
||||
const dropdownProps: IDropdownProps = {
|
||||
label: "Subscription",
|
||||
className: "accountSwitchSubscriptionDropdown",
|
||||
options: subscriptions.map(sub => {
|
||||
return {
|
||||
key: sub.subscriptionId,
|
||||
text: sub.displayName,
|
||||
data: sub
|
||||
};
|
||||
}),
|
||||
onChange: (event, option) => {
|
||||
const subscriptionId = String(option.key);
|
||||
setSelectedSubscriptionId(subscriptionId);
|
||||
localStorage.setItem("cachedSubscriptionId", subscriptionId);
|
||||
},
|
||||
defaultSelectedKey: selectedSubscriptionId,
|
||||
placeholder: "Select subscription from list",
|
||||
styles: {
|
||||
callout: "accountSwitchSubscriptionDropdownMenu"
|
||||
}
|
||||
};
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "switchAccount",
|
||||
onRender: (item, dismissMenu) => {
|
||||
const dropdownProps: IDropdownProps = {
|
||||
label: "Cosmos DB Account Name",
|
||||
className: "accountSwitchAccountDropdown",
|
||||
options: accounts.map(account => ({
|
||||
key: account.name,
|
||||
text: account.name,
|
||||
data: account
|
||||
})),
|
||||
onChange: (event, option) => {
|
||||
const accountName = String(option.key);
|
||||
setSelectedAccoutName(String(option.key));
|
||||
localStorage.setItem("cachedDatabaseAccountName", accountName);
|
||||
dismissMenu();
|
||||
},
|
||||
defaultSelectedKey: selectedAccountName,
|
||||
placeholder: "No Cosmos DB accounts found",
|
||||
styles: {
|
||||
callout: "accountSwitchAccountDropdownMenu"
|
||||
}
|
||||
};
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaultButton
|
||||
text={selectedAccountName || "Select Database Account"}
|
||||
menuProps={menuProps}
|
||||
styles={buttonStyles}
|
||||
className="accountSwitchButton"
|
||||
id="accountSwitchButton"
|
||||
/>
|
||||
);
|
||||
};
|
||||
95
src/Platform/Hosted/Components/ConnectExplorer.tsx
Normal file
95
src/Platform/Hosted/Components/ConnectExplorer.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as React from "react";
|
||||
import { useBoolean } from "@uifabric/react-hooks";
|
||||
import { HttpHeaders } from "../../../Common/Constants";
|
||||
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
||||
|
||||
interface Props {
|
||||
connectionString: string;
|
||||
login: () => void;
|
||||
setEncryptedToken: (token: string) => void;
|
||||
setConnectionString: (connectionString: string) => void;
|
||||
setAuthType: (authType: AuthType) => void;
|
||||
}
|
||||
|
||||
export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
setEncryptedToken,
|
||||
login,
|
||||
setAuthType,
|
||||
connectionString,
|
||||
setConnectionString
|
||||
}: Props) => {
|
||||
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
|
||||
|
||||
return (
|
||||
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
||||
<div className="connectExplorerFormContainer">
|
||||
<div className="connectExplorer">
|
||||
<p className="connectExplorerContent">
|
||||
<img src="images/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
||||
{isFormVisible ? (
|
||||
<form
|
||||
id="connectWithConnectionString"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault();
|
||||
|
||||
if (isResourceTokenConnectionString(connectionString)) {
|
||||
setAuthType(AuthType.ResourceToken);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
// This API has a quirk where it must be parsed twice
|
||||
const result: GenerateTokenResponse = JSON.parse(await response.json());
|
||||
console.log(result.readWrite || result.read);
|
||||
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
|
||||
setAuthType(AuthType.ConnectionString);
|
||||
}}
|
||||
>
|
||||
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
|
||||
<p className="connectExplorerContent">
|
||||
<input
|
||||
className="inputToken"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Please enter a connection string"
|
||||
value={connectionString}
|
||||
onChange={event => {
|
||||
setConnectionString(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
|
||||
<img className="errorImg" src="images/error.svg" alt="Error notification" />
|
||||
<span className="errorDetails"></span>
|
||||
</span>
|
||||
</p>
|
||||
<p className="connectExplorerContent">
|
||||
<input className="filterbtnstyle" type="submit" value="Connect" />
|
||||
</p>
|
||||
<p className="switchConnectTypeText" onClick={login}>
|
||||
Sign In with Azure Account
|
||||
</p>
|
||||
</form>
|
||||
) : (
|
||||
<div id="connectWithAad">
|
||||
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
|
||||
<p className="switchConnectTypeText" onClick={showForm}>
|
||||
Connect to your account with connection string
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
src/Platform/Hosted/Components/DirectoryPickerPanel.tsx
Normal file
39
src/Platform/Hosted/Components/DirectoryPickerPanel.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Panel, PanelType, ChoiceGroup } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { useDirectories } from "../../../hooks/useDirectories";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
dismissPanel: () => void;
|
||||
tenantId: string;
|
||||
armToken: string;
|
||||
switchTenant: (tenantId: string) => void;
|
||||
}
|
||||
|
||||
export const DirectoryPickerPanel: React.FunctionComponent<Props> = ({
|
||||
isOpen,
|
||||
dismissPanel,
|
||||
armToken,
|
||||
tenantId,
|
||||
switchTenant
|
||||
}: Props) => {
|
||||
const directories = useDirectories(armToken);
|
||||
return (
|
||||
<Panel
|
||||
type={PanelType.medium}
|
||||
headerText="Select Directory"
|
||||
isOpen={isOpen}
|
||||
onDismiss={dismissPanel}
|
||||
closeButtonAriaLabel="Close"
|
||||
>
|
||||
<ChoiceGroup
|
||||
options={directories.map(dir => ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))}
|
||||
selectedKey={tenantId}
|
||||
onChange={async (event, option) => {
|
||||
switchTenant(option.key);
|
||||
dismissPanel();
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
22
src/Platform/Hosted/Components/FeedbackCommandButton.tsx
Normal file
22
src/Platform/Hosted/Components/FeedbackCommandButton.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import FeedbackIcon from "../../../../images/Feedback.svg";
|
||||
|
||||
export const FeedbackCommandButton: React.FunctionComponent = () => {
|
||||
return (
|
||||
<div className="feedbackConnectSettingIcons">
|
||||
<CommandButtonComponent
|
||||
id="commandbutton-feedback"
|
||||
iconSrc={FeedbackIcon}
|
||||
iconAlt="feeback button"
|
||||
onCommandClick={() =>
|
||||
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback")
|
||||
}
|
||||
ariaLabel="feeback button"
|
||||
tooltipText="Send feedback"
|
||||
hasPopup={true}
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
68
src/Platform/Hosted/Components/MeControl.tsx
Normal file
68
src/Platform/Hosted/Components/MeControl.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
FocusZone,
|
||||
DefaultButton,
|
||||
DirectionalHint,
|
||||
Persona,
|
||||
PersonaInitialsColor,
|
||||
PersonaSize
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { Account } from "msal";
|
||||
import { useGraphPhoto } from "../../../hooks/useGraphPhoto";
|
||||
|
||||
interface Props {
|
||||
graphToken: string;
|
||||
account: Account;
|
||||
openPanel: () => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const MeControl: React.FunctionComponent<Props> = ({ openPanel, logout, account, graphToken }: Props) => {
|
||||
const photo = useGraphPhoto(graphToken);
|
||||
return (
|
||||
<FocusZone>
|
||||
<DefaultButton
|
||||
id="mecontrolHeader"
|
||||
className="mecontrolHeaderButton"
|
||||
menuProps={{
|
||||
className: "mecontrolContextualMenu",
|
||||
isBeakVisible: false,
|
||||
directionalHintFixed: true,
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
calloutProps: {
|
||||
minPagePadding: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
key: "SwitchDirectory",
|
||||
text: "Switch Directory",
|
||||
onClick: openPanel
|
||||
},
|
||||
{
|
||||
key: "SignOut",
|
||||
text: "Sign Out",
|
||||
onClick: logout
|
||||
}
|
||||
]
|
||||
}}
|
||||
styles={{
|
||||
rootHovered: { backgroundColor: "#393939" },
|
||||
rootFocused: { backgroundColor: "#393939" },
|
||||
rootPressed: { backgroundColor: "#393939" },
|
||||
rootExpanded: { backgroundColor: "#393939" }
|
||||
}}
|
||||
>
|
||||
<Persona
|
||||
imageUrl={photo}
|
||||
text={account?.name}
|
||||
secondaryText={account?.userName}
|
||||
showSecondaryText={true}
|
||||
showInitialsUntilImageLoads={true}
|
||||
initialsColor={PersonaInitialsColor.teal}
|
||||
size={PersonaSize.size28}
|
||||
className="mecontrolHeaderPersona"
|
||||
/>
|
||||
</DefaultButton>
|
||||
</FocusZone>
|
||||
);
|
||||
};
|
||||
21
src/Platform/Hosted/Components/SignInButton.tsx
Normal file
21
src/Platform/Hosted/Components/SignInButton.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { DefaultButton } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
|
||||
interface Props {
|
||||
login: () => void;
|
||||
}
|
||||
|
||||
export const SignInButton: React.FunctionComponent<Props> = ({ login }: Props) => {
|
||||
return (
|
||||
<DefaultButton
|
||||
className="mecontrolSigninButton"
|
||||
text="Sign In"
|
||||
onClick={login}
|
||||
styles={{
|
||||
rootHovered: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootFocused: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootPressed: { backgroundColor: "#393939", color: "#fff" }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
101
src/Platform/Hosted/ConnectScreen.less
Normal file
101
src/Platform/Hosted/ConnectScreen.less
Normal file
@@ -0,0 +1,101 @@
|
||||
.connectExplorerContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorerFormContainer {
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: -ms-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer {
|
||||
text-align: center;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: -ms-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .welcomeText {
|
||||
font-size: 14px;
|
||||
color: #393939;
|
||||
margin: 8px 8px 16px 8px;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .switchConnectTypeText {
|
||||
margin: 8px;
|
||||
font-size: 12px;
|
||||
color: #0058ad;
|
||||
cursor: pointer;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectStringText {
|
||||
font-size: 12px;
|
||||
color: #393939;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent {
|
||||
margin: 8px;
|
||||
color: #393939;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .inputToken {
|
||||
width: 300px;
|
||||
padding: 0px 4px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .inputToken::placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-left: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip:hover .errorDetails {
|
||||
visibility: visible;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails {
|
||||
bottom: 24px;
|
||||
width: 145px;
|
||||
visibility: hidden;
|
||||
background-color: #393939;
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: -10px;
|
||||
padding: 6px;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails:after {
|
||||
border-width: 10px 10px 0px 10px;
|
||||
bottom: -8px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
border-style: solid;
|
||||
left: 12px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: #3b3b3b transparent;
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorImg {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.filterbtnstyle {
|
||||
background: #0058ad;
|
||||
width: 90px;
|
||||
height: 25px;
|
||||
color: white;
|
||||
border: solid 1px;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { ConnectionStringParser } from "./ConnectionStringParser";
|
||||
import { parseConnectionString } from "./ConnectionStringParser";
|
||||
|
||||
describe("ConnectionStringParser", () => {
|
||||
const mockAccountName: string = "Test";
|
||||
const mockMasterKey: string = "some-key";
|
||||
|
||||
it("should parse a valid sql account connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
const metadata = parseConnectionString(
|
||||
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};`
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("ConnectionStringParser", () => {
|
||||
});
|
||||
|
||||
it("should parse a valid mongo account connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
const metadata = parseConnectionString(
|
||||
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255`
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("ConnectionStringParser", () => {
|
||||
});
|
||||
|
||||
it("should parse a valid compute mongo account connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
const metadata = parseConnectionString(
|
||||
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255`
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("ConnectionStringParser", () => {
|
||||
});
|
||||
|
||||
it("should parse a valid graph account connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
const metadata = parseConnectionString(
|
||||
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;`
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("ConnectionStringParser", () => {
|
||||
});
|
||||
|
||||
it("should parse a valid table account connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
const metadata = parseConnectionString(
|
||||
`DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("ConnectionStringParser", () => {
|
||||
});
|
||||
|
||||
it("should parse a valid cassandra account connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
const metadata = parseConnectionString(
|
||||
`AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};`
|
||||
);
|
||||
|
||||
@@ -60,15 +60,13 @@ describe("ConnectionStringParser", () => {
|
||||
});
|
||||
|
||||
it("should fail to parse an invalid connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
|
||||
"some-rogue-connection-string"
|
||||
);
|
||||
const metadata = parseConnectionString("some-rogue-connection-string");
|
||||
|
||||
expect(metadata).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should fail to parse an empty connection string", () => {
|
||||
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString("");
|
||||
const metadata = parseConnectionString("");
|
||||
|
||||
expect(metadata).toBe(undefined);
|
||||
});
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { AccessInputMetadata, ApiKind } from "../../../Contracts/DataModels";
|
||||
|
||||
export class ConnectionStringParser {
|
||||
public static parseConnectionString(connectionString: string): DataModels.AccessInputMetadata {
|
||||
if (!!connectionString) {
|
||||
try {
|
||||
const accessInput: DataModels.AccessInputMetadata = {} as DataModels.AccessInputMetadata;
|
||||
const connectionStringParts = connectionString.split(";");
|
||||
export function parseConnectionString(connectionString: string): AccessInputMetadata {
|
||||
if (connectionString) {
|
||||
try {
|
||||
const accessInput = {} as AccessInputMetadata;
|
||||
const connectionStringParts = connectionString.split(";");
|
||||
|
||||
connectionStringParts.forEach((connectionStringPart: string) => {
|
||||
if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) {
|
||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1];
|
||||
accessInput.apiKind = DataModels.ApiKind.SQL;
|
||||
} else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) {
|
||||
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo);
|
||||
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
||||
accessInput.apiKind = DataModels.ApiKind.MongoDB;
|
||||
} else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) {
|
||||
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
|
||||
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
||||
accessInput.apiKind = DataModels.ApiKind.MongoDBCompute;
|
||||
} else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) {
|
||||
Constants.EndpointsRegex.cassandra.forEach(regex => {
|
||||
if (RegExp(regex).test(connectionStringPart)) {
|
||||
accessInput.accountName = connectionStringPart.match(regex)[1];
|
||||
accessInput.apiKind = DataModels.ApiKind.Cassandra;
|
||||
}
|
||||
});
|
||||
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
|
||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
|
||||
accessInput.apiKind = DataModels.ApiKind.Table;
|
||||
} else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) {
|
||||
accessInput.apiKind = DataModels.ApiKind.Graph;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(accessInput).length === 0) {
|
||||
return undefined;
|
||||
connectionStringParts.forEach((connectionStringPart: string) => {
|
||||
if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) {
|
||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1];
|
||||
accessInput.apiKind = ApiKind.SQL;
|
||||
} else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) {
|
||||
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo);
|
||||
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
||||
accessInput.apiKind = ApiKind.MongoDB;
|
||||
} else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) {
|
||||
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
|
||||
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
||||
accessInput.apiKind = ApiKind.MongoDBCompute;
|
||||
} else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) {
|
||||
Constants.EndpointsRegex.cassandra.forEach(regex => {
|
||||
if (RegExp(regex).test(connectionStringPart)) {
|
||||
accessInput.accountName = connectionStringPart.match(regex)[1];
|
||||
accessInput.apiKind = ApiKind.Cassandra;
|
||||
}
|
||||
});
|
||||
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
|
||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
|
||||
accessInput.apiKind = ApiKind.Table;
|
||||
} else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) {
|
||||
accessInput.apiKind = ApiKind.Graph;
|
||||
}
|
||||
});
|
||||
|
||||
return accessInput;
|
||||
} catch (error) {
|
||||
if (Object.keys(accessInput).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return accessInput;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
import Main from "./Main";
|
||||
|
||||
describe("Main", () => {
|
||||
it("correctly detects feature flags", () => {
|
||||
// Search containing non-features, with Camelcase keys and uri encoded values
|
||||
const params = new URLSearchParams(
|
||||
"?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey"
|
||||
);
|
||||
const features = Main.extractFeatures(params);
|
||||
|
||||
expect(features).toEqual({
|
||||
notebookserverurl: "https://localhost:10001/12345/notebook",
|
||||
notebookservertoken: "token",
|
||||
enablenotebooks: "true"
|
||||
});
|
||||
});
|
||||
import { isResourceTokenConnectionString, parseResourceTokenConnectionString } from "./ResourceTokenUtils";
|
||||
|
||||
describe("parseResourceTokenConnectionString", () => {
|
||||
it("correctly parses resource token connection string", () => {
|
||||
const connectionString =
|
||||
"AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;";
|
||||
const properties = Main.parseResourceTokenConnectionString(connectionString);
|
||||
const properties = parseResourceTokenConnectionString(connectionString);
|
||||
|
||||
expect(properties).toEqual({
|
||||
accountEndpoint: "fakeEndpoint",
|
||||
@@ -32,7 +18,7 @@ describe("Main", () => {
|
||||
it("correctly parses resource token connection string with partition key", () => {
|
||||
const connectionString =
|
||||
"type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;PartitionKey=fakePartitionKey;";
|
||||
const properties = Main.parseResourceTokenConnectionString(connectionString);
|
||||
const properties = parseResourceTokenConnectionString(connectionString);
|
||||
|
||||
expect(properties).toEqual({
|
||||
accountEndpoint: "fakeEndpoint",
|
||||
@@ -43,3 +29,16 @@ describe("Main", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isResourceToken", () => {
|
||||
it("valid resource connection string", () => {
|
||||
const connectionString =
|
||||
"AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;";
|
||||
expect(isResourceTokenConnectionString(connectionString)).toBe(true);
|
||||
});
|
||||
|
||||
it("non-resource connection string", () => {
|
||||
const connectionString = "AccountEndpoint=https://stfaul-sql.documents.azure.com:443/;AccountKey=foo;";
|
||||
expect(isResourceTokenConnectionString(connectionString)).toBe(false);
|
||||
});
|
||||
});
|
||||
43
src/Platform/Hosted/Helpers/ResourceTokenUtils.ts
Normal file
43
src/Platform/Hosted/Helpers/ResourceTokenUtils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface ParsedResourceTokenConnectionString {
|
||||
accountEndpoint: string;
|
||||
collectionId: string;
|
||||
databaseId: string;
|
||||
partitionKey?: string;
|
||||
resourceToken: string;
|
||||
}
|
||||
|
||||
export function parseResourceTokenConnectionString(connectionString: string): ParsedResourceTokenConnectionString {
|
||||
let accountEndpoint: string;
|
||||
let collectionId: string;
|
||||
let databaseId: string;
|
||||
let partitionKey: string;
|
||||
let resourceToken: string;
|
||||
const connectionStringParts = connectionString.split(";");
|
||||
connectionStringParts.forEach((part: string) => {
|
||||
if (part.startsWith("type=resource")) {
|
||||
resourceToken = part + ";";
|
||||
} else if (part.startsWith("AccountEndpoint=")) {
|
||||
accountEndpoint = part.substring(16);
|
||||
} else if (part.startsWith("DatabaseId=")) {
|
||||
databaseId = part.substring(11);
|
||||
} else if (part.startsWith("CollectionId=")) {
|
||||
collectionId = part.substring(13);
|
||||
} else if (part.startsWith("PartitionKey=")) {
|
||||
partitionKey = part.substring(13);
|
||||
} else if (part !== "") {
|
||||
resourceToken += part + ";";
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accountEndpoint,
|
||||
collectionId,
|
||||
databaseId,
|
||||
partitionKey,
|
||||
resourceToken
|
||||
};
|
||||
}
|
||||
|
||||
export function isResourceTokenConnectionString(connectionString: string): boolean {
|
||||
return connectionString && connectionString.includes("type=resource");
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AccessInputMetadata } from "../../Contracts/DataModels";
|
||||
import { HostedUtils } from "./HostedUtils";
|
||||
import { getDatabaseAccountPropertiesFromMetadata } from "./HostedUtils";
|
||||
|
||||
describe("getDatabaseAccountPropertiesFromMetadata", () => {
|
||||
it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => {
|
||||
let mongoComputeAccount: AccessInputMetadata = {
|
||||
const mongoComputeAccount: AccessInputMetadata = {
|
||||
accountName: "compute-batch2",
|
||||
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
|
||||
apiKind: 5,
|
||||
@@ -11,21 +11,21 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => {
|
||||
expiryTimestamp: "1234",
|
||||
mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/"
|
||||
};
|
||||
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({
|
||||
expect(getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({
|
||||
mongoEndpoint: mongoComputeAccount.mongoEndpoint,
|
||||
documentEndpoint: mongoComputeAccount.documentEndpoint
|
||||
});
|
||||
});
|
||||
|
||||
it("should not return an object with the mongoEndpoint key if the apiKind is mongo (1)", () => {
|
||||
let mongoAccount: AccessInputMetadata = {
|
||||
const mongoAccount: AccessInputMetadata = {
|
||||
accountName: "compute-batch2",
|
||||
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
|
||||
apiKind: 1,
|
||||
documentEndpoint: "https://compute-batch2.documents.azure.com:443/",
|
||||
expiryTimestamp: "1234"
|
||||
};
|
||||
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({
|
||||
expect(getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({
|
||||
documentEndpoint: mongoAccount.documentEndpoint
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,33 +3,49 @@ import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AccessInputMetadata } from "../../Contracts/DataModels";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
|
||||
export class HostedUtils {
|
||||
static getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): any {
|
||||
let properties = { documentEndpoint: metadata.documentEndpoint };
|
||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind);
|
||||
export function getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): unknown {
|
||||
let properties = { documentEndpoint: metadata.documentEndpoint };
|
||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind);
|
||||
|
||||
if (apiExperience === Constants.DefaultAccountExperience.Cassandra) {
|
||||
if (apiExperience === Constants.DefaultAccountExperience.Cassandra) {
|
||||
properties = Object.assign(properties, {
|
||||
cassandraEndpoint: metadata.apiEndpoint,
|
||||
capabilities: [{ name: Constants.CapabilityNames.EnableCassandra }]
|
||||
});
|
||||
} else if (apiExperience === Constants.DefaultAccountExperience.Table) {
|
||||
properties = Object.assign(properties, {
|
||||
tableEndpoint: metadata.apiEndpoint,
|
||||
capabilities: [{ name: Constants.CapabilityNames.EnableTable }]
|
||||
});
|
||||
} else if (apiExperience === Constants.DefaultAccountExperience.Graph) {
|
||||
properties = Object.assign(properties, {
|
||||
gremlinEndpoint: metadata.apiEndpoint,
|
||||
capabilities: [{ name: Constants.CapabilityNames.EnableGremlin }]
|
||||
});
|
||||
} else if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||
if (metadata.apiKind === DataModels.ApiKind.MongoDBCompute) {
|
||||
properties = Object.assign(properties, {
|
||||
cassandraEndpoint: metadata.apiEndpoint,
|
||||
capabilities: [{ name: Constants.CapabilityNames.EnableCassandra }]
|
||||
mongoEndpoint: metadata.mongoEndpoint
|
||||
});
|
||||
} else if (apiExperience === Constants.DefaultAccountExperience.Table) {
|
||||
properties = Object.assign(properties, {
|
||||
tableEndpoint: metadata.apiEndpoint,
|
||||
capabilities: [{ name: Constants.CapabilityNames.EnableTable }]
|
||||
});
|
||||
} else if (apiExperience === Constants.DefaultAccountExperience.Graph) {
|
||||
properties = Object.assign(properties, {
|
||||
gremlinEndpoint: metadata.apiEndpoint,
|
||||
capabilities: [{ name: Constants.CapabilityNames.EnableGremlin }]
|
||||
});
|
||||
} else if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||
if (metadata.apiKind === DataModels.ApiKind.MongoDBCompute) {
|
||||
properties = Object.assign(properties, {
|
||||
mongoEndpoint: metadata.mongoEndpoint
|
||||
});
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
export function getDatabaseAccountKindFromExperience(apiExperience: string): string {
|
||||
if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||
return Constants.AccountKind.MongoDB;
|
||||
}
|
||||
|
||||
if (apiExperience === Constants.DefaultAccountExperience.ApiForMongoDB) {
|
||||
return Constants.AccountKind.MongoDB;
|
||||
}
|
||||
|
||||
return Constants.AccountKind.GlobalDocumentDB;
|
||||
}
|
||||
|
||||
export function extractMasterKeyfromConnectionString(connectionString: string): string {
|
||||
// Only Gremlin uses the actual master key for connection to cosmos
|
||||
const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$");
|
||||
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
|
||||
}
|
||||
|
||||
@@ -1,598 +0,0 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import AuthHeadersUtil from "./Authorization";
|
||||
import Q from "q";
|
||||
import {
|
||||
AccessInputMetadata,
|
||||
AccountKeys,
|
||||
ApiKind,
|
||||
DatabaseAccount,
|
||||
GenerateTokenResponse,
|
||||
resourceTokenConnectionStringProperties
|
||||
} from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { CollectionCreation } from "../../Shared/Constants";
|
||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { HostedUtils } from "./HostedUtils";
|
||||
import { sendMessage } from "../../Common/MessageHandler";
|
||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||
import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { SubscriptionUtilMappings } from "../../Shared/Constants";
|
||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||
import Explorer from "../../Explorer/Explorer";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
export default class Main {
|
||||
private static _databaseAccountId: string;
|
||||
private static _encryptedToken: string;
|
||||
private static _accessInputMetadata: AccessInputMetadata;
|
||||
private static _features: { [key: string]: string };
|
||||
// For AAD, Need to post message to hosted frame to do the auth
|
||||
// Use local deferred variable as work around until we find better solution
|
||||
private static _getAadAccessDeferred: Q.Deferred<Explorer>;
|
||||
private static _explorer: Explorer;
|
||||
|
||||
public static isUsingEncryptionToken(): boolean {
|
||||
const params = new URLSearchParams(window.parent.location.search);
|
||||
if ((!!params && params.has("key")) || Main._hasCachedEncryptedKey()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static initializeExplorer(): Q.Promise<Explorer> {
|
||||
window.addEventListener("message", this._handleMessage.bind(this), false);
|
||||
this._features = {};
|
||||
const params = new URLSearchParams(window.parent.location.search);
|
||||
const deferred: Q.Deferred<Explorer> = Q.defer<Explorer>();
|
||||
let authType: string = null;
|
||||
|
||||
// Encrypted token flow
|
||||
if (!!params && params.has("key")) {
|
||||
Main._encryptedToken = encodeURIComponent(params.get("key"));
|
||||
SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken);
|
||||
authType = AuthType.EncryptedToken;
|
||||
} else if (Main._hasCachedEncryptedKey()) {
|
||||
Main._encryptedToken = SessionStorageUtility.getEntryString(StorageKey.EncryptedKeyToken);
|
||||
authType = AuthType.EncryptedToken;
|
||||
}
|
||||
|
||||
// Aad flow
|
||||
if (AuthHeadersUtil.isUserSignedIn()) {
|
||||
authType = AuthType.AAD;
|
||||
}
|
||||
|
||||
if (params) {
|
||||
this._features = Main.extractFeatures(params);
|
||||
}
|
||||
|
||||
(<any>window).authType = authType;
|
||||
if (!authType) {
|
||||
return Q.reject("Sign in needed");
|
||||
}
|
||||
|
||||
const explorer: Explorer = this._instantiateExplorer();
|
||||
if (authType === AuthType.EncryptedToken) {
|
||||
sendMessage({
|
||||
type: MessageTypes.UpdateAccountSwitch,
|
||||
props: {
|
||||
authType: AuthType.EncryptedToken,
|
||||
displayText: "Loading..."
|
||||
}
|
||||
});
|
||||
updateUserContext({
|
||||
accessToken: Main._encryptedToken
|
||||
});
|
||||
Main._getAccessInputMetadata(Main._encryptedToken).then(
|
||||
() => {
|
||||
const expiryTimestamp: number =
|
||||
Main._accessInputMetadata && parseInt(Main._accessInputMetadata.expiryTimestamp);
|
||||
if (authType === AuthType.EncryptedToken && (isNaN(expiryTimestamp) || expiryTimestamp <= 0)) {
|
||||
return deferred.reject("Token expired");
|
||||
}
|
||||
|
||||
Main._initDataExplorerFrameInputs(explorer);
|
||||
deferred.resolve(explorer);
|
||||
},
|
||||
(error: any) => {
|
||||
console.error(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
} else if (authType === AuthType.AAD) {
|
||||
sendMessage({
|
||||
type: MessageTypes.GetAccessAadRequest
|
||||
});
|
||||
if (this._getAadAccessDeferred != null) {
|
||||
// already request aad access, don't duplicate
|
||||
return Q(null);
|
||||
}
|
||||
this._explorer = explorer;
|
||||
this._getAadAccessDeferred = Q.defer<Explorer>();
|
||||
return this._getAadAccessDeferred.promise.finally(() => {
|
||||
this._getAadAccessDeferred = null;
|
||||
});
|
||||
} else {
|
||||
Main._initDataExplorerFrameInputs(explorer);
|
||||
deferred.resolve(explorer);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public static extractFeatures(params: URLSearchParams): { [key: string]: string } {
|
||||
const featureParamRegex = /feature.(.*)/i;
|
||||
const features: { [key: string]: string } = {};
|
||||
params.forEach((value: string, param: string) => {
|
||||
if (featureParamRegex.test(param)) {
|
||||
const matches: string[] = param.match(featureParamRegex);
|
||||
if (matches.length > 0) {
|
||||
features[matches[1].toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return features;
|
||||
}
|
||||
|
||||
public static configureTokenValidationDisplayPrompt(explorer: Explorer): void {
|
||||
const authType: AuthType = (<any>window).authType;
|
||||
if (
|
||||
!explorer ||
|
||||
!Main._encryptedToken ||
|
||||
!Main._accessInputMetadata ||
|
||||
Main._accessInputMetadata.expiryTimestamp == null ||
|
||||
authType !== AuthType.EncryptedToken
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
Main._showGuestAccessTokenRenewalPromptInMs(explorer, parseInt(Main._accessInputMetadata.expiryTimestamp));
|
||||
}
|
||||
|
||||
public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties {
|
||||
let accountEndpoint: string;
|
||||
let collectionId: string;
|
||||
let databaseId: string;
|
||||
let partitionKey: string;
|
||||
let resourceToken: string;
|
||||
const connectionStringParts = connectionString.split(";");
|
||||
connectionStringParts.forEach((part: string) => {
|
||||
if (part.startsWith("type=resource")) {
|
||||
resourceToken = part + ";";
|
||||
} else if (part.startsWith("AccountEndpoint=")) {
|
||||
accountEndpoint = part.substring(16);
|
||||
} else if (part.startsWith("DatabaseId=")) {
|
||||
databaseId = part.substring(11);
|
||||
} else if (part.startsWith("CollectionId=")) {
|
||||
collectionId = part.substring(13);
|
||||
} else if (part.startsWith("PartitionKey=")) {
|
||||
partitionKey = part.substring(13);
|
||||
} else if (part !== "") {
|
||||
resourceToken += part + ";";
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accountEndpoint,
|
||||
collectionId,
|
||||
databaseId,
|
||||
partitionKey,
|
||||
resourceToken
|
||||
};
|
||||
}
|
||||
|
||||
public static renewExplorerAccess = (explorer: Explorer, connectionString: string): Q.Promise<void> => {
|
||||
if (!connectionString) {
|
||||
console.error("Missing or invalid connection string input");
|
||||
Q.reject("Missing or invalid connection string input");
|
||||
}
|
||||
|
||||
if (Main._isResourceToken(connectionString)) {
|
||||
return Main._renewExplorerAccessWithResourceToken(explorer, connectionString);
|
||||
}
|
||||
|
||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
||||
AuthHeadersUtil.generateUnauthenticatedEncryptedTokenForConnectionString(connectionString).then(
|
||||
(encryptedToken: GenerateTokenResponse) => {
|
||||
if (!encryptedToken || !encryptedToken.readWrite) {
|
||||
deferred.reject("Encrypted token is empty or undefined");
|
||||
}
|
||||
|
||||
Main._encryptedToken = encryptedToken.readWrite;
|
||||
window.authType = AuthType.EncryptedToken;
|
||||
|
||||
updateUserContext({
|
||||
accessToken: Main._encryptedToken
|
||||
});
|
||||
Main._getAccessInputMetadata(Main._encryptedToken).then(
|
||||
() => {
|
||||
if (explorer.isConnectExplorerVisible()) {
|
||||
explorer.notificationConsoleData([]);
|
||||
explorer.hideConnectExplorerForm();
|
||||
}
|
||||
|
||||
if (Main._accessInputMetadata.apiKind != ApiKind.Graph) {
|
||||
// do not save encrypted token for graphs because we cannot extract master key in the client
|
||||
SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken);
|
||||
window.parent &&
|
||||
window.parent.history.replaceState(
|
||||
{ encryptedToken: encryptedToken },
|
||||
"",
|
||||
`?key=${Main._encryptedToken}${(window.parent && window.parent.location.hash) || ""}`
|
||||
); // replace query params if any
|
||||
} else {
|
||||
SessionStorageUtility.removeEntry(StorageKey.EncryptedKeyToken);
|
||||
window.parent &&
|
||||
window.parent.history.replaceState(
|
||||
{ encryptedToken: encryptedToken },
|
||||
"",
|
||||
`?${(window.parent && window.parent.location.hash) || ""}`
|
||||
); // replace query params if any
|
||||
}
|
||||
|
||||
const masterKey: string = Main._getMasterKeyFromConnectionString(connectionString);
|
||||
Main.configureTokenValidationDisplayPrompt(explorer);
|
||||
Main._setExplorerReady(explorer, masterKey);
|
||||
|
||||
deferred.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
console.error(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error: any) => {
|
||||
deferred.reject(`Failed to generate encrypted token: ${getErrorMessage(error)}`);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
};
|
||||
|
||||
public static getUninitializedExplorerForGuestAccess(): Explorer {
|
||||
const explorer = Main._instantiateExplorer();
|
||||
if (window.authType === AuthType.AAD) {
|
||||
this._explorer = explorer;
|
||||
}
|
||||
(<any>window).dataExplorer = explorer;
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
private static _initDataExplorerFrameInputs(
|
||||
explorer: Explorer,
|
||||
masterKey?: string /* master key extracted from connection string if available */,
|
||||
account?: DatabaseAccount,
|
||||
authorizationToken?: string /* access key */
|
||||
): void {
|
||||
const serverId: string = AuthHeadersUtil.serverId;
|
||||
const authType: string = (<any>window).authType;
|
||||
const accountResourceId =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Main._databaseAccountId
|
||||
: authType === AuthType.AAD && account
|
||||
? account.id
|
||||
: "";
|
||||
const subscriptionId: string = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
||||
const resourceGroup: string = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
||||
|
||||
explorer.isTryCosmosDBSubscription(SubscriptionUtilMappings.FreeTierSubscriptionIds.indexOf(subscriptionId) >= 0);
|
||||
if (authorizationToken && authorizationToken.indexOf("Bearer") !== 0) {
|
||||
// Portal sends the auth token with bearer suffix, so we prepend the same to be consistent
|
||||
authorizationToken = `Bearer ${authorizationToken}`;
|
||||
}
|
||||
|
||||
if (authType === AuthType.EncryptedToken) {
|
||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||
Main._accessInputMetadata.apiKind
|
||||
);
|
||||
sendMessage({
|
||||
type: MessageTypes.UpdateAccountSwitch,
|
||||
props: {
|
||||
authType: AuthType.EncryptedToken,
|
||||
selectedAccountName: Main._accessInputMetadata.accountName
|
||||
}
|
||||
});
|
||||
return explorer.initDataExplorerWithFrameInputs({
|
||||
databaseAccount: {
|
||||
id: Main._databaseAccountId,
|
||||
name: Main._accessInputMetadata.accountName,
|
||||
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
|
||||
properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
|
||||
tags: { defaultExperience: apiExperience }
|
||||
},
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
masterKey,
|
||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||
authorizationToken: undefined,
|
||||
features: this._features,
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: null,
|
||||
serverId: serverId,
|
||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||
quotaId: undefined,
|
||||
addCollectionDefaultFlight: explorer.flight(),
|
||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
|
||||
});
|
||||
}
|
||||
|
||||
if (authType === AuthType.AAD) {
|
||||
const inputs: DataExplorerInputsFrame = {
|
||||
databaseAccount: account,
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
masterKey,
|
||||
hasWriteAccess: true, //TODO: 425017 - support read access
|
||||
authorizationToken,
|
||||
features: this._features,
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: null,
|
||||
serverId: serverId,
|
||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||
quotaId: undefined,
|
||||
addCollectionDefaultFlight: explorer.flight(),
|
||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription()
|
||||
};
|
||||
return explorer.initDataExplorerWithFrameInputs(inputs);
|
||||
}
|
||||
|
||||
if (authType === AuthType.ResourceToken) {
|
||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||
Main._accessInputMetadata.apiKind
|
||||
);
|
||||
return explorer.initDataExplorerWithFrameInputs({
|
||||
databaseAccount: {
|
||||
id: Main._databaseAccountId,
|
||||
name: Main._accessInputMetadata.accountName,
|
||||
kind: this._getDatabaseAccountKindFromExperience(apiExperience),
|
||||
properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata),
|
||||
tags: { defaultExperience: apiExperience }
|
||||
},
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
masterKey,
|
||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||
authorizationToken: undefined,
|
||||
features: this._features,
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: null,
|
||||
serverId: serverId,
|
||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||
quotaId: undefined,
|
||||
addCollectionDefaultFlight: explorer.flight(),
|
||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
||||
isAuthWithresourceToken: true
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported AuthType ${authType}`);
|
||||
}
|
||||
|
||||
private static _instantiateExplorer(): Explorer {
|
||||
const explorer = new Explorer();
|
||||
// workaround to resolve cyclic refs with view
|
||||
explorer.renewExplorerShareAccess = Main.renewExplorerAccess;
|
||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||
|
||||
// Hosted needs click to dismiss any menu
|
||||
if (window.authType === AuthType.AAD) {
|
||||
window.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
sendMessage({
|
||||
type: MessageTypes.ExplorerClickEvent
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
private static _showGuestAccessTokenRenewalPromptInMs(explorer: Explorer, interval: number): void {
|
||||
if (interval != null && !isNaN(interval)) {
|
||||
setTimeout(() => {
|
||||
explorer.displayGuestAccessTokenRenewalPrompt();
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
|
||||
private static _hasCachedEncryptedKey(): boolean {
|
||||
return SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken);
|
||||
}
|
||||
|
||||
private static _getDatabaseAccountKindFromExperience(apiExperience: string): string {
|
||||
if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||
return Constants.AccountKind.MongoDB;
|
||||
}
|
||||
|
||||
if (apiExperience === Constants.DefaultAccountExperience.ApiForMongoDB) {
|
||||
return Constants.AccountKind.MongoDB;
|
||||
}
|
||||
|
||||
return Constants.AccountKind.GlobalDocumentDB;
|
||||
}
|
||||
|
||||
private static _getAccessInputMetadata(accessInput: string): Q.Promise<void> {
|
||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
||||
AuthHeadersUtil.getAccessInputMetadata(accessInput).then(
|
||||
(metadata: any) => {
|
||||
Main._accessInputMetadata = metadata;
|
||||
deferred.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
}
|
||||
|
||||
private static _getMasterKeyFromConnectionString(connectionString: string): string {
|
||||
if (!connectionString || Main._accessInputMetadata == null || Main._accessInputMetadata.apiKind !== ApiKind.Graph) {
|
||||
// client only needs master key for Graph API
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const matchedParts: string[] = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$");
|
||||
return (matchedParts.length > 1 && matchedParts[1]) || undefined;
|
||||
}
|
||||
|
||||
private static _isResourceToken(connectionString: string): boolean {
|
||||
return connectionString && connectionString.includes("type=resource");
|
||||
}
|
||||
|
||||
private static _renewExplorerAccessWithResourceToken = (
|
||||
explorer: Explorer,
|
||||
connectionString: string
|
||||
): Q.Promise<void> => {
|
||||
window.authType = AuthType.ResourceToken;
|
||||
|
||||
const properties: resourceTokenConnectionStringProperties = Main.parseResourceTokenConnectionString(
|
||||
connectionString
|
||||
);
|
||||
if (
|
||||
!properties.accountEndpoint ||
|
||||
!properties.resourceToken ||
|
||||
!properties.databaseId ||
|
||||
!properties.collectionId
|
||||
) {
|
||||
console.error("Invalid connection string input");
|
||||
Q.reject("Invalid connection string input");
|
||||
}
|
||||
updateUserContext({
|
||||
resourceToken: properties.resourceToken,
|
||||
endpoint: properties.accountEndpoint
|
||||
});
|
||||
explorer.resourceTokenDatabaseId(properties.databaseId);
|
||||
explorer.resourceTokenCollectionId(properties.collectionId);
|
||||
if (properties.partitionKey) {
|
||||
explorer.resourceTokenPartitionKey(properties.partitionKey);
|
||||
}
|
||||
Main._accessInputMetadata = Main._getAccessInputMetadataFromAccountEndpoint(properties.accountEndpoint);
|
||||
|
||||
if (explorer.isConnectExplorerVisible()) {
|
||||
explorer.notificationConsoleData([]);
|
||||
explorer.hideConnectExplorerForm();
|
||||
}
|
||||
|
||||
Main._setExplorerReady(explorer);
|
||||
return Q.resolve();
|
||||
};
|
||||
|
||||
private static _getAccessInputMetadataFromAccountEndpoint = (accountEndpoint: string): AccessInputMetadata => {
|
||||
const documentEndpoint: string = accountEndpoint;
|
||||
const result: RegExpMatchArray = accountEndpoint.match("https://([^\\.]+)\\..+");
|
||||
const accountName: string = result && result[1];
|
||||
const apiEndpoint: string = accountEndpoint.substring(8);
|
||||
const apiKind: number = ApiKind.SQL;
|
||||
|
||||
return {
|
||||
accountName,
|
||||
apiEndpoint,
|
||||
apiKind,
|
||||
documentEndpoint,
|
||||
expiryTimestamp: ""
|
||||
};
|
||||
};
|
||||
|
||||
private static _setExplorerReady(
|
||||
explorer: Explorer,
|
||||
masterKey?: string,
|
||||
account?: DatabaseAccount,
|
||||
authorizationToken?: string
|
||||
) {
|
||||
Main._initDataExplorerFrameInputs(explorer, masterKey, account, authorizationToken);
|
||||
explorer.isAccountReady.valueHasMutated();
|
||||
sendMessage("ready");
|
||||
}
|
||||
|
||||
private static _shouldProcessMessage(event: MessageEvent): boolean {
|
||||
if (typeof event.data !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (event.data["signature"] !== "pcIframe") {
|
||||
return false;
|
||||
}
|
||||
if (!("data" in event.data)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof event.data["data"] !== "object") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _handleMessage(event: MessageEvent) {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._shouldProcessMessage(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message: any = event.data.data;
|
||||
if (message.type) {
|
||||
if (message.type === MessageTypes.GetAccessAadResponse && (message.response || message.error)) {
|
||||
if (message.response) {
|
||||
Main._handleGetAccessAadSucceed(message.response);
|
||||
}
|
||||
if (message.error) {
|
||||
Main._handleGetAccessAadFailed(message.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.type === MessageTypes.SwitchAccount && message.account && message.keys) {
|
||||
Main._handleSwitchAccountSucceed(message.account, message.keys, message.authorizationToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static _handleSwitchAccountSucceed(account: DatabaseAccount, keys: AccountKeys, authorizationToken: string) {
|
||||
if (!this._explorer) {
|
||||
console.error("no explorer found");
|
||||
return;
|
||||
}
|
||||
|
||||
this._explorer.hideConnectExplorerForm();
|
||||
|
||||
const masterKey = Main._getMasterKey(keys);
|
||||
this._explorer.notificationConsoleData([]);
|
||||
Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken);
|
||||
}
|
||||
|
||||
private static _handleGetAccessAadSucceed(response: [DatabaseAccount, AccountKeys, string]) {
|
||||
if (!response || response.length < 1) {
|
||||
return;
|
||||
}
|
||||
const account = response[0];
|
||||
const masterKey = Main._getMasterKey(response[1]);
|
||||
const authorizationToken = response[2];
|
||||
Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken);
|
||||
this._getAadAccessDeferred.resolve(this._explorer);
|
||||
}
|
||||
|
||||
private static _getMasterKey(keys: AccountKeys): string {
|
||||
return (
|
||||
keys?.primaryMasterKey ??
|
||||
keys?.secondaryMasterKey ??
|
||||
keys?.primaryReadonlyMasterKey ??
|
||||
keys?.secondaryReadonlyMasterKey
|
||||
);
|
||||
}
|
||||
|
||||
private static _handleGetAccessAadFailed(error: any) {
|
||||
this._getAadAccessDeferred.reject(error);
|
||||
}
|
||||
}
|
||||
17
src/Platform/Hosted/extractFeatures.test.ts
Normal file
17
src/Platform/Hosted/extractFeatures.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { extractFeatures } from "./extractFeatures";
|
||||
|
||||
describe("extractFeatures", () => {
|
||||
it("correctly detects feature flags", () => {
|
||||
// Search containing non-features, with Camelcase keys and uri encoded values
|
||||
const params = new URLSearchParams(
|
||||
"?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey"
|
||||
);
|
||||
const features = extractFeatures(params);
|
||||
|
||||
expect(features).toEqual({
|
||||
notebookserverurl: "https://localhost:10001/12345/notebook",
|
||||
notebookservertoken: "token",
|
||||
enablenotebooks: "true"
|
||||
});
|
||||
});
|
||||
});
|
||||
14
src/Platform/Hosted/extractFeatures.ts
Normal file
14
src/Platform/Hosted/extractFeatures.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function extractFeatures(params?: URLSearchParams): { [key: string]: string } {
|
||||
params = params || new URLSearchParams(window.parent.location.search);
|
||||
const featureParamRegex = /feature.(.*)/i;
|
||||
const features: { [key: string]: string } = {};
|
||||
params.forEach((value: string, param: string) => {
|
||||
if (featureParamRegex.test(param)) {
|
||||
const matches: string[] = param.match(featureParamRegex);
|
||||
if (matches.length > 0) {
|
||||
features[matches[1].toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return features;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||
import Explorer from "../../Explorer/Explorer";
|
||||
import { handleMessage } from "../../Controls/Heatmap/Heatmap";
|
||||
|
||||
export function initializeExplorer(): Explorer {
|
||||
const explorer = new Explorer();
|
||||
|
||||
// In development mode, try to load the iframe message from session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
|
||||
if (initMessage) {
|
||||
const message = JSON.parse(initMessage);
|
||||
console.warn("Loaded cached portal iframe message from session storage");
|
||||
console.dir(message);
|
||||
explorer.initDataExplorerWithFrameInputs(message);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||
|
||||
return explorer;
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
if (window.authType === AuthType.EncryptedToken) {
|
||||
@@ -21,19 +19,6 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet
|
||||
}
|
||||
}
|
||||
|
||||
export async function getArcadiaAuthToken(
|
||||
arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT,
|
||||
tenantId?: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const token = await AuthHeadersUtil.getAccessToken(arcadiaEndpoint, tenantId);
|
||||
return token;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "AuthorizationUtils/getArcadiaAuthToken");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function decryptJWTToken(token: string) {
|
||||
if (!token) {
|
||||
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
|
||||
import * as UserUtils from "./UserUtils";
|
||||
|
||||
describe("UserUtils", () => {
|
||||
it("getFullName works in regular data explorer (inside portal)", () => {
|
||||
const user: AuthenticationContext.UserInfo = {
|
||||
userName: "userName",
|
||||
profile: {
|
||||
name: "name"
|
||||
}
|
||||
};
|
||||
AuthHeadersUtil.getCachedUser = jest.fn().mockReturnValue(user);
|
||||
|
||||
expect(UserUtils.getFullName()).toBe("name");
|
||||
});
|
||||
|
||||
it("getFullName works in fullscreen data explorer (outside portal)", () => {
|
||||
jest.mock("./AuthorizationUtils", () => {
|
||||
(): { name: string } => ({ name: "name" });
|
||||
});
|
||||
|
||||
expect(UserUtils.getFullName()).toBe("name");
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,8 @@
|
||||
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
|
||||
import { decryptJWTToken } from "./AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getFullName(): string {
|
||||
let fullName: string;
|
||||
const user = AuthHeadersUtil.getCachedUser();
|
||||
if (user) {
|
||||
fullName = user.profile.name;
|
||||
} else {
|
||||
const authToken = userContext.authorizationToken;
|
||||
const props = decryptJWTToken(authToken);
|
||||
fullName = props.name;
|
||||
}
|
||||
|
||||
return fullName;
|
||||
const authToken = userContext.authorizationToken;
|
||||
const props = decryptJWTToken(authToken);
|
||||
return props.name;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import Explorer from "./Explorer/Explorer";
|
||||
|
||||
export const applyExplorerBindings = (explorer: Explorer) => {
|
||||
if (!!explorer) {
|
||||
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
||||
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
||||
sendMessage("ready");
|
||||
window.dataExplorer = explorer;
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
ko.applyBindings(explorer);
|
||||
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
||||
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
||||
sendMessage("ready");
|
||||
$("#divExplorer").show();
|
||||
}
|
||||
};
|
||||
|
||||
96
src/hooks/useAADAuth.ts
Normal file
96
src/hooks/useAADAuth.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as React from "react";
|
||||
import { useBoolean } from "@uifabric/react-hooks";
|
||||
import { UserAgentApplication, Account } from "msal";
|
||||
|
||||
const msal = new UserAgentApplication({
|
||||
cache: {
|
||||
cacheLocation: "localStorage"
|
||||
},
|
||||
auth: {
|
||||
authority: "https://login.microsoftonline.com/common",
|
||||
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
|
||||
redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set in development
|
||||
}
|
||||
});
|
||||
|
||||
const cachedAccount = msal.getAllAccounts()?.[0];
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
|
||||
interface ReturnType {
|
||||
isLoggedIn: boolean;
|
||||
graphToken: string;
|
||||
armToken: string;
|
||||
login: () => void;
|
||||
logout: () => void;
|
||||
tenantId: string;
|
||||
account: Account;
|
||||
switchTenant: (tenantId: string) => void;
|
||||
}
|
||||
|
||||
export function useAADAuth(): ReturnType {
|
||||
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(
|
||||
Boolean(cachedAccount && cachedTenantId) || false
|
||||
);
|
||||
const [account, setAccount] = React.useState<Account>(cachedAccount);
|
||||
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
||||
const [graphToken, setGraphToken] = React.useState<string>();
|
||||
const [armToken, setArmToken] = React.useState<string>();
|
||||
|
||||
const login = React.useCallback(async () => {
|
||||
const response = await msal.loginPopup();
|
||||
setLoggedIn();
|
||||
setAccount(response.account);
|
||||
setTenantId(response.tenantId);
|
||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||
}, []);
|
||||
|
||||
const logout = React.useCallback(() => {
|
||||
setLoggedOut();
|
||||
localStorage.removeItem("cachedTenantId");
|
||||
msal.logout();
|
||||
}, []);
|
||||
|
||||
const switchTenant = React.useCallback(
|
||||
async id => {
|
||||
const response = await msal.loginPopup({
|
||||
authority: `https://login.microsoftonline.com/${id}`
|
||||
});
|
||||
setTenantId(response.tenantId);
|
||||
setAccount(response.account);
|
||||
},
|
||||
[account, tenantId]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (account && tenantId) {
|
||||
Promise.all([
|
||||
msal.acquireTokenSilent({
|
||||
// There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority
|
||||
forceRefresh: true,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
scopes: ["https://graph.windows.net//.default"]
|
||||
}),
|
||||
msal.acquireTokenSilent({
|
||||
// There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority
|
||||
forceRefresh: true,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
scopes: ["https://management.azure.com//.default"]
|
||||
})
|
||||
]).then(([graphTokenResponse, armTokenResponse]) => {
|
||||
setGraphToken(graphTokenResponse.accessToken);
|
||||
setArmToken(armTokenResponse.accessToken);
|
||||
});
|
||||
}
|
||||
}, [account, tenantId]);
|
||||
|
||||
return {
|
||||
account,
|
||||
tenantId,
|
||||
isLoggedIn,
|
||||
graphToken,
|
||||
armToken,
|
||||
login,
|
||||
logout,
|
||||
switchTenant
|
||||
};
|
||||
}
|
||||
50
src/hooks/useDatabaseAccounts.tsx
Normal file
50
src/hooks/useDatabaseAccounts.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
|
||||
interface AccountListResult {
|
||||
nextLink: string;
|
||||
value: DatabaseAccount[];
|
||||
}
|
||||
|
||||
export async function fetchDatabaseAccounts(
|
||||
subscriptionIds: string[],
|
||||
accessToken: string
|
||||
): Promise<DatabaseAccount[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
if (!subscriptionIds || !subscriptionIds.length) {
|
||||
return Promise.reject("No subscription passed in");
|
||||
}
|
||||
|
||||
let accounts: Array<DatabaseAccount> = [];
|
||||
|
||||
const subscriptionFilter = "subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'";
|
||||
const urlFilter = `$filter=(${subscriptionFilter}) and (resourceType eq 'microsoft.documentdb/databaseaccounts')`;
|
||||
let nextLink = `https://management.azure.com/resources?api-version=2020-01-01&${urlFilter}`;
|
||||
|
||||
while (nextLink) {
|
||||
const response: Response = await fetch(nextLink, { headers });
|
||||
const result: AccountListResult =
|
||||
response.status === 204 || response.status === 304 ? undefined : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
accounts = [...accounts, ...result.value];
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] {
|
||||
const [state, setState] = useState<DatabaseAccount[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (subscriptionId && armToken) {
|
||||
fetchDatabaseAccounts([subscriptionId], armToken).then(response => setState(response));
|
||||
}
|
||||
}, [subscriptionId, armToken]);
|
||||
return state || [];
|
||||
}
|
||||
40
src/hooks/useDirectories.tsx
Normal file
40
src/hooks/useDirectories.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tenant } from "../Contracts/DataModels";
|
||||
|
||||
interface TenantListResult {
|
||||
nextLink: string;
|
||||
value: Tenant[];
|
||||
}
|
||||
|
||||
export async function fetchDirectories(accessToken: string): Promise<Tenant[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
let tenents: Array<Tenant> = [];
|
||||
let nextLink = `https://management.azure.com/tenants?api-version=2020-01-01`;
|
||||
|
||||
while (nextLink) {
|
||||
const response = await fetch(nextLink, { headers });
|
||||
const result: TenantListResult =
|
||||
response.status === 204 || response.status === 304 ? undefined : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
tenents = [...tenents, ...result.value];
|
||||
}
|
||||
return tenents;
|
||||
}
|
||||
|
||||
export function useDirectories(armToken: string): Tenant[] {
|
||||
const [state, setState] = useState<Tenant[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (armToken) {
|
||||
fetchDirectories(armToken).then(response => setState(response));
|
||||
}
|
||||
}, [armToken]);
|
||||
return state || [];
|
||||
}
|
||||
29
src/hooks/useGraphPhoto.tsx
Normal file
29
src/hooks/useGraphPhoto.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
headers.append("Content-Type", "image/jpg");
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options)
|
||||
.then(response => response.blob())
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
export function useGraphPhoto(graphToken: string): string {
|
||||
const [photo, setPhoto] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (graphToken) {
|
||||
fetchPhoto(graphToken).then(response => setPhoto(URL.createObjectURL(response)));
|
||||
}
|
||||
}, [graphToken]);
|
||||
return photo;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useAccount, useMsal } from "@azure/msal-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export async function fetchMe(accessToken: string): Promise<GraphMeResponse> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
console.log("EXECUTING REQUEST");
|
||||
return fetch("https://graph.microsoft.com/v1.0/me", options)
|
||||
.then(response => response.json())
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
headers.append("Content-Type", "image/jpg");
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
console.log("EXECUTING REQUEST");
|
||||
return fetch("https://graph.microsoft.com/v1.0/me/photo/$value", options)
|
||||
.then(response => response.blob())
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
interface GraphMeResponse {
|
||||
businessPhones: any[];
|
||||
displayName: string;
|
||||
givenName: string;
|
||||
jobTitle: string;
|
||||
mail: string;
|
||||
mobilePhone: null;
|
||||
officeLocation: string;
|
||||
preferredLanguage: null;
|
||||
surname: string;
|
||||
userPrincipalName: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function useGraphProfile(): { graphData: GraphMeResponse; photo: string } {
|
||||
const { instance, accounts } = useMsal();
|
||||
const account = useAccount(accounts[0] || {});
|
||||
const [graphData, setGraphData] = useState<GraphMeResponse>();
|
||||
const [photo, setPhoto] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("account", account);
|
||||
if (account) {
|
||||
instance
|
||||
.acquireTokenSilent({
|
||||
scopes: ["User.Read"],
|
||||
account
|
||||
})
|
||||
.then(response => {
|
||||
fetchMe(response.accessToken).then(response => setGraphData(response));
|
||||
fetchPhoto(response.accessToken).then(response => setPhoto(URL.createObjectURL(response)));
|
||||
});
|
||||
}
|
||||
}, [account]);
|
||||
return { graphData, photo };
|
||||
}
|
||||
36
src/hooks/usePortalAccessToken.tsx
Normal file
36
src/hooks/usePortalAccessToken.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ApiEndpoints } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { AccessInputMetadata } from "../Contracts/DataModels";
|
||||
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
|
||||
|
||||
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
||||
const headers = new Headers();
|
||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||
headers.append("x-ms-encrypted-auth-token", encodeURIComponent(portalToken));
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
return (
|
||||
fetch(url, options)
|
||||
.then(response => response.json())
|
||||
// Portal encrypted token API quirk: The response is double JSON encoded
|
||||
.then(json => JSON.parse(json))
|
||||
.catch(error => console.log(error))
|
||||
);
|
||||
}
|
||||
|
||||
export function usePortalAccessToken(token: string): AccessInputMetadata {
|
||||
const [state, setState] = useState<AccessInputMetadata>();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchAccessData(token).then(response => setState(response));
|
||||
}
|
||||
}, [token]);
|
||||
return state;
|
||||
}
|
||||
43
src/hooks/useSubscriptions.tsx
Normal file
43
src/hooks/useSubscriptions.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Subscription } from "../Contracts/DataModels";
|
||||
|
||||
interface SubscriptionListResult {
|
||||
nextLink: string;
|
||||
value: Subscription[];
|
||||
}
|
||||
|
||||
export async function fetchSubscriptions(accessToken: string): Promise<Subscription[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
let subscriptions: Array<Subscription> = [];
|
||||
let nextLink = `https://management.azure.com/subscriptions?api-version=2020-01-01`;
|
||||
|
||||
while (nextLink) {
|
||||
const response = await fetch(nextLink, { headers });
|
||||
const result: SubscriptionListResult =
|
||||
response.status === 204 || response.status === 304 ? undefined : await response.json();
|
||||
if (!response.ok) {
|
||||
throw result;
|
||||
}
|
||||
nextLink = result.nextLink;
|
||||
const validSubscriptions = result.value.filter(
|
||||
sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue"
|
||||
);
|
||||
subscriptions = [...subscriptions, ...validSubscriptions];
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
export function useSubscriptions(armToken: string): Subscription[] {
|
||||
const [state, setState] = useState<Subscription[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (armToken) {
|
||||
fetchSubscriptions(armToken).then(response => setState(response));
|
||||
}
|
||||
}, [armToken]);
|
||||
return state || [];
|
||||
}
|
||||
@@ -7,5 +7,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="App"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,13 +9,13 @@ export async function login(connectionString: string): Promise<Frame> {
|
||||
return page.mainFrame();
|
||||
}
|
||||
// log in with connection string
|
||||
await page.waitFor("div > p.switchConnectTypeText", { visible: true });
|
||||
await page.click("div > p.switchConnectTypeText");
|
||||
const connStr = connectionString;
|
||||
await page.type("input[class='inputToken']", connStr);
|
||||
await page.click("input[value='Connect']");
|
||||
const handle = await page.waitForSelector("iframe");
|
||||
const frame = await handle.contentFrame();
|
||||
await frame.waitFor("div > p.switchConnectTypeText", { visible: true });
|
||||
await frame.click("div > p.switchConnectTypeText");
|
||||
const connStr = connectionString;
|
||||
await frame.type("input[class='inputToken']", connStr);
|
||||
await frame.click("input[value='Connect']");
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"downlevelIteration": true,
|
||||
"module": "esnext",
|
||||
"target": "es5",
|
||||
"target": "es2017",
|
||||
"lib": ["es5", "es6", "dom", "webworker.importscripts"],
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -78,7 +78,7 @@ const ModulesRule = {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
cacheDirectory: ".cache/babel",
|
||||
presets: [["@babel/preset-env", { targets: { ie: "11" }, useBuiltIns: false }]]
|
||||
presets: [["@babel/preset-env", { targets: { chrome: "80" }, useBuiltIns: false }]]
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -132,8 +132,8 @@ module.exports = function(env = {}, argv = {}) {
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "index.html",
|
||||
template: "src/index.html",
|
||||
chunks: ["index"]
|
||||
template: "src/hostedExplorer.html",
|
||||
chunks: ["hostedExplorer"]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "hostedExplorer.html",
|
||||
|
||||
Reference in New Issue
Block a user