Ying/features aad (#543)
* The start of AAD (merge to feature branch) (#528) * Ying/aad (#2) IPC login/logout and token get Add an auth preference selection view * update requestScopes * add unit tests * hub list and sub list * add filter to sub and hub list * update tests and address comments
This commit is contained in:
Родитель
d392a7af9b
Коммит
1e6e6b14e5
|
@ -3,7 +3,7 @@
|
|||
|
||||
CI Pipeline [![Build Status](https://dev.azure.com/azure/azure-iot-explorer/_apis/build/status/Azure%20IoT%20Explorer%20CI%20Pipeline?branchName=main)](https://dev.azure.com/azure/azure-iot-explorer/_build/latest?definitionId=31&branchName=main)
|
||||
|
||||
Release Pipeline [![Build Status](https://msazure.visualstudio.com/One/_apis/build/status/Custom/Azure_IOT/Portal/Azure.azure-iot-explorer?branchName=features%2FRBAC)](https://msazure.visualstudio.com/One/_build/latest?definitionId=118522&branchName=features%2FRBAC)
|
||||
Release Pipeline [![Build Status](https://dev.azure.com/azureiotdevxp/azure-iot-explorer/_apis/build/status/official/PROD%20-%20Release%20-%20Azure%20IoT%20Explorer?branchName=main)](https://dev.azure.com/azureiotdevxp/azure-iot-explorer/_build/latest?definitionId=95&branchName=main)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
@ -40,7 +40,10 @@ If you'd like to package the app yourself, please refer to the [FAQ](https://git
|
|||
|
||||
### Configure an IoT Hub connection
|
||||
|
||||
- Upon opening the application, add the connection string of your IoT hub. You can add multiple strings, view, update or delete them anytime by returning to Home.
|
||||
- Upon opening the application, choose an authentication method and connect to an Azure IoT hub.
|
||||
- If you picked **Connect via IoT Hub connection string**, you can add multiple strings, view, update or delete them anytime by returning to Home.
|
||||
- If you picked **Connect via Azure Active Directory**, you will be redirected to login in through AAD, from where you will be able to pick a subscription, and then pick an IoT hub.
|
||||
- You can switch back and forth between these two authentication methods.
|
||||
|
||||
<img src="doc/screenRecords/login.gif" alt="login" width="800"/>
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"name": "azure-iot-explorer",
|
||||
"version": "0.14.14",
|
||||
"version": "0.14.15",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "azure-iot-explorer",
|
||||
"version": "0.14.12",
|
||||
"version": "0.14.15",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/event-hubs": "1.0.7",
|
||||
"@azure/msal-node": "1.3.0",
|
||||
"@fluentui/react": "8.20.2",
|
||||
"@microsoft/applicationinsights-web": "2.8.4",
|
||||
"azure-iot-common": "1.12.14",
|
||||
|
@ -295,6 +296,17 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@azure/msal-node": {
|
||||
"version": "1.0.0-beta.6",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz",
|
||||
"integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "^4.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"uuid": "^8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@opentelemetry/api": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz",
|
||||
|
@ -421,14 +433,17 @@
|
|||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@azure/msal-node": {
|
||||
"version": "1.0.0-beta.6",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz",
|
||||
"integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.3.0.tgz",
|
||||
"integrity": "sha512-BM5S5sMB6N0aPux4l85NnRNO/5/G+w3oT+JtLbMDBsc/aUxLVYoWMmxVECrYzlQRm5QZzFWRo04Rv5AnAF7z2g==",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "^4.0.0",
|
||||
"@azure/msal-common": "^4.5.0",
|
||||
"axios": "^0.21.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10 || 12 || 14 || 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-node/node_modules/uuid": {
|
||||
|
@ -2605,17 +2620,6 @@
|
|||
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz",
|
||||
"integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"xmlbuilder": ">=11.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prettier": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
|
||||
|
@ -2792,13 +2796,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/verror": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz",
|
||||
"integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
||||
|
@ -3779,16 +3776,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
|
@ -5389,78 +5376,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
||||
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"slice-ansi": "^3.0.0",
|
||||
"string-width": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/cli-truncate/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
|
@ -6012,16 +5927,6 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/crc": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
|
||||
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||
|
@ -6855,33 +6760,6 @@
|
|||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dmg-license": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
|
||||
"integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
|
||||
"deprecated": "Disk image license agreements are deprecated by Apple and will probably be removed in a future macOS release. Discussion at: https://github.com/argv-minus-one/dmg-license/issues/11",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/plist": "^3.0.1",
|
||||
"@types/verror": "^1.10.3",
|
||||
"ajv": "^6.10.0",
|
||||
"crc": "^3.8.0",
|
||||
"iconv-corefoundation": "^1.1.7",
|
||||
"plist": "^3.0.4",
|
||||
"smart-buffer": "^4.0.2",
|
||||
"verror": "^1.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"dmg-license": "bin/dmg-license.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dns-equal": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
|
||||
|
@ -8631,20 +8509,6 @@
|
|||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
|
@ -9611,30 +9475,6 @@
|
|||
"@babel/runtime": "^7.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-corefoundation": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
|
||||
"integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"dependencies": {
|
||||
"cli-truncate": "^2.1.0",
|
||||
"node-addon-api": "^1.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8.11.2 || >=10"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-corefoundation/node_modules/node-addon-api": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
|
||||
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
|
||||
|
@ -16779,42 +16619,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
||||
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smooth-dnd": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/smooth-dnd/-/smooth-dnd-0.12.0.tgz",
|
||||
|
@ -20573,6 +20377,17 @@
|
|||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@azure/msal-node": {
|
||||
"version": "1.0.0-beta.6",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz",
|
||||
"integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==",
|
||||
"requires": {
|
||||
"@azure/msal-common": "^4.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"uuid": "^8.3.0"
|
||||
}
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz",
|
||||
|
@ -20673,11 +20488,11 @@
|
|||
}
|
||||
},
|
||||
"@azure/msal-node": {
|
||||
"version": "1.0.0-beta.6",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz",
|
||||
"integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.3.0.tgz",
|
||||
"integrity": "sha512-BM5S5sMB6N0aPux4l85NnRNO/5/G+w3oT+JtLbMDBsc/aUxLVYoWMmxVECrYzlQRm5QZzFWRo04Rv5AnAF7z2g==",
|
||||
"requires": {
|
||||
"@azure/msal-common": "^4.0.0",
|
||||
"@azure/msal-common": "^4.5.0",
|
||||
"axios": "^0.21.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"uuid": "^8.3.0"
|
||||
|
@ -22485,17 +22300,6 @@
|
|||
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/plist": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz",
|
||||
"integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"xmlbuilder": ">=11.0.1"
|
||||
}
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
|
||||
|
@ -22678,13 +22482,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/verror": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz",
|
||||
"integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
||||
|
@ -23481,13 +23278,6 @@
|
|||
"integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==",
|
||||
"dev": true
|
||||
},
|
||||
"astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"async": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
|
@ -24753,62 +24543,6 @@
|
|||
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
|
||||
"dev": true
|
||||
},
|
||||
"cli-truncate": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
||||
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"slice-ansi": "^3.0.0",
|
||||
"string-width": "^4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
|
@ -25262,16 +24996,6 @@
|
|||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"crc": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
|
||||
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"create-ecdh": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||
|
@ -25907,23 +25631,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"dmg-license": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
|
||||
"integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/plist": "^3.0.1",
|
||||
"@types/verror": "^1.10.3",
|
||||
"ajv": "^6.10.0",
|
||||
"crc": "^3.8.0",
|
||||
"iconv-corefoundation": "^1.1.7",
|
||||
"plist": "^3.0.4",
|
||||
"smart-buffer": "^4.0.2",
|
||||
"verror": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"dns-equal": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
|
||||
|
@ -27313,13 +27020,6 @@
|
|||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
|
@ -28074,26 +27774,6 @@
|
|||
"@babel/runtime": "^7.12.0"
|
||||
}
|
||||
},
|
||||
"iconv-corefoundation": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
|
||||
"integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"cli-truncate": "^2.1.0",
|
||||
"node-addon-api": "^1.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-addon-api": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
|
||||
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
|
||||
|
@ -33522,34 +33202,6 @@
|
|||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"slice-ansi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
||||
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"smooth-dnd": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/smooth-dnd/-/smooth-dnd-0.12.0.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "azure-iot-explorer",
|
||||
"version": "0.14.14",
|
||||
"version": "0.14.15",
|
||||
"description": "This project welcomes contributions and suggestions. Most contributions require you to agree to a\r Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\r the rights to use your contribution. For details, visit https://cla.microsoft.com.",
|
||||
"main": "host/electron.js",
|
||||
"build": {
|
||||
|
@ -70,6 +70,7 @@
|
|||
"homepage": "https://github.com/Azure/azure-iot-explorer#readme",
|
||||
"dependencies": {
|
||||
"@azure/event-hubs": "1.0.7",
|
||||
"@azure/msal-node": "1.3.0",
|
||||
"@fluentui/react": "8.20.2",
|
||||
"@microsoft/applicationinsights-web": "2.8.4",
|
||||
"azure-iot-common": "1.12.14",
|
||||
|
|
|
@ -7,6 +7,9 @@ export const PLATFORMS = {
|
|||
};
|
||||
|
||||
export const MESSAGE_CHANNELS = {
|
||||
AUTHENTICATION_GET_PROFILE_TOKEN: 'authentication_get_profile_token',
|
||||
AUTHENTICATION_LOGIN: 'authentication_login',
|
||||
AUTHENTICATION_LOGOUT: 'authentication_logout',
|
||||
DEVICE_SEND_MESSAGE: 'device_sendMessage',
|
||||
DIRECTORY_GET_DIRECTORIES: 'directory_getDirectories',
|
||||
EVENTHUB_START_MONITORING: 'eventhub_startMonitoring',
|
||||
|
@ -16,6 +19,7 @@ export const MESSAGE_CHANNELS = {
|
|||
};
|
||||
|
||||
export const API_INTERFACES = {
|
||||
AUTHENTICATION: 'api_authentication',
|
||||
DEVICE: 'api_device',
|
||||
DIRECTORY: 'api_directory',
|
||||
EVENTHUB: 'api_eventhub',
|
||||
|
|
|
@ -8,6 +8,7 @@ import { generateDirectoryInterface } from './factories/directoryInterfaceFactor
|
|||
import { generateModelRepositoryInterface } from './factories/modelRepositoryInterfaceFactory';
|
||||
import { generateDeviceInterface } from './factories/deviceInterfaceFactory';
|
||||
import { generateEventHubInterface } from './factories/eventHubInterfaceFactory';
|
||||
import { generateAuthenticationInterface } from './factories/authenticationInterfaceFactory';
|
||||
import { API_INTERFACES } from './constants';
|
||||
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.DEVICE, generateDeviceInterface());
|
||||
|
@ -15,3 +16,4 @@ contextBridge.exposeInMainWorld(API_INTERFACES.DIRECTORY, generateDirectoryInter
|
|||
contextBridge.exposeInMainWorld(API_INTERFACES.EVENTHUB, generateEventHubInterface());
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.MODEL_DEFINITION, generateModelRepositoryInterface());
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.SETTINGS, generateSettingsInterface());
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.AUTHENTICATION, generateAuthenticationInterface());
|
||||
|
|
|
@ -13,12 +13,14 @@ import { onGetDirectories } from './handlers/directoryHandler';
|
|||
import { onSendMessageToDevice } from './handlers/deviceHandler';
|
||||
import { onStartMonitoring, onStopMonitoring } from './handlers/eventHubHandler';
|
||||
import { formatError } from './utils/errorHelper';
|
||||
import { AuthProvider } from './utils/authProvider';
|
||||
import '../dist/server/serverElectron';
|
||||
|
||||
class Main {
|
||||
private static application: Electron.App;
|
||||
private static mainWindow: BrowserWindow;
|
||||
private static readonly target = path.join(__dirname, '/../dist/index.html');
|
||||
private static authProvider: AuthProvider;
|
||||
|
||||
public static start() {
|
||||
Main.application = app;
|
||||
|
@ -35,6 +37,28 @@ class Main {
|
|||
Main.registerHandler(MESSAGE_CHANNELS.DEVICE_SEND_MESSAGE, onSendMessageToDevice);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.EVENTHUB_START_MONITORING, onStartMonitoring);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.EVENTHUB_STOP_MONITORING, onStopMonitoring);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_LOGIN, Main.onLogin);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_LOGOUT, Main.onLogout);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_GET_PROFILE_TOKEN, Main.onGetProfileToken);
|
||||
}
|
||||
|
||||
private static async loadTarget(redirect?: string): Promise<void> {
|
||||
Main.mainWindow.loadFile(Main.target, { query: {redirect: redirect || ''} });
|
||||
}
|
||||
|
||||
private static async onLogin(): Promise<void> {
|
||||
await Main.authProvider.login(Main.mainWindow)
|
||||
await Main.loadTarget();
|
||||
}
|
||||
|
||||
private static async onLogout(): Promise<void> {
|
||||
await Main.authProvider.logout();
|
||||
await Main.loadTarget();
|
||||
}
|
||||
|
||||
private static async onGetProfileToken(): Promise<string> {
|
||||
const token = await Main.authProvider.getProfileTokenIfPresent();
|
||||
return token;
|
||||
}
|
||||
|
||||
private static setApplicationLock(): void {
|
||||
|
@ -121,6 +145,8 @@ class Main {
|
|||
|
||||
Main.setErrorBoundary();
|
||||
Main.setApplicationLock();
|
||||
|
||||
Main.authProvider = new AuthProvider();
|
||||
Main.setMessageHandlers();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { MESSAGE_CHANNELS } from '../constants';
|
||||
import { AuthenticationInterface } from '../interfaces/authenticationInterface';
|
||||
import { invokeInMainWorld } from '../utils/invokeHelper';
|
||||
|
||||
export const generateAuthenticationInterface = (): AuthenticationInterface => {
|
||||
return {
|
||||
login: async (): Promise<void> => {
|
||||
return invokeInMainWorld<void>(MESSAGE_CHANNELS.AUTHENTICATION_LOGIN);
|
||||
},
|
||||
logout: async (): Promise<void> => {
|
||||
return invokeInMainWorld<void>(MESSAGE_CHANNELS.AUTHENTICATION_LOGOUT);
|
||||
},
|
||||
getProfileToken: async (): Promise<string> => {
|
||||
return invokeInMainWorld<string>(MESSAGE_CHANNELS.AUTHENTICATION_GET_PROFILE_TOKEN);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export interface AuthenticationInterface {
|
||||
login(): Promise<void>;
|
||||
logout(): Promise<void>;
|
||||
getProfileToken(): Promise<string>;
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import {
|
||||
PublicClientApplication,
|
||||
Configuration,
|
||||
LogLevel,
|
||||
AccountInfo,
|
||||
AuthorizationCodeRequest,
|
||||
AuthorizationUrlRequest,
|
||||
AuthenticationResult,
|
||||
SilentFlowRequest } from '@azure/msal-node';
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
const MSAL_CONFIG: Configuration = {
|
||||
auth: {
|
||||
clientId: '67ccd9d7-f5c7-475c-9da0-9700c24b2e66',
|
||||
authority: 'https://login.microsoftonline.com/common',
|
||||
},
|
||||
system: {
|
||||
loggerOptions: {
|
||||
loggerCallback(loglevel, message, containsPii) {
|
||||
console.log(message);
|
||||
},
|
||||
piiLoggingEnabled: false,
|
||||
logLevel: LogLevel.Verbose,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class AuthProvider {
|
||||
private clientApplication: PublicClientApplication;
|
||||
private account: AccountInfo;
|
||||
private authCodeUrlParams: AuthorizationUrlRequest;
|
||||
private authCodeRequest: AuthorizationCodeRequest;
|
||||
private silentProfileRequest: SilentFlowRequest;
|
||||
|
||||
constructor() {
|
||||
this.clientApplication = new PublicClientApplication(MSAL_CONFIG);
|
||||
this.account = null;
|
||||
this.setRequestObjects();
|
||||
}
|
||||
|
||||
public get currentAccount(): AccountInfo | null {
|
||||
return this.account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize request objects used by this AuthModule.
|
||||
*/
|
||||
private setRequestObjects(): void {
|
||||
const requestScopes = ['openid', 'profile', 'https://management.azure.com/user_impersonation'];
|
||||
const redirectUri = 'https://login.microsoftonline.com/oauth2/nativeclient';
|
||||
|
||||
const baseSilentRequest = {
|
||||
account: null,
|
||||
forceRefresh: false
|
||||
};
|
||||
|
||||
this.authCodeUrlParams = {
|
||||
scopes: requestScopes,
|
||||
redirectUri: redirectUri
|
||||
};
|
||||
|
||||
this.authCodeRequest = {
|
||||
scopes: requestScopes,
|
||||
redirectUri: redirectUri,
|
||||
code: null
|
||||
}
|
||||
|
||||
this.silentProfileRequest = {
|
||||
...baseSilentRequest,
|
||||
scopes: [],
|
||||
};
|
||||
}
|
||||
|
||||
async getProfileTokenIfPresent(): Promise<string> {
|
||||
let authResponse: AuthenticationResult;
|
||||
const account = this.account || await this.getAccount();
|
||||
|
||||
if (account) {
|
||||
this.silentProfileRequest.account = account;
|
||||
try {
|
||||
authResponse = await this.clientApplication.acquireTokenSilent(this.silentProfileRequest);
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return authResponse?.accessToken || null;
|
||||
}
|
||||
|
||||
async getToken(authWindow: BrowserWindow, request: SilentFlowRequest): Promise<string> {
|
||||
let authResponse: AuthenticationResult;
|
||||
const account = this.account || await this.getAccount();
|
||||
if (account) {
|
||||
request.account = account;
|
||||
authResponse = await this.getTokenSilent(authWindow, request);
|
||||
} else {
|
||||
authResponse = await this.getTokenInteractive(authWindow, this.authCodeRequest);
|
||||
}
|
||||
|
||||
return authResponse.accessToken || null;
|
||||
}
|
||||
|
||||
async getTokenSilent(authWindow: BrowserWindow, tokenRequest: SilentFlowRequest): Promise<AuthenticationResult> {
|
||||
try {
|
||||
return await this.clientApplication.acquireTokenSilent(tokenRequest);
|
||||
} catch (error) {
|
||||
console.log('Silent token acquisition failed, acquiring token using redirect');
|
||||
return await this.getTokenInteractive(authWindow, this.authCodeRequest);
|
||||
}
|
||||
}
|
||||
|
||||
async getTokenInteractive(authWindow: BrowserWindow, tokenRequest: AuthorizationUrlRequest ): Promise<AuthenticationResult> {
|
||||
const authCodeUrlParams = { ...this.authCodeUrlParams, scopes: tokenRequest.scopes };
|
||||
const authCodeUrl = await this.clientApplication.getAuthCodeUrl(authCodeUrlParams);
|
||||
const authCode = await this.listenForAuthCode(authCodeUrl, authWindow);
|
||||
const authResult = await this.clientApplication.acquireTokenByCode({ ...this.authCodeRequest, scopes: tokenRequest.scopes, code: authCode});
|
||||
return authResult;
|
||||
}
|
||||
|
||||
async login(authWindow: BrowserWindow): Promise<void> {
|
||||
const authResult = await this.getTokenInteractive(authWindow, this.authCodeUrlParams);
|
||||
return this.handleResponse(authResult);
|
||||
}
|
||||
|
||||
async loginSilent(): Promise<AccountInfo> {
|
||||
if (!this.account) {
|
||||
this.account = await this.getAccount();
|
||||
}
|
||||
|
||||
return this.account;
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
if (this.account) {
|
||||
await this.clientApplication.getTokenCache().removeAccount(this.account);
|
||||
this.account = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async listenForAuthCode(navigateUrl: string, authWindow: BrowserWindow): Promise<string> {
|
||||
authWindow.loadURL(navigateUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
authWindow.webContents.on('will-redirect', (event, responseUrl) => {
|
||||
try {
|
||||
const parsedUrl = new URL(responseUrl);
|
||||
const authCode = parsedUrl.searchParams.get('code');
|
||||
if(authCode) {
|
||||
resolve(authCode);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the response from a popup or redirect. If response is null, will check if we have any accounts and attempt to sign in.
|
||||
* @param response
|
||||
*/
|
||||
private async handleResponse(response: AuthenticationResult) {
|
||||
if (response !== null) {
|
||||
this.account = response.account;
|
||||
} else {
|
||||
this.account = await this.getAccount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls getAllAccounts and determines the correct account to sign into, currently defaults to first account found in cache.
|
||||
* TODO: Add account chooser code
|
||||
*
|
||||
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
|
||||
*/
|
||||
private async getAccount(): Promise<AccountInfo> {
|
||||
// need to call getAccount here?
|
||||
const cache = this.clientApplication.getTokenCache();
|
||||
const currentAccounts = await cache.getAllAccounts();
|
||||
|
||||
if (currentAccounts === null) {
|
||||
console.log('No accounts detected');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentAccounts.length > 1) {
|
||||
// Add choose account code here
|
||||
console.log('Multiple accounts detected, need to add choose account code.');
|
||||
return currentAccounts[0];
|
||||
} else if (currentAccounts.length === 1) {
|
||||
return currentAccounts[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
|
||||
export interface AzureResourceIdentifier {
|
||||
id: string;
|
||||
location: string;
|
|
@ -2,8 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
|
||||
export interface AzureResourceManagementEndpoint {
|
||||
authorizationToken: string;
|
||||
endpoint: string;
|
||||
}
|
||||
export interface AzureResourceManagementEndpoint {
|
||||
authorizationToken: string;
|
||||
endpoint: string;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export interface AzureSubscription {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
subscriptionId: string;
|
||||
displayName: string;
|
||||
state: SubscriptionState;
|
||||
}
|
||||
|
||||
export enum SubscriptionState {
|
||||
Deleted = 'Deleted',
|
||||
Disabled = 'Disabled',
|
||||
Enabled = 'Enabled',
|
||||
PastDue = 'PastDue',
|
||||
Warned = 'Warned'
|
||||
}
|
|
@ -2,8 +2,8 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export interface AzureSubscription {
|
||||
export interface IotHubDescription {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
subscriptionId: string;
|
||||
name: string;
|
||||
location: string;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export interface SharedAccessSignatureAuthorizationRule {
|
||||
keyName: string;
|
||||
primaryKey: string;
|
||||
rights: string;
|
||||
}
|
||||
|
||||
export enum AccessRights {
|
||||
RegistryWrite = 'RegistryWrite',
|
||||
ServiceConnect = 'ServiceConnect',
|
||||
DeviceConnect = 'DeviceConnect'
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { getAuthenticationInterface } from '../shared/interfaceUtils';
|
||||
|
||||
export const login = async (): Promise<void> => {
|
||||
const api = getAuthenticationInterface();
|
||||
return api.login();
|
||||
};
|
||||
|
||||
export const logout = async (): Promise<void> => {
|
||||
const api = getAuthenticationInterface();
|
||||
return api.logout();
|
||||
};
|
||||
|
||||
export const getProfileToken = async (): Promise<string> => {
|
||||
const api = getAuthenticationInterface();
|
||||
return api.getProfileToken();
|
||||
};
|
|
@ -2,15 +2,15 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { ContinuingResultSet } from '../../api/models/continuingResultSet';
|
||||
import { ContinuingResultSet } from '../models/continuingResultSet';
|
||||
import { AzureResourceIdentifier } from '../models/azureResourceIdentifier';
|
||||
import { AzureResourceIdentifierType } from '../models/azureResourceIdentifierType';
|
||||
import { AzureResourceIdentifierQuery } from '../models/azureResourceIdentifierQuery';
|
||||
import { AzureResourceIdentifierQueryResult } from '../models/azureResourceIdentifierQueryResult';
|
||||
import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants';
|
||||
import { AzureResourceManagementEndpoint } from '../models/azureResourceManagementEndpoint';
|
||||
import { HttpError } from '../../api/models/httpError';
|
||||
import { mapPropertyArrayToObject } from '../../api/shared/mapUtils';
|
||||
import { HttpError } from '../models/httpError';
|
||||
import { mapPropertyArrayToObject } from '../shared/mapUtils';
|
||||
|
||||
const azureResourceManagementAPIVersion = '2019-04-01';
|
||||
const azureResourceManagementQueryFields = [
|
|
@ -1,4 +1,3 @@
|
|||
import { HTTP_OPERATION_TYPES } from './../../constants/apiConstants';
|
||||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
|
@ -6,6 +5,7 @@ import { HTTP_OPERATION_TYPES } from './../../constants/apiConstants';
|
|||
import { getAzureSubscriptions } from './azureSubscriptionService';
|
||||
import { HttpError } from '../../api/models/httpError';
|
||||
import { APPLICATION_JSON } from '../../constants/apiConstants';
|
||||
import { HTTP_OPERATION_TYPES } from './../../constants/apiConstants';
|
||||
|
||||
describe('getAzureSubscriptions', () => {
|
||||
it('calls fetch with expected parameters', () => {
|
||||
|
@ -17,10 +17,8 @@ describe('getAzureSubscriptions', () => {
|
|||
|
||||
} as any); // tslint:disable-line:no-any
|
||||
getAzureSubscriptions({
|
||||
azureResourceManagementEndpoint: {
|
||||
authorizationToken: 'token',
|
||||
endpoint: 'managementEndpoint'
|
||||
},
|
||||
authorizationToken: 'token',
|
||||
endpoint: 'managementEndpoint'
|
||||
});
|
||||
|
||||
const resourceUrl = `https://managementEndpoint/subscriptions?api-version=2019-06-01`;
|
||||
|
@ -47,11 +45,9 @@ describe('getAzureSubscriptions', () => {
|
|||
} as any); // tslint:disable-line:no-any
|
||||
|
||||
await expect(getAzureSubscriptions({
|
||||
azureResourceManagementEndpoint: {
|
||||
authorizationToken: 'token',
|
||||
endpoint: 'managementEndpoint'
|
||||
},
|
||||
})).rejects.toThrow(httpError);
|
||||
authorizationToken: 'token',
|
||||
endpoint: 'managementEndpoint'
|
||||
})).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('returns expected data', async () => {
|
||||
|
@ -75,10 +71,8 @@ describe('getAzureSubscriptions', () => {
|
|||
} as any); // tslint:disable-line:no-any
|
||||
|
||||
const result = await getAzureSubscriptions({
|
||||
azureResourceManagementEndpoint: {
|
||||
authorizationToken: 'token',
|
||||
endpoint: 'managementEndpoint'
|
||||
},
|
||||
authorizationToken: 'token',
|
||||
endpoint: 'managementEndpoint'
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2); // tslint:disable-line:no-magic-numbers
|
|
@ -4,17 +4,14 @@
|
|||
**********************************************************/
|
||||
import { AzureResourceManagementEndpoint } from '../models/azureResourceManagementEndpoint';
|
||||
import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants';
|
||||
import { HttpError } from '../../api/models/httpError';
|
||||
import { AzureSubscription } from '../models/azureSubscription';
|
||||
import { throwHttpErrorWhenResponseNotOk } from '../shared/fetchUtils';
|
||||
|
||||
const azureSubscriptionAPIVersion = '2019-06-01';
|
||||
export interface GetSubscriptionsParameters {
|
||||
azureResourceManagementEndpoint: AzureResourceManagementEndpoint;
|
||||
}
|
||||
export type GetSubscriptionsParameters = AzureResourceManagementEndpoint;
|
||||
|
||||
export const getAzureSubscriptions = async (parameters: GetSubscriptionsParameters): Promise<AzureSubscription[]> => {
|
||||
const { azureResourceManagementEndpoint } = parameters;
|
||||
const { authorizationToken, endpoint } = azureResourceManagementEndpoint;
|
||||
const { authorizationToken, endpoint } = parameters;
|
||||
|
||||
const resourceUrl = `https://${endpoint}/subscriptions?api-version=${azureSubscriptionAPIVersion}`;
|
||||
const serviceRequestParams: RequestInit = {
|
||||
|
@ -26,9 +23,7 @@ export const getAzureSubscriptions = async (parameters: GetSubscriptionsParamete
|
|||
method: HTTP_OPERATION_TYPES.Get
|
||||
};
|
||||
const response = await fetch(resourceUrl, serviceRequestParams);
|
||||
if (!response.ok) {
|
||||
throw new HttpError(response.status);
|
||||
}
|
||||
await throwHttpErrorWhenResponseNotOk(response);
|
||||
|
||||
const responseBody = await response.json() as { value: AzureSubscription[] };
|
||||
return responseBody.value;
|
|
@ -5,8 +5,9 @@
|
|||
import { CONTROLLER_API_ENDPOINT, DATAPLANE, DataPlaneStatusCode, HTTP_OPERATION_TYPES } from '../../constants/apiConstants';
|
||||
import { getConnectionInfoFromConnectionString, generateSasToken } from '../shared/utils';
|
||||
import { PortIsInUseError } from '../models/portIsInUseError';
|
||||
import { CONNECTION_STRING_NAME_LIST } from '../../constants/browserStorage';
|
||||
import { AUTHENTICATION_METHOD_PREFERENCE, CONNECTION_STRING_NAME_LIST, CONNECTION_STRING_THROUGH_AAD } from '../../constants/browserStorage';
|
||||
import { getActiveConnectionString } from '../../shared/utils/hubConnectionStringHelper';
|
||||
import { AuthenticationMethodPreference } from '../../authentication/state';
|
||||
|
||||
export const DATAPLANE_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${DATAPLANE}`;
|
||||
|
||||
|
@ -38,9 +39,18 @@ export const request = async (endpoint: string, parameters: any) => { // tslint:
|
|||
);
|
||||
};
|
||||
|
||||
export const getConnectionStringHelper = async () => {
|
||||
const authSelection = await localStorage.getItem(AUTHENTICATION_METHOD_PREFERENCE);
|
||||
if (authSelection === AuthenticationMethodPreference.ConnectionString) {
|
||||
return getActiveConnectionString(await localStorage.getItem(CONNECTION_STRING_NAME_LIST));
|
||||
}
|
||||
else {
|
||||
return localStorage.getItem(CONNECTION_STRING_THROUGH_AAD);
|
||||
}
|
||||
};
|
||||
|
||||
export const dataPlaneConnectionHelper = async () => {
|
||||
const connectionStrings = await localStorage.getItem(CONNECTION_STRING_NAME_LIST);
|
||||
const connectionString = getActiveConnectionString(connectionStrings);
|
||||
const connectionString = await getConnectionStringHelper();
|
||||
const connectionInfo = getConnectionInfoFromConnectionString(connectionString);
|
||||
if (!(connectionInfo && connectionInfo.hostName)) {
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { AzureResourceManagementEndpoint } from '../models/azureResourceManagementEndpoint';
|
||||
import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants';
|
||||
import { IotHubDescription } from '../models/iotHubDescription';
|
||||
import { throwHttpErrorWhenResponseNotOk } from '../shared/fetchUtils';
|
||||
import { SharedAccessSignatureAuthorizationRule } from '../models/sharedAccessSignatureAuthorizationRule';
|
||||
|
||||
const apiVersion = '2018-04-01';
|
||||
export interface GetIotHubsBySubscriptionParameters extends AzureResourceManagementEndpoint {
|
||||
subscriptionId: string;
|
||||
}
|
||||
export const getIotHubsBySubscription = async (parameters: GetIotHubsBySubscriptionParameters): Promise<IotHubDescription[]> => {
|
||||
const { authorizationToken, endpoint, subscriptionId } = parameters;
|
||||
|
||||
const resourceUrl = `https://${endpoint}/subscriptions/${subscriptionId}/providers/Microsoft.Devices/IotHubs?api-version=${apiVersion}`;
|
||||
const serviceRequestParams: RequestInit = {
|
||||
headers: new Headers({
|
||||
'Accept': APPLICATION_JSON,
|
||||
'Authorization': `Bearer ${authorizationToken}`,
|
||||
'Content-Type': APPLICATION_JSON
|
||||
}),
|
||||
method: HTTP_OPERATION_TYPES.Get
|
||||
};
|
||||
const response = await fetch(resourceUrl, serviceRequestParams);
|
||||
await throwHttpErrorWhenResponseNotOk(response);
|
||||
|
||||
const responseBody = await response.json() as { value: IotHubDescription[] };
|
||||
return responseBody.value;
|
||||
};
|
||||
|
||||
export interface GetIotHubKeysParameters extends AzureResourceManagementEndpoint {
|
||||
hubId: string;
|
||||
}
|
||||
export const getIotHubKeys = async (parameters: GetIotHubKeysParameters): Promise<SharedAccessSignatureAuthorizationRule[]> => {
|
||||
const { authorizationToken, endpoint, hubId } = parameters;
|
||||
|
||||
const resourceUrl = `https://${endpoint}/${hubId}/listkeys?api-version=${apiVersion}`;
|
||||
const serviceRequestParams: RequestInit = {
|
||||
headers: new Headers({
|
||||
'Accept': APPLICATION_JSON,
|
||||
'Authorization': `Bearer ${authorizationToken}`,
|
||||
'Content-Type': APPLICATION_JSON
|
||||
}),
|
||||
method: HTTP_OPERATION_TYPES.Post
|
||||
};
|
||||
const response = await fetch(resourceUrl, serviceRequestParams);
|
||||
await throwHttpErrorWhenResponseNotOk(response);
|
||||
|
||||
const responseBody = await response.json() as { value: SharedAccessSignatureAuthorizationRule[] };
|
||||
return responseBody.value;
|
||||
};
|
|
@ -9,3 +9,22 @@ export const getHeaderValue = (response: { headers: Headers }, headerName: strin
|
|||
|
||||
return response.headers.has(headerName) ? response.headers.get(headerName) : undefinedValue;
|
||||
};
|
||||
|
||||
export const throwHttpErrorWhenResponseNotOk = async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const error = await getHttpErrorFromResponse(response);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getHttpErrorFromResponse = async (response: Response): Promise<Error> => {
|
||||
let message: string | object = await response.text();
|
||||
|
||||
try {
|
||||
message = JSON.stringify(JSON.parse(message), null, 2); // tslint:disable-line:no-magic-numbers
|
||||
} catch {
|
||||
// intentionally blank
|
||||
}
|
||||
|
||||
return new Error(message);
|
||||
};
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { getResourceNameFromHostName, getResourceTypeFromHostName, tryGetHostNameFromConnectionString } from './hostNameUtils';
|
||||
import { AzureResourceIdentifierType } from '../../azureResourceIdentifier/models/azureResourceIdentifierType';
|
||||
import { getResourceNameFromHostName, getResourceTypeFromHostName, tryGetHostNameFromConnectionString } from './hostNameUtils';
|
||||
import { AzureResourceIdentifierType } from '../models/azureResourceIdentifierType';
|
||||
import * as utils from './utils';
|
||||
|
||||
describe('getResourceNameFromHostName', () => {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { AzureResourceIdentifierType } from '../../azureResourceIdentifier/models/azureResourceIdentifierType';
|
||||
import { AzureResourceHostNameType } from '../../azureResourceIdentifier/models/azureResourceHostNameType';
|
||||
import { AzureResourceIdentifierType } from '../models/azureResourceIdentifierType';
|
||||
import { AzureResourceHostNameType } from '../models/azureResourceHostNameType';
|
||||
import { getConnectionInfoFromConnectionString } from './utils';
|
||||
|
||||
export const getResourceNameFromHostName = (hostName: string): string | undefined => {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { DeviceInterface } from '../../../../public/interfaces/deviceInterface';
|
|||
import { DirectoryInterface } from '../../../../public/interfaces/directoryInterface';
|
||||
import { ModelRepositoryInterface } from '../../../../public/interfaces/modelRepositoryInterface';
|
||||
import { EventHubInterface } from './../../../../public/interfaces/eventHubInterface';
|
||||
import { AuthenticationInterface } from './../../../../public/interfaces/authenticationInterface';
|
||||
import { API_INTERFACES } from '../../../../public/constants';
|
||||
import { appConfig, HostMode } from '../../../appConfig/appConfig';
|
||||
import { HIGH_CONTRAST } from '../../constants/browserStorage';
|
||||
|
@ -67,6 +68,14 @@ export const getPublicDigitalTwinsModelInterface = (): PublicDigitalTwinsModelIn
|
|||
return new PublicDigitalTwinsModelRepoHelper();
|
||||
};
|
||||
|
||||
export const getAuthenticationInterface = (): AuthenticationInterface => {
|
||||
if (appConfig.hostMode !== HostMode.Electron) {
|
||||
throw new Error(NOT_AVAILABLE);
|
||||
}
|
||||
|
||||
return getElectronInterface(API_INTERFACES.AUTHENTICATION);
|
||||
};
|
||||
|
||||
export const getElectronInterface = <T>(name: string): T => {
|
||||
// tslint:disable-next-line: no-any no-string-literal
|
||||
const api = (window as any)[name];
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { setLoginPreferenceAction, getLoginPreferenceAction } from './actions';
|
||||
|
||||
describe('actions', () => {
|
||||
context('setLoginPreferenceAction', () => {
|
||||
|
||||
it('returns AUTHENTICATION/SET_STARTED action object', () => {
|
||||
expect(setLoginPreferenceAction.started('').type).toEqual('AUTHENTICATION/SET_STARTED');
|
||||
});
|
||||
|
||||
it('returns AUTHENTICATION/SET_DONE action object', () => {
|
||||
expect(setLoginPreferenceAction.done({params: ''}).type).toEqual('AUTHENTICATION/SET_DONE');
|
||||
});
|
||||
|
||||
it('returns AUTHENTICATION/SET_FAILED action object', () => {
|
||||
expect(setLoginPreferenceAction.failed({params: '', error: {}}).type).toEqual('AUTHENTICATION/SET_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
context('getLoginPreferenceAction', () => {
|
||||
|
||||
it('returns AUTHENTICATION/GET_STARTED action object', () => {
|
||||
expect(getLoginPreferenceAction.started().type).toEqual('AUTHENTICATION/GET_STARTED');
|
||||
});
|
||||
|
||||
it('returns AUTHENTICATION/GET_DONE action object', () => {
|
||||
expect(getLoginPreferenceAction.done({result: ''}).type).toEqual('AUTHENTICATION/GET_DONE');
|
||||
});
|
||||
|
||||
it('returns AUTHENTICATION/GET_FAILED action object', () => {
|
||||
expect(getLoginPreferenceAction.failed({error: {}}).type).toEqual('AUTHENTICATION/GET_FAILED');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import { GET, SET } from '../constants/actionTypes';
|
||||
|
||||
const actionCreator = actionCreatorFactory('AUTHENTICATION');
|
||||
export const setLoginPreferenceAction = actionCreator.async<string, void>(SET);
|
||||
export const getLoginPreferenceAction = actionCreator.async<void, string>(GET);
|
|
@ -0,0 +1,99 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { getUserProfileTokenAction, getSubscriptionListAction, getIotHubsBySubscriptionAction, loginAction, logoutAction, getIoTHubKeyAction } from './actions';
|
||||
|
||||
describe('actions', () => {
|
||||
|
||||
context('getUserProfileTokenAction', () => {
|
||||
|
||||
it('returns AAD/GET_TOKEN_STARTED action object', () => {
|
||||
expect(getUserProfileTokenAction.started().type).toEqual('AAD/GET_TOKEN_STARTED');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_TOKEN_DONE action object', () => {
|
||||
expect(getUserProfileTokenAction.done({result: 'token'}).type).toEqual('AAD/GET_TOKEN_DONE');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_TOKEN_FAILED action object', () => {
|
||||
expect(getUserProfileTokenAction.failed({error: {}}).type).toEqual('AAD/GET_TOKEN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
context('getSubscriptionListAction', () => {
|
||||
|
||||
it('returns AAD/GET_SUBSCRIPTIONS_STARTED action object', () => {
|
||||
expect(getSubscriptionListAction.started().type).toEqual('AAD/GET_SUBSCRIPTIONS_STARTED');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_SUBSCRIPTIONS_DONE action object', () => {
|
||||
expect(getSubscriptionListAction.done({result: []}).type).toEqual('AAD/GET_SUBSCRIPTIONS_DONE');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_SUBSCRIPTIONS_FAILED action object', () => {
|
||||
expect(getSubscriptionListAction.failed({error: {}}).type).toEqual('AAD/GET_SUBSCRIPTIONS_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
context('getIotHubsBySubscriptionAction', () => {
|
||||
|
||||
it('returns AAD/GET_IOTHUBS_STARTED action object', () => {
|
||||
expect(getIotHubsBySubscriptionAction.started('subscriptionId').type).toEqual('AAD/GET_IOTHUBS_STARTED');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_IOTHUBS_DONE action object', () => {
|
||||
expect(getIotHubsBySubscriptionAction.done({params: 'subscriptionId', result: []}).type).toEqual('AAD/GET_IOTHUBS_DONE');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_IOTHUBS_FAILED action object', () => {
|
||||
expect(getIotHubsBySubscriptionAction.failed({params: 'subscriptionId', error: {}}).type).toEqual('AAD/GET_IOTHUBS_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
context('getIoTHubKeyAction', () => {
|
||||
|
||||
const params = {hubId: 'hubid', hubName: 'test'};
|
||||
it('returns AAD/GET_HUBKEYSTARTED action object', () => {
|
||||
expect(getIoTHubKeyAction.started(params).type).toEqual('AAD/GET_HUBKEY_STARTED');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_HUBKEY_DONE action object', () => {
|
||||
expect(getIoTHubKeyAction.done({params, result: 'key'}).type).toEqual('AAD/GET_HUBKEY_DONE');
|
||||
});
|
||||
|
||||
it('returns AAD/GET_HUBKEY_FAILED action object', () => {
|
||||
expect(getIoTHubKeyAction.failed({params, error: {}}).type).toEqual('AAD/GET_HUBKEY_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
context('loginAction', () => {
|
||||
|
||||
it('returns AAD/LOGIN_STARTED action object', () => {
|
||||
expect(loginAction.started().type).toEqual('AAD/LOGIN_STARTED');
|
||||
});
|
||||
|
||||
it('returns AAD/LOGIN_DONE action object', () => {
|
||||
expect(loginAction.done({}).type).toEqual('AAD/LOGIN_DONE');
|
||||
});
|
||||
|
||||
it('returns AAD/LOGIN_FAILED action object', () => {
|
||||
expect(loginAction.failed({error: {}}).type).toEqual('AAD/LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
context('logoutAction', () => {
|
||||
|
||||
it('returns AAD/LOGOUT_STARTED action object', () => {
|
||||
expect(logoutAction.started().type).toEqual('AAD/LOGOUT_STARTED');
|
||||
});
|
||||
|
||||
it('returns AAD/LOGOUT_DONE action object', () => {
|
||||
expect(logoutAction.done({}).type).toEqual('AAD/LOGOUT_DONE');
|
||||
});
|
||||
|
||||
it('returns AAD/LOGOUT_FAILED action object', () => {
|
||||
expect(logoutAction.failed({error: {}}).type).toEqual('AAD/LOGOUT_FAILED');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import { IotHubDescription } from '../../api/models/iotHubDescription';
|
||||
import { AzureSubscription } from '../../api/models/azureSubscription';
|
||||
|
||||
export interface GetIotHubKeyActionParmas {
|
||||
hubId: string;
|
||||
hubName: string;
|
||||
}
|
||||
|
||||
const actionCreator = actionCreatorFactory('AAD');
|
||||
export const getIotHubsBySubscriptionAction = actionCreator.async<string, IotHubDescription[]>('GET_IOTHUBS');
|
||||
export const getIoTHubKeyAction = actionCreator.async<GetIotHubKeyActionParmas, string>('GET_HUBKEY');
|
||||
export const getSubscriptionListAction = actionCreator.async<void, AzureSubscription[]>('GET_SUBSCRIPTIONS');
|
||||
export const getUserProfileTokenAction = actionCreator.async<void, string>('GET_TOKEN');
|
||||
export const loginAction = actionCreator.async<void, void>('LOGIN');
|
||||
export const logoutAction = actionCreator.async<void, void>('LOGOUT');
|
|
@ -0,0 +1,14 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BackButton matches snapshot 1`] = `
|
||||
<CustomizedActionButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "back",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
authentication.azureActiveDirectory.hubList.backButton
|
||||
</CustomizedActionButton>
|
||||
`;
|
|
@ -0,0 +1,55 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AzureActiveDirectoryCommandBar matches snapshot when token is not present 1`] = `
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "authentication.azureActiveDirectory.command.login",
|
||||
"iconProps": Object {
|
||||
"iconName": "Signin",
|
||||
},
|
||||
"key": "signin",
|
||||
"onClick": [Function],
|
||||
"text": "authentication.azureActiveDirectory.command.login",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "authentication.autheSelection.switchAuthType",
|
||||
"iconProps": Object {
|
||||
"iconName": "NavigateBack",
|
||||
},
|
||||
"key": "switch",
|
||||
"onClick": [Function],
|
||||
"text": "authentication.autheSelection.switchAuthType",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`AzureActiveDirectoryCommandBar matches snapshot when token is present 1`] = `
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "authentication.azureActiveDirectory.command.logout",
|
||||
"iconProps": Object {
|
||||
"iconName": "Signout",
|
||||
},
|
||||
"key": "signout",
|
||||
"onClick": [Function],
|
||||
"text": "authentication.azureActiveDirectory.command.logout",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "authentication.autheSelection.switchAuthType",
|
||||
"iconProps": Object {
|
||||
"iconName": "NavigateBack",
|
||||
},
|
||||
"key": "switch",
|
||||
"onClick": [Function],
|
||||
"text": "authentication.autheSelection.switchAuthType",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterTextBox matches snapshot 1`] = `
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="authentication.azureActiveDirectory.filter.placeHolder"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Filter",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
placeholder="authentication.azureActiveDirectory.filter.placeHolder"
|
||||
role="searchbox"
|
||||
value=""
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,68 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HubList matches snapshot when there are list items 1`] = `
|
||||
<Fragment>
|
||||
<FilterTextBox
|
||||
filterType={1}
|
||||
setFilteredList={[Function]}
|
||||
/>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "name",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.hubList.columns.name",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "id",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.hubList.columns.location",
|
||||
"onRender": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
selectionMode={0}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`HubList matches snapshot when there are no list items 1`] = `
|
||||
<Fragment>
|
||||
<FilterTextBox
|
||||
filterType={1}
|
||||
setFilteredList={[Function]}
|
||||
/>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "name",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.hubList.columns.name",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "id",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.hubList.columns.location",
|
||||
"onRender": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
selectionMode={0}
|
||||
/>
|
||||
authentication.azureActiveDirectory.hubList.noItemText
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,35 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HubSelection matches snapshot when hub list is shown 1`] = `
|
||||
<Fragment>
|
||||
<AzureActiveDirectoryCommandBar />
|
||||
<div
|
||||
className="hub-selection-list"
|
||||
>
|
||||
<BackButton
|
||||
backToSubscription={[Function]}
|
||||
/>
|
||||
<HubList />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`HubSelection matches snapshot when page is loading 1`] = `
|
||||
<Fragment>
|
||||
<AzureActiveDirectoryCommandBar />
|
||||
<MultiLineShimmer />
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`HubSelection matches snapshot when token is present 1`] = `
|
||||
<Fragment>
|
||||
<AzureActiveDirectoryCommandBar />
|
||||
<div
|
||||
className="hub-selection-list"
|
||||
>
|
||||
<SubscriptionList
|
||||
renderHubList={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,84 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SubscriptionList matches snapshot when there are list items 1`] = `
|
||||
<Fragment>
|
||||
<FilterTextBox
|
||||
filterType={0}
|
||||
setFilteredList={[Function]}
|
||||
/>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "name",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.subscriptionList.columns.name",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "id",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.subscriptionList.columns.id",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "state",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.subscriptionList.columns.state",
|
||||
"onRender": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
selectionMode={0}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`SubscriptionList matches snapshot when there are no list items 1`] = `
|
||||
<Fragment>
|
||||
<FilterTextBox
|
||||
filterType={0}
|
||||
setFilteredList={[Function]}
|
||||
/>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "name",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.subscriptionList.columns.name",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "id",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.subscriptionList.columns.id",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"isResizable": true,
|
||||
"key": "state",
|
||||
"maxWidth": 400,
|
||||
"minWidth": 150,
|
||||
"name": "authentication.azureActiveDirectory.subscriptionList.columns.state",
|
||||
"onRender": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
selectionMode={0}
|
||||
/>
|
||||
authentication.azureActiveDirectory.subscriptionList.noItemText
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,18 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BackButton } from './backButton';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import * as azureActiveDirectoryStateContext from '../context/azureActiveDirectoryStateContext';
|
||||
|
||||
describe('BackButton', () => {
|
||||
it('matches snapshot ', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[getInitialAzureActiveDirectoryState(), azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<BackButton backToSubscription={jest.fn()}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionButton } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
|
||||
export interface BackButtonProps {
|
||||
backToSubscription: () => void;
|
||||
}
|
||||
|
||||
export const BackButton: React.FC<BackButtonProps> = props => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ActionButton iconProps={{ iconName: 'back' }} onClick={props.backToSubscription}>
|
||||
{t(ResourceKeys.authentication.azureActiveDirectory.hubList.backButton)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { CommandBar } from '@fluentui/react';
|
||||
import { AzureActiveDirectoryCommandBar } from './commandBar';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import * as azureActiveDirectoryStateContext from '../context/azureActiveDirectoryStateContext';
|
||||
import * as authenticationStateContext from '../../../authentication/context/authenticationStateContext';
|
||||
import { getInitialAuthenticationState } from '../../../authentication/state';
|
||||
|
||||
describe('AzureActiveDirectoryCommandBar', () => {
|
||||
it('matches snapshot when token is not present', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[getInitialAzureActiveDirectoryState(), azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<AzureActiveDirectoryCommandBar/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when token is present', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), token: 'token'}, azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<AzureActiveDirectoryCommandBar/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
it('calls api respectively', () => {
|
||||
const logout = jest.fn();
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), token: 'token'}, {...azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps(), logout}]);
|
||||
|
||||
const setLoginPreference = jest.fn();
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[getInitialAuthenticationState(), {...authenticationStateContext.getInitialAuthenticationOps(), setLoginPreference}]);
|
||||
|
||||
const wrapper = shallow(<AzureActiveDirectoryCommandBar/>);
|
||||
|
||||
wrapper.find(CommandBar).props().items[0].onClick(undefined);
|
||||
wrapper.update();
|
||||
expect(logout).toHaveBeenCalled();
|
||||
|
||||
wrapper.find(CommandBar).props().items[1].onClick(undefined);
|
||||
wrapper.update();
|
||||
|
||||
expect(setLoginPreference).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CommandBar } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { useAuthenticationStateContext } from '../../context/authenticationStateContext';
|
||||
import { NAVIGATE_BACK } from '../../../constants/iconNames';
|
||||
import { useAzureActiveDirectoryStateContext } from '../context/azureActiveDirectoryStateContext';
|
||||
|
||||
export const AzureActiveDirectoryCommandBar: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [ , { setLoginPreference } ] = useAuthenticationStateContext();
|
||||
const [{ token }, { logout, login }] = useAzureActiveDirectoryStateContext();
|
||||
|
||||
const switchAuth = () => {
|
||||
setLoginPreference('');
|
||||
};
|
||||
|
||||
const getCommandBarItems = () => {
|
||||
const items = [{
|
||||
ariaLabel: t(ResourceKeys.authentication.autheSelection.switchAuthType),
|
||||
iconProps: { iconName: NAVIGATE_BACK },
|
||||
key: 'switch',
|
||||
onClick: switchAuth,
|
||||
text: t(ResourceKeys.authentication.autheSelection.switchAuthType)
|
||||
}];
|
||||
|
||||
return token ? [{
|
||||
ariaLabel: t(ResourceKeys.authentication.azureActiveDirectory.command.logout),
|
||||
iconProps: { iconName: 'Signout' },
|
||||
key: 'signout',
|
||||
onClick: logout,
|
||||
text: t(ResourceKeys.authentication.azureActiveDirectory.command.logout)
|
||||
}, ...items] :
|
||||
[{
|
||||
ariaLabel: t(ResourceKeys.authentication.azureActiveDirectory.command.login),
|
||||
iconProps: { iconName: 'Signin' },
|
||||
key: 'signin',
|
||||
onClick: login,
|
||||
text: t(ResourceKeys.authentication.azureActiveDirectory.command.login)
|
||||
}, ...items];
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandBar
|
||||
items={getCommandBarItems()}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FilterTextBox, FilterType } from './filterTextBox';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import * as azureActiveDirectoryStateContext from '../context/azureActiveDirectoryStateContext';
|
||||
|
||||
describe('FilterTextBox', () => {
|
||||
it('matches snapshot', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[getInitialAzureActiveDirectoryState(), azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<FilterTextBox filterType={FilterType.IoTHub} setFilteredList={jest.fn()}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextField } from '@fluentui/react';
|
||||
import { useAzureActiveDirectoryStateContext } from '../context/azureActiveDirectoryStateContext';
|
||||
import { AzureSubscription } from '../../../api/models/azureSubscription';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { IotHubDescription } from '../../../api/models/iotHubDescription';
|
||||
|
||||
export enum FilterType {
|
||||
Subscription,
|
||||
IoTHub
|
||||
}
|
||||
|
||||
export interface FilterTextBoxPros {
|
||||
filterType: FilterType;
|
||||
setFilteredList: (listValue: AzureSubscription[] | IotHubDescription[]) => void;
|
||||
}
|
||||
|
||||
export const FilterTextBox: React.FC<FilterTextBoxPros> = props => {
|
||||
const { t } = useTranslation();
|
||||
const [{ subscriptions, iotHubs }, ] = useAzureActiveDirectoryStateContext();
|
||||
const [ filterValue, setFilterValue ] = React.useState('');
|
||||
|
||||
const onValueChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
setFilterValue(newValue);
|
||||
if (props.filterType === FilterType.IoTHub) {
|
||||
props.setFilteredList(iotHubs.filter(item => item.name.toLowerCase().includes(newValue.toLowerCase())));
|
||||
}
|
||||
else {
|
||||
props.setFilteredList(subscriptions.filter(item => item.displayName.toLowerCase().includes(newValue.toLowerCase())));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
placeholder={t(ResourceKeys.authentication.azureActiveDirectory.filter.placeHolder)}
|
||||
ariaLabel={t(ResourceKeys.authentication.azureActiveDirectory.filter.placeHolder)}
|
||||
iconProps={{ iconName: 'Filter' }}
|
||||
role="searchbox"
|
||||
value={filterValue}
|
||||
onChange={onValueChange}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { HubList } from './hubList';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import * as azureActiveDirectoryStateContext from '../context/azureActiveDirectoryStateContext';
|
||||
|
||||
describe('HubList', () => {
|
||||
it('matches snapshot when there are no list items', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), formState: 'idle'}, azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<HubList/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when there are list items', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), iotHubs: [{
|
||||
name: 'hub',
|
||||
location: 'westus',
|
||||
id: 'id'
|
||||
}]}, azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<HubList/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DetailsList, IColumn, Link, SelectionMode } from '@fluentui/react';
|
||||
import { useAzureActiveDirectoryStateContext } from '../context/azureActiveDirectoryStateContext';
|
||||
import { IotHubDescription } from '../../../api/models/iotHubDescription';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { LARGE_COLUMN_WIDTH } from '../../../constants/columnWidth';
|
||||
import { getConnectionInfoFromConnectionString } from '../../../api/shared/utils';
|
||||
import { ROUTE_PARTS } from '../../../constants/routes';
|
||||
import { FilterTextBox, FilterType } from './filterTextBox';
|
||||
|
||||
export const HubList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [{ formState, iotHubs, iotHubKey }, { getIotHubKey }] = useAzureActiveDirectoryStateContext();
|
||||
const [ filteredHubs, setFilteredHubs ] = React.useState<IotHubDescription[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (formState === 'keyPicked') { // only when connection string got picked successfully would navigate to device list view
|
||||
const hostName = getConnectionInfoFromConnectionString(iotHubKey).hostName;
|
||||
history.push(`/${ROUTE_PARTS.IOT_HUB}/${ROUTE_PARTS.HOST_NAME}/${hostName}/`);
|
||||
}
|
||||
}, [formState]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFilteredHubs(iotHubs);
|
||||
}, [iotHubs]);
|
||||
|
||||
const getHubKey = (hubId: string, hubName: string) => () => {
|
||||
getIotHubKey(hubId, hubName);
|
||||
};
|
||||
|
||||
const getColumns = (): IColumn[] => {
|
||||
const columnProps = {
|
||||
isResizable: true,
|
||||
maxWidth: LARGE_COLUMN_WIDTH,
|
||||
minWidth: 150
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
...columnProps,
|
||||
key: 'name',
|
||||
name: t(ResourceKeys.authentication.azureActiveDirectory.hubList.columns.name),
|
||||
onRender: (item: IotHubDescription) => (
|
||||
<Link key={item.name} onClick={getHubKey(item.id, item.name)}>
|
||||
{item.name}
|
||||
</Link>)
|
||||
},
|
||||
{ ...columnProps,
|
||||
key: 'id',
|
||||
name: t(ResourceKeys.authentication.azureActiveDirectory.hubList.columns.location),
|
||||
onRender: item => item.location
|
||||
}];
|
||||
};
|
||||
|
||||
const setFilteredList = (listValue: IotHubDescription[]) => {
|
||||
setFilteredHubs(listValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterTextBox
|
||||
filterType={FilterType.IoTHub}
|
||||
setFilteredList={setFilteredList}
|
||||
/>
|
||||
<DetailsList
|
||||
items={filteredHubs}
|
||||
columns={getColumns()}
|
||||
selectionMode={SelectionMode.none}
|
||||
/>
|
||||
{formState === 'idle' && filteredHubs?.length === 0 && t(ResourceKeys.authentication.azureActiveDirectory.hubList.noItemText)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
.hub-selection-list {
|
||||
margin-left: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { HubSelection } from './hubSelection';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import * as azureActiveDirectoryStateContext from '../context/azureActiveDirectoryStateContext';
|
||||
|
||||
describe('HubSelection', () => {
|
||||
it('matches snapshot when page is loading', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), formState: 'working'}, azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<HubSelection/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when token is present', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), token: 'token'}, azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<HubSelection/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when hub list is shown', () => {
|
||||
jest.spyOn(React, 'useState').mockImplementationOnce(() => React.useState(true));
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[getInitialAzureActiveDirectoryState(), azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<HubSelection/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { MultiLineShimmer } from '../../../shared/components/multiLineShimmer';
|
||||
import { useAzureActiveDirectoryStateContext } from '../context/azureActiveDirectoryStateContext';
|
||||
import { AzureActiveDirectoryCommandBar } from './commandBar';
|
||||
import { HubList } from './hubList';
|
||||
import { SubscriptionList } from './subscrptionList';
|
||||
import { BackButton } from './backButton';
|
||||
import './hubSelection.scss';
|
||||
|
||||
export const HubSelection: React.FC = () => {
|
||||
const [{ token, formState }, { getToken, getSubscriptions }] = useAzureActiveDirectoryStateContext();
|
||||
const [ showHubList, setShowHubList ] = React.useState<boolean>(false);
|
||||
|
||||
const renderSubscriptionList = () => {
|
||||
setShowHubList(false);
|
||||
};
|
||||
|
||||
const renderHubList = () => {
|
||||
setShowHubList(true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
getToken();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (token) {
|
||||
getSubscriptions();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AzureActiveDirectoryCommandBar/>
|
||||
{formState === 'working' ? <MultiLineShimmer/> : <div className="hub-selection-list">
|
||||
{token && !showHubList &&
|
||||
<SubscriptionList renderHubList={renderHubList}/>
|
||||
}
|
||||
{showHubList &&
|
||||
<>
|
||||
<BackButton backToSubscription={renderSubscriptionList}/>
|
||||
<HubList/>
|
||||
</>
|
||||
}
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SubscriptionList } from './subscrptionList';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import * as azureActiveDirectoryStateContext from '../context/azureActiveDirectoryStateContext';
|
||||
import { SubscriptionState } from '../../../api/models/azureSubscription';
|
||||
|
||||
describe('SubscriptionList', () => {
|
||||
it('matches snapshot when there are no list items', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), formState: 'idle'}, azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<SubscriptionList renderHubList={jest.fn()}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when there are list items', () => {
|
||||
jest.spyOn(azureActiveDirectoryStateContext, 'useAzureActiveDirectoryStateContext').mockReturnValue(
|
||||
[{...getInitialAzureActiveDirectoryState(), subscriptions: [{
|
||||
displayName: 'test',
|
||||
id: 'id',
|
||||
tenantId: 'id',
|
||||
state: SubscriptionState.Disabled,
|
||||
subscriptionId: 'id'
|
||||
}]}, azureActiveDirectoryStateContext.getInitialAzureActiveDirectoryOps()]);
|
||||
const wrapper = shallow(<SubscriptionList renderHubList={jest.fn()}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DetailsList, IColumn, Link, SelectionMode } from '@fluentui/react';
|
||||
import { FilterTextBox, FilterType } from './filterTextBox';
|
||||
import { useAzureActiveDirectoryStateContext } from '../context/azureActiveDirectoryStateContext';
|
||||
import { AzureSubscription } from '../../../api/models/azureSubscription';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { LARGE_COLUMN_WIDTH } from '../../../constants/columnWidth';
|
||||
|
||||
export interface SubscriptionListPros {
|
||||
renderHubList: () => void;
|
||||
}
|
||||
|
||||
export const SubscriptionList: React.FC<SubscriptionListPros> = props => {
|
||||
const { t } = useTranslation();
|
||||
const [{ formState, subscriptions }, { getIotHubs } ] = useAzureActiveDirectoryStateContext();
|
||||
const [ filteredSubscriptions, setFilteredSubscriptions ] = React.useState<AzureSubscription[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFilteredSubscriptions(subscriptions);
|
||||
}, [subscriptions]);
|
||||
|
||||
const renderHubList = (subscriptionId: string) => () => {
|
||||
getIotHubs(subscriptionId);
|
||||
props.renderHubList();
|
||||
};
|
||||
|
||||
const getColumns = (): IColumn[] => {
|
||||
const columnProps = {
|
||||
isResizable: true,
|
||||
maxWidth: LARGE_COLUMN_WIDTH,
|
||||
minWidth: 150
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
...columnProps,
|
||||
key: 'name',
|
||||
name: t(ResourceKeys.authentication.azureActiveDirectory.subscriptionList.columns.name),
|
||||
onRender: (item: AzureSubscription) => (
|
||||
<Link key={item.displayName} onClick={renderHubList(item.subscriptionId)}>
|
||||
{item.displayName}
|
||||
</Link>)
|
||||
},
|
||||
{ ...columnProps,
|
||||
key: 'id',
|
||||
name: t(ResourceKeys.authentication.azureActiveDirectory.subscriptionList.columns.id),
|
||||
onRender: item => item.subscriptionId
|
||||
},
|
||||
{
|
||||
...columnProps,
|
||||
key: 'state',
|
||||
name: t(ResourceKeys.authentication.azureActiveDirectory.subscriptionList.columns.state),
|
||||
onRender: item => item.state
|
||||
}];
|
||||
};
|
||||
|
||||
const setFilteredList = (listValue: AzureSubscription[]) => {
|
||||
setFilteredSubscriptions(listValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterTextBox
|
||||
filterType={FilterType.Subscription}
|
||||
setFilteredList={setFilteredList}
|
||||
/>
|
||||
<DetailsList
|
||||
items={filteredSubscriptions}
|
||||
columns={getColumns()}
|
||||
selectionMode={SelectionMode.none}
|
||||
/>
|
||||
{formState === 'idle' && filteredSubscriptions?.length === 0 && t(ResourceKeys.authentication.azureActiveDirectory.subscriptionList.noItemText)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AzureActiveDirectoryStateContextProvider matches snapshot 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Array [
|
||||
Object {
|
||||
"formState": "initialized",
|
||||
"iotHubKey": undefined,
|
||||
"iotHubs": Array [],
|
||||
"subscriptions": Array [],
|
||||
"token": undefined,
|
||||
},
|
||||
Object {
|
||||
"getIotHubKey": [Function],
|
||||
"getIotHubs": [Function],
|
||||
"getSubscriptions": [Function],
|
||||
"getToken": [Function],
|
||||
"login": [Function],
|
||||
"logout": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<span>
|
||||
test
|
||||
</span>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,23 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { getInitialAzureActiveDirectoryState, AzureActiveDirectoryStateInterface } from '../state';
|
||||
import { AzureActiveDirectoryInterface } from './azureActiveDirectoryStateProvider';
|
||||
|
||||
export const getInitialAzureActiveDirectoryOps = (): AzureActiveDirectoryInterface => ({
|
||||
getIotHubKey: () => undefined,
|
||||
getIotHubs: () => undefined,
|
||||
getSubscriptions: () => undefined,
|
||||
getToken: () => undefined,
|
||||
login: () => undefined,
|
||||
logout: () => undefined
|
||||
});
|
||||
|
||||
export const AzureActiveDirectoryStateContext = React.createContext<[AzureActiveDirectoryStateInterface, AzureActiveDirectoryInterface]>
|
||||
([
|
||||
getInitialAzureActiveDirectoryState(),
|
||||
getInitialAzureActiveDirectoryOps()
|
||||
]);
|
||||
export const useAzureActiveDirectoryStateContext = () => React.useContext(AzureActiveDirectoryStateContext);
|
|
@ -0,0 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AzureActiveDirectoryStateContextProvider } from './azureActiveDirectoryStateProvider';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import * as AsyncSagaReducer from '../../../shared/hooks/useAsyncSagaReducer';
|
||||
|
||||
describe('AzureActiveDirectoryStateContextProvider', ()=> {
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([getInitialAzureActiveDirectoryState(), jest.fn()]);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
const component = <AzureActiveDirectoryStateContextProvider>
|
||||
<span>test</span>
|
||||
</AzureActiveDirectoryStateContextProvider>;
|
||||
expect(shallow(component)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import * as React from 'react';
|
||||
import { getInitialAzureActiveDirectoryState } from '../state';
|
||||
import { azureActiveDirectoryReducer } from '../reducers';
|
||||
import { useAsyncSagaReducer } from '../../../shared/hooks/useAsyncSagaReducer';
|
||||
import { AzureActiveDirectoryStateContext } from './azureActiveDirectoryStateContext';
|
||||
import { getUserProfileTokenAction, getSubscriptionListAction, loginAction, logoutAction, getIotHubsBySubscriptionAction, getIoTHubKeyAction } from '../actions';
|
||||
import { azureActiveDirectorySaga } from '../saga';
|
||||
|
||||
export interface AzureActiveDirectoryInterface {
|
||||
getIotHubKey(hubId: string, hubName: string): void;
|
||||
getIotHubs(subscriptionId: string): void;
|
||||
getSubscriptions(): void;
|
||||
getToken(): void;
|
||||
login(): void;
|
||||
logout(): void;
|
||||
}
|
||||
|
||||
export const AzureActiveDirectoryStateContextProvider: React.FC = props => {
|
||||
const [state, dispatch] = useAsyncSagaReducer(azureActiveDirectoryReducer, azureActiveDirectorySaga, getInitialAzureActiveDirectoryState(), 'azureActiveDirectoryState');
|
||||
|
||||
const azureActiveDirectoryApi: AzureActiveDirectoryInterface = {
|
||||
getIotHubKey: (hubId: string, hubName: string) => dispatch(getIoTHubKeyAction.started({hubId, hubName})),
|
||||
getIotHubs: (subscriptionId: string) => dispatch(getIotHubsBySubscriptionAction.started(subscriptionId)),
|
||||
getSubscriptions: () => dispatch(getSubscriptionListAction.started()),
|
||||
getToken: () => dispatch(getUserProfileTokenAction.started()),
|
||||
login: () => dispatch(loginAction.started()),
|
||||
logout: () => dispatch(logoutAction.started())
|
||||
};
|
||||
|
||||
return (
|
||||
<AzureActiveDirectoryStateContext.Provider value={[state, azureActiveDirectoryApi]}>
|
||||
{props.children}
|
||||
</AzureActiveDirectoryStateContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { azureActiveDirectoryReducer } from './reducers';
|
||||
import { loginAction, logoutAction, getIotHubsBySubscriptionAction, getSubscriptionListAction, getUserProfileTokenAction, getIoTHubKeyAction } from './actions';
|
||||
import { getInitialAzureActiveDirectoryState } from './state';
|
||||
import { SubscriptionState } from '../../api/models/azureSubscription';
|
||||
|
||||
describe('azureActiveDirectoryReducer', () => {
|
||||
context('AAD/GET_IOTHUBS', () => {
|
||||
it('handles AAD/GET_IOTHUBS_STARTED action', ()=> {
|
||||
const action = getIotHubsBySubscriptionAction.started('subscriptionId');
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AAD/GET_IOTHUBS_DONE action', ()=> {
|
||||
const iotHub = {
|
||||
name: 'hub',
|
||||
location: 'westus',
|
||||
id: 'id'
|
||||
}
|
||||
const action = getIotHubsBySubscriptionAction.done({params: 'subscriptionId', result: [iotHub]});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action)).toEqual({
|
||||
...getInitialAzureActiveDirectoryState(),
|
||||
formState: 'idle',
|
||||
iotHubs: [iotHub]
|
||||
});
|
||||
});
|
||||
|
||||
it('handles AAD/GET_IOTHUBS_FAILED action', ()=> {
|
||||
const action = getIotHubsBySubscriptionAction.failed({params: 'subscriptionId', error: {}});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
|
||||
context('AAD/GET_SUBSCRIPTIONS', () => {
|
||||
it('handles AAD/GET_SUBSCRIPTIONS_STARTED action', ()=> {
|
||||
const action = getSubscriptionListAction.started();
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AAD/GET_SUBSCRIPTIONS_DONE action', ()=> {
|
||||
const sub = {
|
||||
displayName: 'test',
|
||||
id: 'id',
|
||||
tenantId: 'id',
|
||||
state: SubscriptionState.Disabled,
|
||||
subscriptionId: 'id'
|
||||
}
|
||||
const action = getSubscriptionListAction.done({result: [sub]});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action)).toEqual({
|
||||
...getInitialAzureActiveDirectoryState(),
|
||||
formState: 'idle',
|
||||
subscriptions: [sub]
|
||||
});
|
||||
});
|
||||
|
||||
it('handles AAD/GET_SUBSCRIPTIONS_FAILED action', ()=> {
|
||||
const action = getSubscriptionListAction.failed({error: {}});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
|
||||
context('AAD/GET_TOKEN', () => {
|
||||
it('handles AAD/GET_TOKEN_STARTED action', ()=> {
|
||||
const action = getUserProfileTokenAction.started();
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AAD/GET_TOKEN_DONE action', ()=> {
|
||||
const action = getUserProfileTokenAction.done({result: 'token'});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action)).toEqual({
|
||||
...getInitialAzureActiveDirectoryState(),
|
||||
formState: 'idle',
|
||||
token: 'token'
|
||||
});
|
||||
});
|
||||
|
||||
it('handles AAD/GET_TOKEN_FAILED action', ()=> {
|
||||
const action = getUserProfileTokenAction.failed({error: {}});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
|
||||
context('AAD/GET_HUBKEY', () => {
|
||||
const params = {hubId: 'hubid', hubName: 'test'};
|
||||
it('handles AAD/GET_HUBKEY_STARTED action', ()=> {
|
||||
const action = getIoTHubKeyAction.started(params);
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AAD/GET_HUBKEY_DONE action', ()=> {
|
||||
const action = getIoTHubKeyAction.done({params, result: 'key'});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action)).toEqual({
|
||||
...getInitialAzureActiveDirectoryState(),
|
||||
formState: 'keyPicked',
|
||||
iotHubKey: 'key'
|
||||
});
|
||||
});
|
||||
|
||||
it('handles AAD/GET_HUBKEY_FAILED action', ()=> {
|
||||
const action = getIoTHubKeyAction.failed({params, error: {}});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
|
||||
context('AAD/LOGIN', () => {
|
||||
it('handles AAD/LOGIN_STARTED action', ()=> {
|
||||
const action = loginAction.started();
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AAD/LOGIN_DONE action', ()=> {
|
||||
const action = loginAction.done({});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('idle');
|
||||
});
|
||||
|
||||
it('handles AAD/LOGIN_FAILED action', ()=> {
|
||||
const action = loginAction.failed({error: {}});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
|
||||
context('AAD/LOGOUT', () => {
|
||||
it('handles AAD/LOGOUT_STARTED action', ()=> {
|
||||
const action = logoutAction.started();
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AAD/LOGOUT_DONE action', ()=> {
|
||||
const action = logoutAction.done({});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('idle');
|
||||
});
|
||||
|
||||
it('handles AAD/LOGOUT_FAILED action', ()=> {
|
||||
const action = logoutAction.failed({error: {}});
|
||||
expect(azureActiveDirectoryReducer(getInitialAzureActiveDirectoryState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
import { getUserProfileTokenAction, loginAction, logoutAction, getSubscriptionListAction, getIotHubsBySubscriptionAction, getIoTHubKeyAction, GetIotHubKeyActionParmas } from './actions';
|
||||
import { getInitialAzureActiveDirectoryState, AzureActiveDirectoryStateInterface } from './state';
|
||||
import { AzureSubscription } from '../../api/models/azureSubscription';
|
||||
import { IotHubDescription } from '../../api/models/iotHubDescription';
|
||||
|
||||
export const azureActiveDirectoryReducer = reducerWithInitialState<AzureActiveDirectoryStateInterface>(getInitialAzureActiveDirectoryState())
|
||||
.case(getUserProfileTokenAction.started, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working'
|
||||
};
|
||||
})
|
||||
.case(getUserProfileTokenAction.done, (state: AzureActiveDirectoryStateInterface, payload: {params: void, result: string}) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'idle',
|
||||
token: payload.result
|
||||
};
|
||||
})
|
||||
.case(getUserProfileTokenAction.failed, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
})
|
||||
.case(loginAction.started, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working',
|
||||
};
|
||||
})
|
||||
.case(loginAction.done, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'idle',
|
||||
};
|
||||
})
|
||||
.case(loginAction.failed, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
})
|
||||
.case(logoutAction.started, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working',
|
||||
};
|
||||
})
|
||||
.case(logoutAction.done, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'idle',
|
||||
};
|
||||
})
|
||||
.case(logoutAction.failed, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
})
|
||||
.case(getSubscriptionListAction.started, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working'
|
||||
};
|
||||
})
|
||||
.case(getSubscriptionListAction.done, (state: AzureActiveDirectoryStateInterface, payload: {result: AzureSubscription[]}) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'idle',
|
||||
subscriptions: payload.result
|
||||
};
|
||||
})
|
||||
.case(getSubscriptionListAction.failed, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
})
|
||||
.case(getIotHubsBySubscriptionAction.started, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working'
|
||||
};
|
||||
})
|
||||
.case(getIotHubsBySubscriptionAction.done, (state: AzureActiveDirectoryStateInterface, payload: {params: string, result: IotHubDescription[]}) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'idle',
|
||||
iotHubs: payload.result
|
||||
};
|
||||
})
|
||||
.case(getIotHubsBySubscriptionAction.failed, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
})
|
||||
.case(getIoTHubKeyAction.started, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working'
|
||||
};
|
||||
})
|
||||
.case(getIoTHubKeyAction.done, (state: AzureActiveDirectoryStateInterface, payload: {params: GetIotHubKeyActionParmas, result: string}) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'keyPicked',
|
||||
iotHubKey: payload.result
|
||||
};
|
||||
})
|
||||
.case(getIoTHubKeyAction.failed, (state: AzureActiveDirectoryStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import { getUserProfileTokenAction, loginAction, logoutAction, getSubscriptionListAction, getIotHubsBySubscriptionAction, getIoTHubKeyAction } from './actions';
|
||||
import { getTokenSaga } from './sagas/getUserProfileTokenSaga';
|
||||
import { loginSaga } from './sagas/loginSaga';
|
||||
import { logoutSaga } from './sagas/logoutSaga';
|
||||
import { getSubscriptionListSaga } from './sagas/getSubscriptionListSaga';
|
||||
import { getIotHubListSaga } from './sagas/getIotHubListSaga';
|
||||
import { getIotHubKeySaga } from './sagas/getIotHubKeySaga';
|
||||
|
||||
export function* azureActiveDirectorySaga() {
|
||||
yield takeLatest(getUserProfileTokenAction.started, getTokenSaga);
|
||||
yield takeLatest(loginAction.started, loginSaga);
|
||||
yield takeLatest(logoutAction.started, logoutSaga);
|
||||
yield takeLatest(getSubscriptionListAction.started, getSubscriptionListSaga);
|
||||
yield takeLatest(getIotHubsBySubscriptionAction.started, getIotHubListSaga);
|
||||
yield takeLatest(getIoTHubKeyAction.started, getIotHubKeySaga);
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { getIoTHubKeyAction } from '../actions';
|
||||
import { formatConnectionString, getIotHubKeySaga } from './getIotHubKeySaga';
|
||||
import * as IotHubService from '../../../api/services/iotHubService';
|
||||
import * as AuthenticationService from '../../../api/services/authenticationService';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
|
||||
describe('getIotHubKeySaga', () => {
|
||||
|
||||
const mockGetProfileToken = jest.spyOn(AuthenticationService, 'getProfileToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
const mockGetIotKeys = jest.spyOn(IotHubService, 'getIotHubKeys').mockImplementationOnce(() => {
|
||||
return [];
|
||||
});
|
||||
const params = {hubId: 'hubid', hubName: 'test'};
|
||||
const authRules = [{
|
||||
keyName: 'iothubowner',
|
||||
primaryKey: 'key12345',
|
||||
rights: 'RegistryWrite, ServiceConnect, DeviceConnect'
|
||||
}]
|
||||
const sagaGenerator = cloneableGenerator(getIotHubKeySaga)(getIoTHubKeyAction.started(params));
|
||||
|
||||
it('calls getProfileToken', () => {
|
||||
expect(sagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: call(mockGetProfileToken)
|
||||
});
|
||||
});
|
||||
|
||||
it('calls getIotHubKeys', () => {
|
||||
expect(sagaGenerator.next('token')).toEqual({
|
||||
done: false,
|
||||
value: call(mockGetIotKeys, {
|
||||
authorizationToken: 'token',
|
||||
endpoint: undefined,
|
||||
hubId: 'hubid',
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the successful action', () => {
|
||||
const success = sagaGenerator.clone();
|
||||
expect(success.next(authRules)).toEqual({
|
||||
done: false,
|
||||
value: put(getIoTHubKeyAction.done({
|
||||
params,
|
||||
result: formatConnectionString(params.hubName, authRules[0])
|
||||
}))
|
||||
});
|
||||
expect(success.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails on error', () => {
|
||||
const failure = sagaGenerator.clone();
|
||||
const error = { message: 'error' };
|
||||
expect(failure.throw(error)).toEqual({
|
||||
done: false,
|
||||
value: call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.getIotHubKeyError,
|
||||
translationOptions: {error: 'error'}
|
||||
},
|
||||
type: NotificationType.error
|
||||
})
|
||||
});
|
||||
|
||||
expect(failure.next(error)).toEqual({
|
||||
done: false,
|
||||
value: put(getIoTHubKeyAction.failed({
|
||||
error,
|
||||
params
|
||||
}))
|
||||
});
|
||||
expect(failure.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { Action } from 'typescript-fsa';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { getIotHubKeys } from '../../../api/services/iotHubService';
|
||||
import { getIoTHubKeyAction, GetIotHubKeyActionParmas } from '../actions';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
import { appConfig } from '../../../../appConfig/appConfig';
|
||||
import { getProfileToken } from '../../../api/services/authenticationService';
|
||||
import { CONNECTION_STRING_THROUGH_AAD } from '../../../constants/browserStorage';
|
||||
import { SharedAccessSignatureAuthorizationRule, AccessRights } from '../../../api/models/sharedAccessSignatureAuthorizationRule';
|
||||
|
||||
export function* getIotHubKeySaga(action: Action<GetIotHubKeyActionParmas>) {
|
||||
try {
|
||||
const authorizationToken: string = yield call(getProfileToken); // always get a fresh token to prevent expiration
|
||||
const results: SharedAccessSignatureAuthorizationRule[] = yield call(getIotHubKeys, {
|
||||
authorizationToken,
|
||||
endpoint: appConfig.azureResourceManagementEndpoint,
|
||||
hubId: action.payload.hubId
|
||||
});
|
||||
|
||||
const filtered = results.filter(element => filterKey(element.rights));
|
||||
if (!filtered || !filtered.length) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const result = formatConnectionString(action.payload.hubName, filtered[0]);
|
||||
localStorage.setItem(CONNECTION_STRING_THROUGH_AAD, result);
|
||||
yield put(getIoTHubKeyAction.done({params: action.payload, result}));
|
||||
}
|
||||
catch (error) {
|
||||
yield call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.getIotHubKeyError,
|
||||
translationOptions: {
|
||||
error: error?.message || error
|
||||
}
|
||||
},
|
||||
type: NotificationType.error
|
||||
});
|
||||
|
||||
yield put(getIoTHubKeyAction.failed({params: action.payload, error}));
|
||||
}
|
||||
}
|
||||
|
||||
export const filterKey = (rights: string) => {
|
||||
const rightArray = rights.split(',').map(element => element.trim());
|
||||
return rightArray.includes(AccessRights.DeviceConnect) &&
|
||||
rightArray.includes(AccessRights.RegistryWrite) &&
|
||||
rightArray.includes(AccessRights.ServiceConnect);
|
||||
};
|
||||
|
||||
export const formatConnectionString = (hubName: string, authRule: SharedAccessSignatureAuthorizationRule) => {
|
||||
return `HostName=${hubName}.azure-devices.net;SharedAccessKeyName=${authRule.keyName};SharedAccessKey=${authRule.primaryKey}`;
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { getIotHubsBySubscriptionAction } from '../actions';
|
||||
import { getIotHubListSaga } from './getIotHubListSaga';
|
||||
import * as IotHubService from '../../../api/services/iotHubService';
|
||||
import * as AuthenticationService from '../../../api/services/authenticationService';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
|
||||
describe('getIotHubListSaga', () => {
|
||||
|
||||
const mockGetProfileToken = jest.spyOn(AuthenticationService, 'getProfileToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
const mockGetIotHubs = jest.spyOn(IotHubService, 'getIotHubsBySubscription').mockImplementationOnce(() => {
|
||||
return [];
|
||||
});
|
||||
const sagaGenerator = cloneableGenerator(getIotHubListSaga)(getIotHubsBySubscriptionAction.started('subscriptionId'));
|
||||
|
||||
it('calls getProfileToken', () => {
|
||||
expect(sagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: call(mockGetProfileToken)
|
||||
});
|
||||
});
|
||||
|
||||
it('calls getIotHubsBySubscription', () => {
|
||||
expect(sagaGenerator.next('token')).toEqual({
|
||||
done: false,
|
||||
value: call(mockGetIotHubs, {
|
||||
authorizationToken: 'token',
|
||||
endpoint: undefined,
|
||||
subscriptionId: 'subscriptionId',
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the successful action', () => {
|
||||
const success = sagaGenerator.clone();
|
||||
expect(success.next([])).toEqual({
|
||||
done: false,
|
||||
value: put(getIotHubsBySubscriptionAction.done({
|
||||
params: 'subscriptionId',
|
||||
result: []
|
||||
}))
|
||||
});
|
||||
expect(success.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails on error', () => {
|
||||
const failure = sagaGenerator.clone();
|
||||
const error = { message: 'error' };
|
||||
expect(failure.throw(error)).toEqual({
|
||||
done: false,
|
||||
value: call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.getIotHubListError,
|
||||
translationOptions: {error: 'error'}
|
||||
},
|
||||
type: NotificationType.error
|
||||
})
|
||||
});
|
||||
|
||||
expect(failure.next(error)).toEqual({
|
||||
done: false,
|
||||
value: put(getIotHubsBySubscriptionAction.failed({
|
||||
error,
|
||||
params: 'subscriptionId',
|
||||
}))
|
||||
});
|
||||
expect(failure.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { Action } from 'typescript-fsa';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { getIotHubsBySubscription } from '../../../api/services/iotHubService';
|
||||
import { getIotHubsBySubscriptionAction } from '../actions';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
import { IotHubDescription } from '../../../api/models/iotHubDescription';
|
||||
import { appConfig } from '../../../../appConfig/appConfig';
|
||||
import { getProfileToken } from '../../../api/services/authenticationService';
|
||||
|
||||
export function* getIotHubListSaga(action: Action<string>) {
|
||||
try {
|
||||
const authorizationToken: string = yield call(getProfileToken); // always get a fresh token to prevent expiration
|
||||
const result: IotHubDescription[] = yield call(getIotHubsBySubscription, {
|
||||
authorizationToken,
|
||||
endpoint: appConfig.azureResourceManagementEndpoint,
|
||||
subscriptionId: action.payload
|
||||
});
|
||||
yield put(getIotHubsBySubscriptionAction.done({params: action.payload, result: result.sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)}));
|
||||
}
|
||||
catch (error) {
|
||||
yield call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.getIotHubListError,
|
||||
translationOptions: {
|
||||
error: error?.message || error
|
||||
}
|
||||
},
|
||||
type: NotificationType.error
|
||||
});
|
||||
|
||||
yield put(getIotHubsBySubscriptionAction.failed({params: action.payload, error}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { getSubscriptionListAction } from '../actions';
|
||||
import { getSubscriptionListSaga } from './getSubscriptionListSaga';
|
||||
import * as AzureSubscriptionService from '../../../api/services/azureSubscriptionService';
|
||||
import * as AuthenticationService from '../../../api/services/authenticationService';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
|
||||
describe('getSubscriptionListSaga', () => {
|
||||
|
||||
const mockGetProfileToken = jest.spyOn(AuthenticationService, 'getProfileToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
const mockGetIubscriptions = jest.spyOn(AzureSubscriptionService, 'getAzureSubscriptions').mockImplementationOnce(() => {
|
||||
return [];
|
||||
});
|
||||
const sagaGenerator = cloneableGenerator(getSubscriptionListSaga)(getSubscriptionListAction.started());
|
||||
|
||||
it('calls getProfileToken', () => {
|
||||
expect(sagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: call(mockGetProfileToken)
|
||||
});
|
||||
});
|
||||
|
||||
it('calls getIotHubsBySubscription', () => {
|
||||
expect(sagaGenerator.next('token')).toEqual({
|
||||
done: false,
|
||||
value: call(mockGetIubscriptions, {
|
||||
authorizationToken: 'token',
|
||||
endpoint: undefined
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the successful action', () => {
|
||||
const success = sagaGenerator.clone();
|
||||
expect(success.next([])).toEqual({
|
||||
done: false,
|
||||
value: put(getSubscriptionListAction.done({
|
||||
result: []
|
||||
}))
|
||||
});
|
||||
expect(success.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails on error', () => {
|
||||
const failure = sagaGenerator.clone();
|
||||
const error = { code: -1 };
|
||||
expect(failure.throw(error)).toEqual({
|
||||
done: false,
|
||||
value: call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.getsubscriptionListError,
|
||||
translationOptions: {error}
|
||||
},
|
||||
type: NotificationType.error
|
||||
})
|
||||
});
|
||||
|
||||
expect(failure.next(error)).toEqual({
|
||||
done: false,
|
||||
value: put(getSubscriptionListAction.failed({
|
||||
error
|
||||
}))
|
||||
});
|
||||
expect(failure.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { getAzureSubscriptions } from '../../../api/services/azureSubscriptionService';
|
||||
import { getSubscriptionListAction } from '../actions';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
import { AzureSubscription } from '../../../api/models/azureSubscription';
|
||||
import { appConfig } from '../../../../appConfig/appConfig';
|
||||
import { getProfileToken } from '../../../api/services/authenticationService';
|
||||
|
||||
export function* getSubscriptionListSaga() {
|
||||
try {
|
||||
const authorizationToken: string = yield call(getProfileToken); // always get a fresh token to prevent expiration
|
||||
const result: AzureSubscription[] = yield call(getAzureSubscriptions, {
|
||||
authorizationToken,
|
||||
endpoint: appConfig.azureResourceManagementEndpoint
|
||||
});
|
||||
yield put(getSubscriptionListAction.done({result: result.sort((a, b) => a.displayName.toLowerCase() > b.displayName.toLowerCase() ? 1 : -1)}));
|
||||
}
|
||||
catch (error) {
|
||||
yield call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.getsubscriptionListError,
|
||||
translationOptions: {
|
||||
error: error?.message || error
|
||||
}
|
||||
},
|
||||
type: NotificationType.error
|
||||
});
|
||||
|
||||
yield put(getSubscriptionListAction.failed({error}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { getUserProfileTokenAction } from '../actions';
|
||||
import { getTokenSaga } from './getUserProfileTokenSaga';
|
||||
import * as AuthenticationService from '../../../api/services/authenticationService';
|
||||
|
||||
describe('getTokenSaga', () => {
|
||||
|
||||
const mockGetProfileToken = jest.spyOn(AuthenticationService, 'getProfileToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
const sagaGenerator = cloneableGenerator(getTokenSaga)();
|
||||
|
||||
it('calls getProfileToken', () => {
|
||||
expect(sagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: call(mockGetProfileToken)
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the successful action', () => {
|
||||
const success = sagaGenerator.clone();
|
||||
expect(success.next('token')).toEqual({
|
||||
done: false,
|
||||
value: put(getUserProfileTokenAction.done({
|
||||
result: 'token'
|
||||
}))
|
||||
});
|
||||
expect(success.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails on error', () => {
|
||||
const failure = sagaGenerator.clone();
|
||||
const error = { code: -1 };
|
||||
expect(failure.throw(error)).toEqual({
|
||||
done: false,
|
||||
value: put(getUserProfileTokenAction.failed({
|
||||
error
|
||||
}))
|
||||
});
|
||||
expect(failure.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { getProfileToken } from '../../../api/services/authenticationService';
|
||||
import { getUserProfileTokenAction } from '../actions';
|
||||
|
||||
export function* getTokenSaga() {
|
||||
try {
|
||||
const token: string = yield call(getProfileToken);
|
||||
yield put(getUserProfileTokenAction.done({result: token}));
|
||||
}
|
||||
catch (error) {
|
||||
yield put(getUserProfileTokenAction.failed({error}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { loginAction } from '../actions';
|
||||
import { loginSaga } from './loginSaga';
|
||||
import * as AuthenticationService from '../../../api/services/authenticationService';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
|
||||
describe('loginSaga', () => {
|
||||
|
||||
const mockLogin = jest.spyOn(AuthenticationService, 'login').mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
const sagaGenerator = cloneableGenerator(loginSaga)();
|
||||
|
||||
it('calls login', () => {
|
||||
expect(sagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: call(mockLogin)
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the successful action', () => {
|
||||
const success = sagaGenerator.clone();
|
||||
expect(success.next()).toEqual({
|
||||
done: false,
|
||||
value: put(loginAction.done({}))
|
||||
});
|
||||
expect(success.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails on error', () => {
|
||||
const failure = sagaGenerator.clone();
|
||||
const error = { code: -1 };
|
||||
|
||||
expect(failure.throw(error)).toEqual({
|
||||
done: false,
|
||||
value: call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.loginError,
|
||||
},
|
||||
type: NotificationType.error
|
||||
})
|
||||
});
|
||||
|
||||
expect(failure.next(error)).toEqual({
|
||||
done: false,
|
||||
value: put(loginAction.failed({
|
||||
error
|
||||
}))
|
||||
});
|
||||
expect(failure.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { login } from '../../../api/services/authenticationService';
|
||||
import { loginAction } from '../actions';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
|
||||
export function* loginSaga() {
|
||||
try {
|
||||
yield call(login);
|
||||
yield put(loginAction.done({}));
|
||||
}
|
||||
catch (error) {
|
||||
yield call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.loginError
|
||||
},
|
||||
type: NotificationType.error
|
||||
});
|
||||
|
||||
yield put(loginAction.failed({error}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { logoutAction } from '../actions';
|
||||
import { logoutSaga } from './logoutSaga';
|
||||
import * as AuthenticationService from '../../../api/services/authenticationService';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
|
||||
describe('logoutSaga', () => {
|
||||
|
||||
const mockLogout = jest.spyOn(AuthenticationService, 'logout').mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
const sagaGenerator = cloneableGenerator(logoutSaga)();
|
||||
|
||||
it('calls logout', () => {
|
||||
expect(sagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: call(mockLogout)
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the successful action', () => {
|
||||
const success = sagaGenerator.clone();
|
||||
expect(success.next()).toEqual({
|
||||
done: false,
|
||||
value: put(logoutAction.done({}))
|
||||
});
|
||||
expect(success.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails on error', () => {
|
||||
const failure = sagaGenerator.clone();
|
||||
const error = { code: -1 };
|
||||
|
||||
expect(failure.throw(error)).toEqual({
|
||||
done: false,
|
||||
value: call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.logoutError,
|
||||
},
|
||||
type: NotificationType.error
|
||||
})
|
||||
});
|
||||
|
||||
expect(failure.next(error)).toEqual({
|
||||
done: false,
|
||||
value: put(logoutAction.failed({
|
||||
error
|
||||
}))
|
||||
});
|
||||
expect(failure.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
import { logout } from '../../../api/services/authenticationService';
|
||||
import { logoutAction } from '../actions';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
|
||||
export function* logoutSaga() {
|
||||
try {
|
||||
yield call(logout);
|
||||
yield put(logoutAction.done({}));
|
||||
}
|
||||
catch (error) {
|
||||
yield call(raiseNotificationToast, {
|
||||
text: {
|
||||
translationKey: ResourceKeys.authentication.azureActiveDirectory.notification.logoutError
|
||||
},
|
||||
type: NotificationType.error
|
||||
});
|
||||
|
||||
yield put(logoutAction.failed({error}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { IotHubDescription } from '../../api/models/iotHubDescription';
|
||||
import { AzureSubscription } from '../../api/models/azureSubscription';
|
||||
export interface AzureActiveDirectoryStateInterface {
|
||||
formState: 'initialized' | 'working' | 'failed' | 'idle' | 'keyPicked';
|
||||
iotHubKey: string;
|
||||
iotHubs: IotHubDescription[];
|
||||
subscriptions: AzureSubscription[];
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const getInitialAzureActiveDirectoryState = (): AzureActiveDirectoryStateInterface => ({
|
||||
formState: 'initialized',
|
||||
iotHubKey: undefined,
|
||||
iotHubs: [],
|
||||
subscriptions: [],
|
||||
token: undefined
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AuthenticationSelection matches snapshot 1`] = `
|
||||
<div
|
||||
className="auth-slection-container"
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
aria-level={1}
|
||||
role="heading"
|
||||
>
|
||||
authentication.autheSelection.header
|
||||
</h3>
|
||||
<span>
|
||||
authentication.autheSelection.subText
|
||||
</span>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedCompoundButton
|
||||
className="auth-selection-tile"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Permissions",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
primary={true}
|
||||
>
|
||||
authentication.autheSelection.selection.connectionString
|
||||
</CustomizedCompoundButton>
|
||||
<CustomizedCompoundButton
|
||||
className="auth-selection-tile"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "AADLogo",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
secondaryText="authentication.autheSelection.selection.comingSoon"
|
||||
>
|
||||
authentication.autheSelection.selection.azureActiveDirectory
|
||||
</CustomizedCompoundButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AuthenticationView matches snapshot when page is loading 1`] = `
|
||||
<Fragment>
|
||||
<MultiLineShimmer />
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`AuthenticationView matches snapshot when preference is aad 1`] = `
|
||||
<Fragment>
|
||||
<AzureActiveDirectoryStateContextProvider>
|
||||
<HubSelection />
|
||||
</AzureActiveDirectoryStateContextProvider>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`AuthenticationView matches snapshot when preference is connection string 1`] = `
|
||||
<Fragment>
|
||||
<ConnectionStringStateContextProvider>
|
||||
<ConnectionStringsView />
|
||||
</ConnectionStringStateContextProvider>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`AuthenticationView matches snapshot when preference not set 1`] = `
|
||||
<Fragment>
|
||||
<AuthenticationSelection />
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,13 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
.auth-slection-container {
|
||||
display: flex;
|
||||
padding-top: 45px;
|
||||
justify-content: center;
|
||||
.auth-selection-tile {
|
||||
padding: 50px;
|
||||
margin-top: 100px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { CompoundButton } from '@fluentui/react';
|
||||
import { AuthenticationSelection } from './authenticationSelection';
|
||||
import { AuthenticationMethodPreference, getInitialAuthenticationState } from '../state';
|
||||
import * as authenticationStateContext from '../context/authenticationStateContext';
|
||||
|
||||
describe('AuthenticationSelection', () => {
|
||||
it('matches snapshot', () => {
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[getInitialAuthenticationState(), authenticationStateContext.getInitialAuthenticationOps()]);
|
||||
const wrapper = shallow(<AuthenticationSelection/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls api respectively', () => {
|
||||
const setLoginPreference = jest.fn();
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[getInitialAuthenticationState(), {...authenticationStateContext.getInitialAuthenticationOps(), setLoginPreference}]);
|
||||
|
||||
const wrapper = shallow(<AuthenticationSelection/>);
|
||||
|
||||
wrapper.find(CompoundButton).get(0).props.onClick(undefined);
|
||||
wrapper.update();
|
||||
expect(setLoginPreference).toHaveBeenCalledWith(AuthenticationMethodPreference.ConnectionString);
|
||||
|
||||
wrapper.find(CompoundButton).get(1).props.onClick(undefined);
|
||||
wrapper.update();
|
||||
expect(setLoginPreference).toHaveBeenCalledWith(AuthenticationMethodPreference.AzureAD);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { CompoundButton, Stack } from '@fluentui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthenticationStateContext } from '../context/authenticationStateContext';
|
||||
import { AuthenticationMethodPreference } from '../state';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import './authenticationSelection.scss';
|
||||
|
||||
export const AuthenticationSelection: React.FC = () => {
|
||||
const [, { setLoginPreference }] = useAuthenticationStateContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const connectViaConnectionString = () => {
|
||||
setLoginPreference(AuthenticationMethodPreference.ConnectionString);
|
||||
};
|
||||
|
||||
const loginViaAad = () => {
|
||||
setLoginPreference(AuthenticationMethodPreference.AzureAD);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-slection-container">
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<h3 role="heading" aria-level={1}>{t(ResourceKeys.authentication.autheSelection.header)}</h3>
|
||||
<span>{t(ResourceKeys.authentication.autheSelection.subText)}</span>
|
||||
<Stack tokens={{ childrenGap: 80 }} horizontal={true} >
|
||||
<CompoundButton
|
||||
primary={true}
|
||||
iconProps={{ iconName: 'Permissions' }}
|
||||
onClick={connectViaConnectionString}
|
||||
className="auth-selection-tile"
|
||||
>
|
||||
{t(ResourceKeys.authentication.autheSelection.selection.connectionString)}
|
||||
</CompoundButton>
|
||||
<CompoundButton
|
||||
iconProps={{ iconName: 'AADLogo' }}
|
||||
className="auth-selection-tile"
|
||||
onClick={loginViaAad}
|
||||
secondaryText={t(ResourceKeys.authentication.autheSelection.selection.comingSoon)}
|
||||
>
|
||||
{t(ResourceKeys.authentication.autheSelection.selection.azureActiveDirectory)}
|
||||
</CompoundButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AuthenticationView } from './authenticationView';
|
||||
import { AuthenticationMethodPreference, getInitialAuthenticationState } from '../state';
|
||||
import * as authenticationStateContext from '../context/authenticationStateContext';
|
||||
|
||||
describe('AuthenticationView', () => {
|
||||
it('matches snapshot when page is loading', () => {
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[{...getInitialAuthenticationState(), formState: 'working'}, authenticationStateContext.getInitialAuthenticationOps()]);
|
||||
const wrapper = shallow(<AuthenticationView/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when preference not set', () => {
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[getInitialAuthenticationState(), authenticationStateContext.getInitialAuthenticationOps()]);
|
||||
const wrapper = shallow(<AuthenticationView/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
it('matches snapshot when preference is aad', () => {
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[{...getInitialAuthenticationState(), preference: AuthenticationMethodPreference.AzureAD}, authenticationStateContext.getInitialAuthenticationOps()]);
|
||||
const wrapper = shallow(<AuthenticationView/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when preference is connection string', () => {
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[{...getInitialAuthenticationState(), preference: AuthenticationMethodPreference.ConnectionString}, authenticationStateContext.getInitialAuthenticationOps()]);
|
||||
const wrapper = shallow(<AuthenticationView/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { MultiLineShimmer } from '../../shared/components/multiLineShimmer';
|
||||
import { HubSelection } from '../azureActiveDirectory/components/hubSelection';
|
||||
import { AzureActiveDirectoryStateContextProvider } from '../azureActiveDirectory/context/azureActiveDirectoryStateProvider';
|
||||
import { ConnectionStringsView } from '../../connectionStrings/components/connectionStringsView';
|
||||
import { ConnectionStringStateContextProvider } from '../../connectionStrings/context/connectionStringStateProvider';
|
||||
import { useAuthenticationStateContext } from '../context/authenticationStateContext';
|
||||
import { AuthenticationMethodPreference } from '../state';
|
||||
import { AuthenticationSelection } from './authenticationSelection';
|
||||
|
||||
export const AuthenticationView: React.FC = () => {
|
||||
const [{ formState, preference }, api] = useAuthenticationStateContext();
|
||||
React.useEffect(() => {
|
||||
api.getLoginPreference();
|
||||
}, []); // tslint:disable-line: align
|
||||
|
||||
return (
|
||||
<>
|
||||
{formState === 'working' ? <MultiLineShimmer/> :
|
||||
<>
|
||||
{!preference && <AuthenticationSelection/>}
|
||||
{preference === AuthenticationMethodPreference.AzureAD &&
|
||||
<AzureActiveDirectoryStateContextProvider>
|
||||
<HubSelection/>
|
||||
</AzureActiveDirectoryStateContextProvider>
|
||||
}
|
||||
{preference === AuthenticationMethodPreference.ConnectionString &&
|
||||
<ConnectionStringStateContextProvider>
|
||||
<ConnectionStringsView/>
|
||||
</ConnectionStringStateContextProvider>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AuthenticationStateContextProvider matches snapshot 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Array [
|
||||
Object {
|
||||
"formState": "initialized",
|
||||
"preference": undefined,
|
||||
},
|
||||
Object {
|
||||
"getLoginPreference": [Function],
|
||||
"setLoginPreference": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<span>
|
||||
test
|
||||
</span>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,19 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { getInitialAuthenticationState, AuthenticationStateInterface } from '../state';
|
||||
import { AuthenticationInterface } from './authenticationStateProvider';
|
||||
|
||||
export const getInitialAuthenticationOps = (): AuthenticationInterface => ({
|
||||
getLoginPreference: () => undefined,
|
||||
setLoginPreference: () => undefined
|
||||
});
|
||||
|
||||
export const AuthenticationStateContext = React.createContext<[AuthenticationStateInterface, AuthenticationInterface]>
|
||||
([
|
||||
getInitialAuthenticationState(),
|
||||
getInitialAuthenticationOps()
|
||||
]);
|
||||
export const useAuthenticationStateContext = () => React.useContext(AuthenticationStateContext);
|
|
@ -0,0 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AuthenticationStateContextProvider } from './authenticationStateProvider';
|
||||
import { getInitialAuthenticationState } from '../state';
|
||||
import * as AsyncSagaReducer from '../../shared/hooks/useAsyncSagaReducer';
|
||||
|
||||
describe('AuthenticationStateContextProvider', ()=> {
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([getInitialAuthenticationState(), jest.fn()]);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
const component = <AuthenticationStateContextProvider>
|
||||
<span>test</span>
|
||||
</AuthenticationStateContextProvider>;
|
||||
expect(shallow(component)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { getInitialAuthenticationState } from '../state';
|
||||
import { authenticationReducer } from '../reducers';
|
||||
import { useAsyncSagaReducer } from '../../shared/hooks/useAsyncSagaReducer';
|
||||
import { AuthenticationStateContext } from './authenticationStateContext';
|
||||
import { getLoginPreferenceAction, setLoginPreferenceAction } from '../actions';
|
||||
import { authenticationSaga } from '../saga';
|
||||
|
||||
export interface AuthenticationInterface {
|
||||
getLoginPreference(): void;
|
||||
setLoginPreference(preference: string): void;
|
||||
}
|
||||
|
||||
export const AuthenticationStateContextProvider: React.FC = props => {
|
||||
const [state, dispatch] = useAsyncSagaReducer(authenticationReducer, authenticationSaga, getInitialAuthenticationState(), 'authenticationState');
|
||||
|
||||
const authenticationApi: AuthenticationInterface = {
|
||||
getLoginPreference: () => dispatch(getLoginPreferenceAction.started()),
|
||||
setLoginPreference: (reference: string) => dispatch(setLoginPreferenceAction.started(reference))
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticationStateContext.Provider value={[state, authenticationApi]}>
|
||||
{props.children}
|
||||
</AuthenticationStateContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { authenticationReducer } from './reducers';
|
||||
import { getLoginPreferenceAction, setLoginPreferenceAction } from './actions';
|
||||
import { getInitialAuthenticationState, AuthenticationMethodPreference } from './state';
|
||||
|
||||
describe('authenticationReducer', () => {
|
||||
context('AUTHENTICATION/GET', () => {
|
||||
it('handles AUTHENTICATION/GET_STARTED action', ()=> {
|
||||
const action = getLoginPreferenceAction.started();
|
||||
expect(authenticationReducer(getInitialAuthenticationState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AUTHENTICATION/GET_DONE action', ()=> {
|
||||
const action = getLoginPreferenceAction.done({result: AuthenticationMethodPreference.AzureAD});
|
||||
expect(authenticationReducer(getInitialAuthenticationState(), action)).toEqual({
|
||||
formState: 'idle',
|
||||
preference: AuthenticationMethodPreference.AzureAD
|
||||
});
|
||||
});
|
||||
|
||||
it('handles AUTHENTICATION/GET_FAILED action', ()=> {
|
||||
const action = getLoginPreferenceAction.failed({error: {}});
|
||||
expect(authenticationReducer(getInitialAuthenticationState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
|
||||
context('AUTHENTICATION/SET', () => {
|
||||
it('handles AUTHENTICATION/SET_STARTED action', ()=> {
|
||||
const action = setLoginPreferenceAction.started(AuthenticationMethodPreference.AzureAD);
|
||||
expect(authenticationReducer(getInitialAuthenticationState(), action).formState).toEqual('working');
|
||||
});
|
||||
|
||||
it('handles AUTHENTICATION/SET_DONE action', ()=> {
|
||||
const action = setLoginPreferenceAction.done({params: AuthenticationMethodPreference.AzureAD});
|
||||
expect(authenticationReducer(getInitialAuthenticationState(), action)).toEqual({
|
||||
formState: 'idle',
|
||||
preference: AuthenticationMethodPreference.AzureAD
|
||||
});
|
||||
});
|
||||
|
||||
it('handles AUTHENTICATION/SET_FAILED action', ()=> {
|
||||
const action = setLoginPreferenceAction.failed({params: AuthenticationMethodPreference.AzureAD, error: {}});
|
||||
expect(authenticationReducer(getInitialAuthenticationState(), action).formState).toEqual('failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
import { getLoginPreferenceAction, setLoginPreferenceAction } from './actions';
|
||||
import { getInitialAuthenticationState, AuthenticationStateInterface } from './state';
|
||||
|
||||
export const authenticationReducer = reducerWithInitialState<AuthenticationStateInterface>(getInitialAuthenticationState())
|
||||
.case(getLoginPreferenceAction.started, (state: AuthenticationStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working'
|
||||
};
|
||||
})
|
||||
.case(getLoginPreferenceAction.done, (state: AuthenticationStateInterface, payload: {params: void, result: string}) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'idle',
|
||||
preference: payload.result
|
||||
};
|
||||
})
|
||||
.case(getLoginPreferenceAction.failed, (state: AuthenticationStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
})
|
||||
.case(setLoginPreferenceAction.started, (state: AuthenticationStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'working'
|
||||
};
|
||||
})
|
||||
.case(setLoginPreferenceAction.done, (state: AuthenticationStateInterface, payload: {params: string}) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'idle',
|
||||
preference: payload.params
|
||||
};
|
||||
})
|
||||
.case(setLoginPreferenceAction.failed, (state: AuthenticationStateInterface) => {
|
||||
return {
|
||||
...state,
|
||||
formState: 'failed'
|
||||
};
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import { getLoginPreferenceAction, setLoginPreferenceAction } from './actions';
|
||||
import { getLoginPreferenceSaga } from './sagas/getLoginPreferenceSaga';
|
||||
import { setLoginPreferenceSaga } from './sagas/setLoginPreferenceSaga';
|
||||
|
||||
export function* authenticationSaga() {
|
||||
yield takeLatest(getLoginPreferenceAction.started, getLoginPreferenceSaga);
|
||||
yield takeLatest(setLoginPreferenceAction.started, setLoginPreferenceSaga);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { AUTHENTICATION_METHOD_PREFERENCE } from '../../constants/browserStorage';
|
||||
import { getLoginPreferenceAction } from '../actions';
|
||||
import { getLoginPreferenceSaga } from './getLoginPreferenceSaga';
|
||||
|
||||
describe('getLoginPreferenceSaga', () => {
|
||||
it('gets login preference', () => {
|
||||
const sagaGenerator = cloneableGenerator(getLoginPreferenceSaga)();
|
||||
localStorage.setItem(AUTHENTICATION_METHOD_PREFERENCE, 'aad');
|
||||
|
||||
expect(sagaGenerator.next('aad')).toEqual({
|
||||
done: false,
|
||||
value: put(getLoginPreferenceAction.done({result: 'aad'}))
|
||||
});
|
||||
expect(sagaGenerator.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { AUTHENTICATION_METHOD_PREFERENCE } from '../../constants/browserStorage';
|
||||
import { getLoginPreferenceAction } from '../actions';
|
||||
import { AuthenticationMethodPreference } from '../state';
|
||||
|
||||
export function* getLoginPreferenceSaga() {
|
||||
const preference = localStorage.getItem(AUTHENTICATION_METHOD_PREFERENCE);
|
||||
yield put(getLoginPreferenceAction.done({result: preference as AuthenticationMethodPreference}));
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { cloneableGenerator } from '@redux-saga/testing-utils';
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { AUTHENTICATION_METHOD_PREFERENCE } from '../../constants/browserStorage';
|
||||
import { setLoginPreferenceAction } from '../actions';
|
||||
import { setLoginPreferenceSaga } from './setLoginPreferenceSaga';
|
||||
|
||||
describe('setLoginPreferenceSaga', () => {
|
||||
it('sets login preference', () => {
|
||||
const sagaGenerator = cloneableGenerator(setLoginPreferenceSaga)(setLoginPreferenceAction.started('aad'));
|
||||
localStorage.setItem(AUTHENTICATION_METHOD_PREFERENCE, '');
|
||||
|
||||
expect(sagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: put(setLoginPreferenceAction.done({params: 'aad'}))
|
||||
});
|
||||
expect(sagaGenerator.next().done).toEqual(true);
|
||||
expect(localStorage.getItem(AUTHENTICATION_METHOD_PREFERENCE)).toEqual('aad');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { Action } from 'typescript-fsa';
|
||||
import { AUTHENTICATION_METHOD_PREFERENCE } from '../../constants/browserStorage';
|
||||
import { setLoginPreferenceAction } from '../actions';
|
||||
|
||||
export function* setLoginPreferenceSaga(action: Action<string>) {
|
||||
localStorage.setItem(AUTHENTICATION_METHOD_PREFERENCE, action.payload);
|
||||
yield put(setLoginPreferenceAction.done({params: action.payload}));
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export enum AuthenticationMethodPreference {
|
||||
AzureAD = 'AzureAD',
|
||||
ConnectionString = 'ConnectionString'
|
||||
}
|
||||
|
||||
export interface AuthenticationStateInterface {
|
||||
formState: 'initialized' | 'working' | 'failed' | 'idle';
|
||||
preference: string;
|
||||
}
|
||||
|
||||
export const getInitialAuthenticationState = (): AuthenticationStateInterface => ({
|
||||
formState: 'initialized',
|
||||
preference: undefined
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConnectionStringCommandBar matches snapshot 1`] = `
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "connectionStrings.addConnectionCommand.ariaLabel",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Add",
|
||||
},
|
||||
"key": "add",
|
||||
"onClick": [MockFunction],
|
||||
"text": "connectionStrings.addConnectionCommand.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "authentication.autheSelection.switchAuthType",
|
||||
"iconProps": Object {
|
||||
"iconName": "NavigateBack",
|
||||
},
|
||||
"key": "switch",
|
||||
"onClick": [Function],
|
||||
"text": "authentication.autheSelection.switchAuthType",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -2,66 +2,36 @@
|
|||
|
||||
exports[`ConnectionStringsView matches snapshot when connection strings present 1`] = `
|
||||
<div>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "connectionStrings.addConnectionCommand.ariaLabel",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Add",
|
||||
},
|
||||
"key": "add",
|
||||
"onClick": [Function],
|
||||
"text": "connectionStrings.addConnectionCommand.label",
|
||||
},
|
||||
]
|
||||
}
|
||||
<ConnectionStringCommandBar
|
||||
onAddConnectionStringClick={[Function]}
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className="connection-strings"
|
||||
>
|
||||
<ConnectionString
|
||||
connectionStringWithExpiry={
|
||||
Object {
|
||||
"connectionString": "connectionString1",
|
||||
"expiration": "Thu, 01 Jan 1970 00:00:00 GMT",
|
||||
}
|
||||
<div
|
||||
className="connection-strings"
|
||||
>
|
||||
<ConnectionString
|
||||
connectionStringWithExpiry={
|
||||
Object {
|
||||
"connectionString": "connectionString1",
|
||||
"expiration": "Thu, 01 Jan 1970 00:00:00 GMT",
|
||||
}
|
||||
key="connectionString1"
|
||||
onDeleteConnectionString={[Function]}
|
||||
onEditConnectionString={[Function]}
|
||||
onSelectConnectionString={[Function]}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
key="connectionString1"
|
||||
onDeleteConnectionString={[Function]}
|
||||
onEditConnectionString={[Function]}
|
||||
onSelectConnectionString={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConnectionStringsView matches snapshot when no connection strings 1`] = `
|
||||
<div>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "connectionStrings.addConnectionCommand.ariaLabel",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Add",
|
||||
},
|
||||
"key": "add",
|
||||
"onClick": [Function],
|
||||
"text": "connectionStrings.addConnectionCommand.label",
|
||||
},
|
||||
]
|
||||
}
|
||||
<ConnectionStringCommandBar
|
||||
onAddConnectionStringClick={[Function]}
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className="connection-strings"
|
||||
/>
|
||||
<ConnectionStringsEmpty />
|
||||
</div>
|
||||
<div
|
||||
className="connection-strings"
|
||||
/>
|
||||
<ConnectionStringsEmpty />
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { CommandBar } from '@fluentui/react';
|
||||
import { ConnectionStringCommandBar } from './commandBar';
|
||||
import { connectionStringsStateInitial } from '../state';
|
||||
import * as connectionStringContext from '../context/connectionStringStateContext';
|
||||
import * as authenticationStateContext from '../../authentication/context/authenticationStateContext';
|
||||
import { getInitialAuthenticationState } from '../../authentication/state';
|
||||
|
||||
describe('ConnectionStringCommandBar', () => {
|
||||
it('matches snapshot', () => {
|
||||
jest.spyOn(connectionStringContext, 'useConnectionStringContext').mockReturnValue(
|
||||
[connectionStringsStateInitial(), connectionStringContext.getInitialConnectionStringOps()]);
|
||||
const wrapper = shallow(<ConnectionStringCommandBar onAddConnectionStringClick={jest.fn()}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
it('upserts when edit view applied', () => {
|
||||
const setLoginPreference = jest.fn();
|
||||
jest.spyOn(authenticationStateContext, 'useAuthenticationStateContext').mockReturnValue(
|
||||
[getInitialAuthenticationState(), {...authenticationStateContext.getInitialAuthenticationOps(), setLoginPreference}]);
|
||||
const wrapper = mount(<ConnectionStringCommandBar onAddConnectionStringClick={jest.fn()}/>);
|
||||
|
||||
wrapper.find(CommandBar).props().items[1].onClick(undefined);
|
||||
wrapper.update();
|
||||
|
||||
expect(setLoginPreference).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CommandBar } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { useConnectionStringContext } from '../context/connectionStringStateContext';
|
||||
import { CONNECTION_STRING_LIST_MAX_LENGTH } from '../../constants/browserStorage';
|
||||
import { ADD, NAVIGATE_BACK } from '../../constants/iconNames';
|
||||
import { useAuthenticationStateContext } from '../../authentication/context/authenticationStateContext';
|
||||
|
||||
interface ConnectionStringCommandBarProps {
|
||||
onAddConnectionStringClick: () => void;
|
||||
}
|
||||
|
||||
export const ConnectionStringCommandBar: React.FC<ConnectionStringCommandBarProps> = props => {
|
||||
const { t } = useTranslation();
|
||||
const [ state, ] = useConnectionStringContext();
|
||||
const [ , { setLoginPreference } ] = useAuthenticationStateContext();
|
||||
|
||||
const switchAuth = () => {
|
||||
setLoginPreference('');
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandBar
|
||||
items={[
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.connectionStrings.addConnectionCommand.ariaLabel),
|
||||
disabled: state.payload.length >= CONNECTION_STRING_LIST_MAX_LENGTH,
|
||||
iconProps: { iconName: ADD },
|
||||
key: 'add',
|
||||
onClick: props.onAddConnectionStringClick,
|
||||
text: t(ResourceKeys.connectionStrings.addConnectionCommand.label)
|
||||
},
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.authentication.autheSelection.switchAuthType),
|
||||
iconProps: { iconName: NAVIGATE_BACK },
|
||||
key: 'switch',
|
||||
onClick: switchAuth,
|
||||
text: t(ResourceKeys.authentication.autheSelection.switchAuthType)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,14 +5,12 @@
|
|||
import * as React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { CommandBar } from '@fluentui/react';
|
||||
import { ConnectionStringsView } from './connectionStringsView';
|
||||
import { ConnectionString } from './connectionString';
|
||||
import { ConnectionStringEditView } from './connectionStringEditView';
|
||||
import * as AsyncSagaReducer from '../../shared/hooks/useAsyncSagaReducer';
|
||||
import { connectionStringsStateInitial } from '../state';
|
||||
import { deleteConnectionStringAction, upsertConnectionStringAction } from '../actions';
|
||||
import * as HubConnectionStringHelper from '../../shared/utils/hubConnectionStringHelper';
|
||||
import * as connectionStringContext from '../context/connectionStringStateContext';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: jest.fn() }),
|
||||
|
@ -23,67 +21,28 @@ describe('ConnectionStringsView', () => {
|
|||
const connectionStringWithExpiry = {connectionString: 'connectionString1', expiration: (new Date(0)).toUTCString()};
|
||||
|
||||
it('matches snapshot when no connection strings', () => {
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([connectionStringsStateInitial(), jest.fn()]);
|
||||
jest.spyOn(connectionStringContext, 'useConnectionStringContext').mockReturnValue(
|
||||
[connectionStringsStateInitial(), connectionStringContext.getInitialConnectionStringOps()]);
|
||||
const wrapper = shallow(<ConnectionStringsView/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when connection strings present', () => {
|
||||
const state = connectionStringsStateInitial().merge({ payload: [connectionStringWithExpiry]});
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([state, jest.fn()]);
|
||||
jest.spyOn(connectionStringContext, 'useConnectionStringContext').mockReturnValue(
|
||||
[state, connectionStringContext.getInitialConnectionStringOps()]);
|
||||
|
||||
const wrapper = shallow(<ConnectionStringsView/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
});
|
||||
|
||||
describe('add scenario', () => {
|
||||
it('mounts edit view when add command clicked', () => {
|
||||
const state = connectionStringsStateInitial().merge({ payload: []});
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([state, jest.fn()]);
|
||||
const wrapper = shallow(<ConnectionStringsView/>);
|
||||
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
|
||||
|
||||
act(() => {
|
||||
wrapper.find(CommandBar).props().items[0].onClick(undefined);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find(ConnectionStringEditView).length).toEqual(1);
|
||||
|
||||
act(() => wrapper.find(ConnectionStringEditView).first().props().onDismiss());
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
|
||||
});
|
||||
|
||||
it('upserts when edit view applied', () => {
|
||||
const upsertConnectionStringActionSpy = jest.spyOn(upsertConnectionStringAction, 'started');
|
||||
const state = connectionStringsStateInitial().merge({ payload: []});
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([state, jest.fn()]);
|
||||
jest.spyOn(HubConnectionStringHelper, 'getExpiryDateInUtcString').mockReturnValue((new Date(0)).toUTCString());
|
||||
|
||||
const wrapper = shallow(<ConnectionStringsView/>);
|
||||
act(() => {
|
||||
wrapper.find(CommandBar).props().items[0].onClick(undefined);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
const connectionStringEditView = wrapper.find(ConnectionStringEditView).first();
|
||||
|
||||
act(() => connectionStringEditView.props().onCommit('newConnectionString'));
|
||||
wrapper.update();
|
||||
|
||||
expect(upsertConnectionStringActionSpy).toHaveBeenCalledWith({ connectionString: 'newConnectionString', expiration: (new Date(0)).toUTCString() });
|
||||
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit scenario', () => {
|
||||
const connectionString = 'HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key';
|
||||
it('mounts edit view when add command clicked', () => {
|
||||
const state = connectionStringsStateInitial().merge({ payload: [connectionStringWithExpiry] });
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([state, jest.fn()]);
|
||||
jest.spyOn(connectionStringContext, 'useConnectionStringContext').mockReturnValue(
|
||||
[state, connectionStringContext.getInitialConnectionStringOps()]);
|
||||
const wrapper = mount(<ConnectionStringsView/>);
|
||||
|
||||
act(() => wrapper.find(ConnectionString).props().onEditConnectionString(connectionString));
|
||||
|
@ -93,10 +52,11 @@ describe('ConnectionStringsView', () => {
|
|||
});
|
||||
|
||||
it('upserts when edit view applied', () => {
|
||||
const upsertConnectionStringActionSpy = jest.spyOn(upsertConnectionStringAction, 'started');
|
||||
const deleteConnectionStringActionSpy = jest.spyOn(deleteConnectionStringAction, 'started');
|
||||
const deleteConnectionString = jest.fn();
|
||||
const upsertConnectionString = jest.fn();
|
||||
const state = connectionStringsStateInitial().merge({ payload: [connectionStringWithExpiry] });
|
||||
jest.spyOn(AsyncSagaReducer, 'useAsyncSagaReducer').mockReturnValue([state, jest.fn()]);
|
||||
jest.spyOn(connectionStringContext, 'useConnectionStringContext').mockReturnValue(
|
||||
[state, {...connectionStringContext.getInitialConnectionStringOps(), deleteConnectionString, upsertConnectionString}]);
|
||||
jest.spyOn(HubConnectionStringHelper, 'getExpiryDateInUtcString').mockReturnValue((new Date(0)).toUTCString());
|
||||
const wrapper = mount(<ConnectionStringsView/>);
|
||||
|
||||
|
@ -107,8 +67,8 @@ describe('ConnectionStringsView', () => {
|
|||
act(() => connectionStringEditView.props().onCommit('newConnectionString'));
|
||||
wrapper.update();
|
||||
|
||||
expect(deleteConnectionStringActionSpy).toHaveBeenCalledWith(connectionString);
|
||||
expect(upsertConnectionStringActionSpy).toHaveBeenCalledWith({ connectionString: 'newConnectionString', expiration: (new Date(0)).toUTCString() });
|
||||
expect(deleteConnectionString).toHaveBeenCalledWith(connectionString);
|
||||
expect(upsertConnectionString).toHaveBeenCalledWith({ connectionString: 'newConnectionString', expiration: (new Date(0)).toUTCString() });
|
||||
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,61 +5,55 @@
|
|||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CommandBar } from '@fluentui/react';
|
||||
import { ConnectionString } from './connectionString';
|
||||
import { ConnectionStringEditView } from './connectionStringEditView';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { CONNECTION_STRING_LIST_MAX_LENGTH } from '../../constants/browserStorage';
|
||||
import { upsertConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction } from '../actions';
|
||||
import { ROUTE_PARTS } from '../../constants/routes';
|
||||
import { formatConnectionStrings, getExpiryDateInUtcString } from '../../shared/utils/hubConnectionStringHelper';
|
||||
import { ConnectionStringsEmpty } from './connectionStringsEmpty';
|
||||
import { useAsyncSagaReducer } from '../../shared/hooks/useAsyncSagaReducer';
|
||||
import { connectionStringsReducer } from '../reducer';
|
||||
import { connectionStringsSaga } from '../sagas';
|
||||
import { connectionStringsStateInitial, ConnectionStringWithExpiry } from '../state';
|
||||
import { ConnectionStringWithExpiry } from '../state';
|
||||
import { SynchronizationStatus } from '../../api/models/synchronizationStatus';
|
||||
import { MultiLineShimmer } from '../../shared/components/multiLineShimmer';
|
||||
import { getConnectionInfoFromConnectionString } from '../../api/shared/utils';
|
||||
import { getConnectionStringsAction } from './../actions';
|
||||
import { useBreadcrumbEntry } from '../../navigation/hooks/useBreadcrumbEntry';
|
||||
import { AppInsightsClient } from '../../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_PAGE_NAMES } from '../../constants/telemetry';
|
||||
import { useConnectionStringContext } from '../context/connectionStringStateContext';
|
||||
import { ConnectionStringCommandBar } from './commandBar';
|
||||
import '../../css/_layouts.scss';
|
||||
import './connectionStringsView.scss';
|
||||
import { AppInsightsClient } from '../../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_PAGE_NAMES } from '../../../app/constants/telemetry';
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const ConnectionStringsView: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
useBreadcrumbEntry({name: t(ResourceKeys.breadcrumb.resources)});
|
||||
|
||||
const [ localState, dispatch ] = useAsyncSagaReducer(connectionStringsReducer, connectionStringsSaga, connectionStringsStateInitial(), 'connectionStringsState');
|
||||
const [ state, api ] = useConnectionStringContext();
|
||||
const [ connectionStringUnderEdit, setConnectionStringUnderEdit ] = React.useState<string>(undefined);
|
||||
|
||||
const connectionStringsWithExpiry = localState.payload;
|
||||
const synchronizationStatus = localState.synchronizationStatus;
|
||||
const connectionStringsWithExpiry = state.payload;
|
||||
const synchronizationStatus = state.synchronizationStatus;
|
||||
|
||||
const onUpsertConnectionString = (newConnectionString: string, connectionString: string) => {
|
||||
if (newConnectionString !== connectionString) {
|
||||
// replacing a connection string with new connection string
|
||||
dispatch(deleteConnectionStringAction.started(connectionString));
|
||||
api.deleteConnectionString(connectionString);
|
||||
}
|
||||
|
||||
const stringWithExpiry: ConnectionStringWithExpiry = {
|
||||
connectionString: newConnectionString,
|
||||
expiration: getExpiryDateInUtcString()
|
||||
};
|
||||
dispatch(upsertConnectionStringAction.started(stringWithExpiry));
|
||||
api.upsertConnectionString(stringWithExpiry);
|
||||
};
|
||||
|
||||
const onDeleteConnectionString = (connectionString: string) => {
|
||||
dispatch(deleteConnectionStringAction.started(connectionString));
|
||||
api.deleteConnectionString(connectionString);
|
||||
};
|
||||
|
||||
const onSelectConnectionString = (connectionString: string) => {
|
||||
const updatedConnectionStrings = formatConnectionStrings(connectionStringsWithExpiry, connectionString);
|
||||
dispatch(setConnectionStringsAction.started(updatedConnectionStrings));
|
||||
api.setConnectionStrings(updatedConnectionStrings);
|
||||
};
|
||||
|
||||
const onAddConnectionStringClick = () => {
|
||||
|
@ -80,7 +74,7 @@ export const ConnectionStringsView: React.FC = () => {
|
|||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(getConnectionStringsAction.started());
|
||||
api.getConnectionStrings();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -102,36 +96,21 @@ export const ConnectionStringsView: React.FC = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<CommandBar
|
||||
items={[
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.connectionStrings.addConnectionCommand.ariaLabel),
|
||||
disabled: connectionStringsWithExpiry.length >= CONNECTION_STRING_LIST_MAX_LENGTH,
|
||||
iconProps: { iconName: 'Add' },
|
||||
key: 'add',
|
||||
onClick: onAddConnectionStringClick,
|
||||
text: t(ResourceKeys.connectionStrings.addConnectionCommand.label)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<div>
|
||||
<div className="connection-strings">
|
||||
{connectionStringsWithExpiry.map(connectionStringWithExpiry =>
|
||||
<ConnectionString
|
||||
key={connectionStringWithExpiry.connectionString}
|
||||
connectionStringWithExpiry={connectionStringWithExpiry}
|
||||
onEditConnectionString={onEditConnectionStringClick}
|
||||
onDeleteConnectionString={onDeleteConnectionString}
|
||||
onSelectConnectionString={onSelectConnectionString}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(!connectionStringsWithExpiry || connectionStringsWithExpiry.length === 0) &&
|
||||
<ConnectionStringsEmpty/>
|
||||
}
|
||||
|
||||
<ConnectionStringCommandBar onAddConnectionStringClick={onAddConnectionStringClick}/>
|
||||
<div className="connection-strings">
|
||||
{connectionStringsWithExpiry.map(connectionStringWithExpiry =>
|
||||
<ConnectionString
|
||||
key={connectionStringWithExpiry.connectionString}
|
||||
connectionStringWithExpiry={connectionStringWithExpiry}
|
||||
onEditConnectionString={onEditConnectionStringClick}
|
||||
onDeleteConnectionString={onDeleteConnectionString}
|
||||
onSelectConnectionString={onSelectConnectionString}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(!connectionStringsWithExpiry || connectionStringsWithExpiry.length === 0) &&
|
||||
<ConnectionStringsEmpty/>
|
||||
}
|
||||
{connectionStringUnderEdit !== undefined &&
|
||||
<ConnectionStringEditView
|
||||
connectionStringUnderEdit={connectionStringUnderEdit}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react';
|
||||
import { connectionStringsStateInitial, ConnectionStringsStateType } from '../state';
|
||||
import { ConnectionStringInterface } from './connectionStringStateProvider';
|
||||
|
||||
export const getInitialConnectionStringOps = (): ConnectionStringInterface => ({
|
||||
deleteConnectionString: () => undefined,
|
||||
getConnectionStrings: () => undefined,
|
||||
setConnectionStrings: () => undefined,
|
||||
upsertConnectionString: () => undefined,
|
||||
});
|
||||
|
||||
export const ConnectionStringStateContext = React.createContext<[ConnectionStringsStateType, ConnectionStringInterface]>
|
||||
([
|
||||
connectionStringsStateInitial(),
|
||||
getInitialConnectionStringOps()
|
||||
]);
|
||||
export const useConnectionStringContext = () => React.useContext(ConnectionStringStateContext);
|
|
@ -0,0 +1,31 @@
|
|||
import * as React from 'react';
|
||||
import { connectionStringsStateInitial, ConnectionStringWithExpiry } from '../state';
|
||||
import { connectionStringsReducer } from '../reducer';
|
||||
import { connectionStringsSaga } from '../sagas';
|
||||
import { useAsyncSagaReducer } from '../../shared/hooks/useAsyncSagaReducer';
|
||||
import { ConnectionStringStateContext } from './connectionStringStateContext';
|
||||
import { deleteConnectionStringAction, getConnectionStringsAction, setConnectionStringsAction, upsertConnectionStringAction } from '../actions';
|
||||
|
||||
export interface ConnectionStringInterface {
|
||||
getConnectionStrings(): void;
|
||||
setConnectionStrings(connectionStringInfoList: ConnectionStringWithExpiry[]): void;
|
||||
upsertConnectionString(connectionStringInfo: ConnectionStringWithExpiry): void;
|
||||
deleteConnectionString(connectionString: string): void;
|
||||
}
|
||||
|
||||
export const ConnectionStringStateContextProvider: React.FC = props => {
|
||||
const [ state, dispatch ] = useAsyncSagaReducer(connectionStringsReducer, connectionStringsSaga, connectionStringsStateInitial(), 'connectionStringsState');
|
||||
|
||||
const connectionStringApi: ConnectionStringInterface = {
|
||||
deleteConnectionString: (connectionString: string) => dispatch(deleteConnectionStringAction.started(connectionString)),
|
||||
getConnectionStrings: () => dispatch(getConnectionStringsAction.started()),
|
||||
setConnectionStrings: (connectionStringInfoList: ConnectionStringWithExpiry[]) => dispatch(setConnectionStringsAction.started(connectionStringInfoList)),
|
||||
upsertConnectionString: (connectionStringInfo: ConnectionStringWithExpiry) => dispatch(upsertConnectionStringAction.started(connectionStringInfo)),
|
||||
};
|
||||
|
||||
return (
|
||||
<ConnectionStringStateContext.Provider value={[state, connectionStringApi]}>
|
||||
{props.children}
|
||||
</ConnectionStringStateContext.Provider>
|
||||
);
|
||||
};
|
|
@ -7,7 +7,7 @@ import { SagaIterator } from 'redux-saga';
|
|||
import { NotificationType } from '../../api/models/notification';
|
||||
import { raiseNotificationToast } from '../../notifications/components/notificationToast';
|
||||
import { CONNECTION_STRING_NAME_LIST } from '../../constants/browserStorage';
|
||||
import { getConnectionStringsAction } from './../actions';
|
||||
import { getConnectionStringsAction } from '../actions';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { ConnectionStringWithExpiry } from '../state';
|
||||
import { setConnectionStrings } from './setConnectionStringsSaga';
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче