Compare commits

..

28 Commits

Author SHA1 Message Date
Steve Faulkner
35213a77e2 Force token refresh 2021-01-07 14:13:41 -06:00
Steve Faulkner
d1ac8eb077 WIP 2021-01-05 01:14:17 -06:00
Steve Faulkner
d9156c47d0 Fix test login 2021-01-04 10:51:44 -06:00
Steve Faulkner
a844042580 Fix tables 2021-01-04 10:09:24 -06:00
Steve Faulkner
1238d30f95 Fix format 2021-01-03 23:55:58 -06:00
Steve Faulkner
684cbfe4a0 more fixes 2021-01-03 23:54:37 -06:00
Steve Faulkner
5b6b4d3583 More cleanup 2021-01-03 23:41:47 -06:00
Steve Faulkner
e05a78e96a WIP 2021-01-03 23:23:42 -06:00
Steve Faulkner
33f7ae1e6d WIP, checkpoint 2021-01-03 23:07:26 -06:00
Steve Faulkner
2089a8881d Add file 2021-01-03 21:52:20 -06:00
Steve Faulkner
2e1665f093 WIP 2021-01-03 21:49:50 -06:00
Steve Faulkner
f6d6222e5c more WIP 2021-01-02 22:18:20 -06:00
Steve Faulkner
2147b10361 Refactor Components 2021-01-02 20:47:54 -06:00
Steve Faulkner
09ac1d1552 WIP 2021-01-02 20:29:37 -06:00
Steve Faulkner
b81cef2a03 WIP 2021-01-02 19:19:26 -06:00
Steve Faulkner
731999c4e8 WIP 2021-01-02 19:19:19 -06:00
Steve Faulkner
aba583abd8 Checkpoint 2021-01-02 19:00:49 -06:00
Steve Faulkner
2e10b96678 Checkpoint 2021-01-02 17:15:18 -06:00
Steve Faulkner
5652f29d03 WIP 2021-01-01 14:36:29 -06:00
Steve Faulkner
15cb4a8fc4 Checkpoint 2021-01-01 00:09:38 -06:00
Steve Faulkner
bf30c3190a WIP 2020-12-31 15:05:34 -06:00
Steve Faulkner
585f75bc91 Checkpoint 2020-12-30 19:12:22 -06:00
Steve Faulkner
7116f25ce4 checkpoint 2020-12-30 13:18:03 -06:00
Steve Faulkner
5f5d9176af WIP 2020-12-28 20:55:44 -06:00
Steve Faulkner
ac2d645fda Add files from previous commit 2020-12-28 18:48:30 -06:00
Steve Faulkner
12a44fdd42 MSAL 2.0 checkpoint 2020-12-28 18:48:19 -06:00
Steve Faulkner
13dbcb6453 Merge branch 'master' into hosted-msal 2020-12-28 11:40:03 -06:00
Steve Faulkner
cc63cdc1fd Remove dependency on canvas (#354) 2020-12-26 21:56:37 -06:00
56 changed files with 3292 additions and 5185 deletions

View File

@@ -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

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
module.exports = {}

11
canvas/package.json Normal file
View 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"
}

File diff suppressed because it is too large Load Diff

249
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -2,5 +2,6 @@ export enum AuthType {
AAD = "aad",
EncryptedToken = "encryptedtoken",
MasterKey = "masterkey",
ResourceToken = "resourcetoken"
ResourceToken = "resourcetoken",
ConnectionString = "connectionstring"
}

View File

@@ -587,11 +587,3 @@ export interface MemoryUsageInfo {
freeKB: number;
totalKB: number;
}
export interface resourceTokenConnectionStringProperties {
accountEndpoint: string;
collectionId: string;
databaseId: string;
partitionKey?: string;
resourceToken: string;
}

View File

@@ -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();
// });
// });

View File

@@ -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>;
}
}

View File

@@ -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()} />;
}
}

View File

@@ -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"
/>
`;

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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"));

View 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;
}

View File

@@ -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

View File

@@ -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;
}

View 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: ""
}
};

View File

@@ -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[];
}

View File

@@ -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;

View 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"
/>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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" }
}}
/>
);
};

View 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;
}

View File

@@ -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);
});

View File

@@ -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;
}

View File

@@ -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);
});
});

View 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");
}

View File

@@ -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
});
});

View File

@@ -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;
}

View File

@@ -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);
}
}

View 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"
});
});
});

View 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;
}

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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");
});
});

View File

@@ -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;
}

View File

@@ -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
View 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
};
}

View 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 || [];
}

View 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 || [];
}

View 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;
}

View File

@@ -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 };
}

View 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;
}

View 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 || [];
}

View File

@@ -7,5 +7,6 @@
</head>
<body>
<div id="App"></div>
</body>
</html>

View File

@@ -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;
}

View File

@@ -11,7 +11,7 @@
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"module": "esnext",
"target": "es5",
"target": "es2017",
"lib": ["es5", "es6", "dom", "webworker.importscripts"],
"jsx": "react",
"moduleResolution": "node",

View File

@@ -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",