Pnp summer refresh (#335)
* Local folder (#246) * enable read from local folder and add tests * electron only for local files * make route hiden if interface is not found * remove focus if value is filled * validate model if not retrieved from model repo; moved constants around (#247) * use @id to match dtdl not names (#250) * use @id to match dtdl not names * update strings * New discovery (#249) * discover end2e * add tests * add bit more tests and some old discovery mech cleanup * address comments wave1 * Implement dt get and add tests (#255) * implement dt get and add tests * update dt value type * Folder picker (#254) * workflow e2e * add tests * address comments * add a resouce key back * Patch dt (#256) * implement dt get and add tests * update dt value type * implement dt patch and tests * address comment * update per telemetry service update; fix property styles (#258) * Rkessler/semantic units (#259) Add Semantic units. * fix a build break (#261) * Rkessler/rpos (#262) Remove private repo. * Update json schema adaptor; integrate service update for json patch (#260) * remove tooltip from json schema form; fix map in object data converter * remove dataform refactor * integrate latest service change * Allow remove of public repo. (#263) * remove model on device; remove query on pnp info (#268) * fix command api path; recomment out validation before the api is ready (#273) * fix command api path; recomment out validation before the api is ready * update api endpoint * Reusable schema (#266) * implement reusable schema and add test * add mock data file * merge master in (#276) * Rkessler/refactor settings (#281) * replay (minus test fixes) * snapshot and test updates. * Clarify discard message. * Address comments. * Fix number of value 0 was not showing (#278) * allow showing number of value 0 * turn off validation when value is 0 for numbers and intergers * make validation less hacky * improve text, comment out things, version update (#283) * Rkessler/notify repo change (#285) * Cosmetic changes and add notification. * Test enhancements. * boost test coverage. * raise test coverage. * comment tune up. * update product name; added privacy statement (#286) * update readme with new images (#284) * render telemetry row with empty body (#287) * render telemetry row with empty body * inform users which files are invalid * address comments * Default component (#289) * support default component in ui * show default component's telemetry * address a few bugs opened (#288) * moving to React 16.13 and Router 5 (#290) * moving to fiber * fiber tests * removing unused imports * addressed review comments * addressed review comments * bug fix * fix telemetry issues Co-authored-by: yingxue <kalian1127@gmail.com> * css and null check (#291) * moving AddDevice to react-async-saga-reducer (#292) * moving AddDevice to react-async-saga-reducer * simplifying notification * addressed review comments * reduxless device list (#293) * Devicetwin refactor (#295) * initial commit * update test * rename folder * Direct method and device events (#296) * refactor direct method * refactor telemetry page * explicit import * reduxless module and cloud to device message (#294) * reduxless cloud to device message * reduxless module identity * rebasing with master * addressed review comments * moving on - goodbye redux (#302) * reduxless pnp * update tests * fix model definition * fix typo * move device identity and related to be reduxless * update tests * reduxless * update tests * fix telemetry and update tests * fix tests Co-authored-by: yingxue <kalian1127@gmail.com> * remove redux, reselect; Note: azureResource and iotHub folder is removed in this commit, when need to bring them back, this is the commit to refer to (#303) * Folder rename (#306) * remove device content folder * remove default exports * update tests * optimizing office-ui-fabric imports (#307) * optimizing office-ui-fabric imports * removing unused code * Localization (#308) * remove localization context * update test * replacing monaco with ace editor (#309) * Remove moment (#311) * remove momentjs and fix bugs and warnings * fix bugs and tests * use exact version * fix bug when switch hubs and others (#313) * moving to brace editor for MIT license (#314) * rebase from master * support root level component; fix the bug in connetion string area (#316) * update dataplane api version (#317) * add missing css style (#318) * add missing css style * exclude button click from header click event * debug console for useReactAsyncReducer (#319) * remove copy to clipboard for preview (#320) * remove dataplane parameter and active connection string (#321) * remove dataplane parameter and active connection string * address comment * state separation (#322) * remove replace op for dt patch (#323) * Fixe bugs from 0706 triage meeting (#325) * fix default telemetry filtering; command response validation; writable property ack validation * enable edit on model location input box * bug bash fixes round 1 (#327) * add digital twin view; remove warning message; show unsupported type of dtdl; allow direct method to accept non-json payload; disallow dataplane response without status code * fix notificaiton effect * event use state refactor; validation fixes for map type; checkbox false value (#329) * add a few more null check (#330) * some css debt paid (#331) * Merge telemetry (#334) * json patch don't like null being the value * add regex check for map key * merge device events pages and make system property toggle in realtime * css update for events; using pivot instead of custome * rebase from master and update readme * mock local time response Co-authored-by: chieftn <rkessler@microsoft.com> Co-authored-by: asrudra <33266774+asrudra@users.noreply.github.com>
|
@ -221,7 +221,7 @@ ClientBin/
|
|||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
|
@ -317,7 +317,7 @@ __pycache__/
|
|||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
|
@ -326,7 +326,7 @@ ASALocalRun/
|
|||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
node_modules/
|
||||
dist/
|
||||
|
@ -338,7 +338,7 @@ dist/
|
|||
coverage/
|
||||
|
||||
# ts complied scripts
|
||||
scripts/composeLocalizationKeys.js
|
||||
scripts/*.js
|
||||
|
||||
# test results
|
||||
jest-test-results.trx
|
||||
|
|
37
README.md
|
@ -1,15 +1,15 @@
|
|||
|
||||
# Azure IoT explorer (preview)
|
||||
# Azure IoT Explorer (preview)
|
||||
|
||||
[![Build Status](https://dev.azure.com/azure/azure-iot-explorer/_apis/build/status/Azure%20IoT%20Explorer%20CI%20Pipeline?branchName=master)](https://dev.azure.com/azure/azure-iot-explorer/_build/latest?definitionId=31&branchName=master)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Azure IoT explorer](#getting-azure-iot-explorer)
|
||||
- [Getting Azure IoT Explorer](#getting-azure-iot-explorer)
|
||||
- [Features](#features)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Getting Azure IoT explorer
|
||||
## Getting Azure IoT Explorer
|
||||
|
||||
You can either download a pre-built version or build it yourself.
|
||||
|
||||
|
@ -33,36 +33,39 @@ If you'd like to package the app yourself, please refer to the [FAQ](https://git
|
|||
|
||||
### Configure an IoT Hub connection
|
||||
|
||||
- After opening the application, add the connection string for your IoT Hub, then click **Connect**.
|
||||
- Upon opening the application, add the connection string of your IoT hub. You can add multiple strings, view, update or detete them anytime by returning to Home.
|
||||
|
||||
<img src="doc/screenshots/login.PNG" alt="login" width="800"/>
|
||||
<img src="doc/screenRecords/login.gif" alt="login" width="800"/>
|
||||
|
||||
### Manage devices
|
||||
### Device CRUD
|
||||
|
||||
- Click **New** to create a new device.
|
||||
- Select device(s) and click **Delete** to delete device(s). Multiple devices can be selected by clicking while dragging the mouse.
|
||||
- Devices can by queried by typing the first few characters of a device name in the query box.
|
||||
|
||||
<img src="doc/screenshots/manage_devices.PNG" alt="manage_devices" width="800"/>
|
||||
<img src="doc/screenRecords/create_device.gif" alt="create_device" width="800"/>
|
||||
|
||||
### Device functionalities
|
||||
- Click on the device name to see the device details and interact with the device.
|
||||
- Check out the [list of features that we support](https://github.com/Azure/azure-iot-explorer/wiki)
|
||||
|
||||
<img src="doc/screenshots/device_details.PNG" alt="device_details" width="800"/>
|
||||
<img src="doc/screenRecords/device_features.gif" alt="device_details" width="800"/>
|
||||
|
||||
### Manage Plug and Play devices
|
||||
### Plug and Play Preview
|
||||
|
||||
- Open the **Settings** panel to configure how PnP Model definitions can be resolved. For more information on PnP devices, please visit [Microsoft Docs](https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play).
|
||||
**If you are looking for a UI tool to get a flavor of Plug and Play, look no futher. Follow this [Microsoft Docs](https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play) to get started.**
|
||||
- Once your device has gone through discovery, **IoT Plug and Play components** page would be available on device details view.
|
||||
- The model ID would be shown.
|
||||
- Follow our guidance to set up how we can retrieve model definitions. If it is already setup, We will inform you where are we resolving your model defintions from.
|
||||
- A table would show the list of components implemented by the device and the corresponding interfaces the components conform to.
|
||||
- You can go back to Home (either from device or by directly clicking the breadcrum) to change how we resolve model definitions. Note this is a global setting which would affect across the hub.
|
||||
|
||||
<img src="doc/screenshots/settings.PNG" alt="settings" width="400"/>
|
||||
|
||||
- Go to the device details page by clicking the name of a PnP device.
|
||||
- Click Plug and Play from the navigation. If the device is a Plug and Play device, the Device capability model ID would be shown. A table would show the list of components implemented by the device and the corresponding interfaces the components conform to.
|
||||
|
||||
<img src="doc/screenshots/pnp_interfaces.PNG" alt="settings" width="800"/>
|
||||
<img src="doc/screenRecords/pnp_discovery.gif" alt="pnp_discovery" width="800"/>
|
||||
|
||||
- Click the name of any component, and switch between interface, properties, commands and telemetry to start interacting with the PnP device.
|
||||
|
||||
<img src="doc/screenshots/pnp_device_details.PNG" alt="pnp_device_details" width="800"/>
|
||||
<img src="doc/screenRecords/pnp_interaction_property.gif" alt="pnp_interaction_property" width="800"/>
|
||||
<img src="doc/screenRecords/pnp_interaction_telemetry.gif" alt="pnp_interaction_telemetry" width="800"/>
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
После Ширина: | Высота: | Размер: 1.3 MiB |
После Ширина: | Высота: | Размер: 413 KiB |
После Ширина: | Высота: | Размер: 1.2 MiB |
После Ширина: | Высота: | Размер: 1.1 MiB |
После Ширина: | Высота: | Размер: 728 KiB |
После Ширина: | Высота: | Размер: 1.4 MiB |
После Ширина: | Высота: | Размер: 694 KiB |
Двоичные данные
doc/screenshots/device_details.PNG
До Ширина: | Высота: | Размер: 34 KiB |
Двоичные данные
doc/screenshots/login.PNG
До Ширина: | Высота: | Размер: 41 KiB |
Двоичные данные
doc/screenshots/manage_devices.PNG
До Ширина: | Высота: | Размер: 40 KiB |
Двоичные данные
doc/screenshots/pnp_device_details.PNG
До Ширина: | Высота: | Размер: 58 KiB |
Двоичные данные
doc/screenshots/pnp_interfaces.PNG
До Ширина: | Высота: | Размер: 54 KiB |
Двоичные данные
doc/screenshots/settings.PNG
До Ширина: | Высота: | Размер: 71 KiB |
11
jestSetup.ts
|
@ -2,10 +2,11 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { setIconOptions } from "office-ui-fabric-react/lib/Styling";
|
||||
import * as Enzyme from "enzyme";
|
||||
import * as Adapter from "enzyme-adapter-react-16";
|
||||
import { setIconOptions } from 'office-ui-fabric-react/lib/Styling';
|
||||
import * as Enzyme from 'enzyme';
|
||||
import * as Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
// tslint:disable-next-line: no-string-literal
|
||||
global['Headers'] = () => {};
|
||||
window.parent.fetch = jest.fn();
|
||||
|
||||
|
@ -22,6 +23,6 @@ Object.defineProperty(global, 'Node', {
|
|||
value: {firstElementChild: jest.fn()}
|
||||
});
|
||||
|
||||
jest.mock('i18next', () => ({
|
||||
t: (key: string) => key
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({t: key => key})
|
||||
}));
|
||||
|
|
41
package.json
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "azure-iot-explorer",
|
||||
"version": "0.10.19",
|
||||
"version": "0.11.1",
|
||||
"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": "public/electron.js",
|
||||
"build": {
|
||||
"appId": "com.microsoft.azure.iot.pnp.ui",
|
||||
"productName": "Azure IoT explorer (preview)",
|
||||
"productName": "Azure IoT Explorer (preview)",
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"package.json",
|
||||
|
@ -41,6 +41,7 @@
|
|||
"clean": "IF EXIST .\\dist RMDIR /Q /S .\\dist",
|
||||
"clean:linux": "rm --recursive -f ./dist",
|
||||
"localization": "tsc ./scripts/composeLocalizationKeys.ts --skipLibCheck && node ./scripts/composeLocalizationKeys.js",
|
||||
"import-semantic-units": "tsc ./scripts/importSemanticUnitTypes.ts --skipLibCheck && node ./scripts/importSemanticUnitTypes.js",
|
||||
"docker": "docker pull electronuserland/electron-builder && docker run --rm -ti --mount source=$(pwd),target=/project,type=bind electronuserland/electron-builder:latest",
|
||||
"electron": "electron .",
|
||||
"electron:compile": "tsc ./public/electron.ts --skipLibCheck --lib es2015 --inlineSourceMap",
|
||||
|
@ -70,39 +71,29 @@
|
|||
"homepage": "https://github.com/Azure/azure-iot-explorer#readme",
|
||||
"dependencies": {
|
||||
"@azure/event-hubs": "1.0.7",
|
||||
"@types/core-js": "2.5.0",
|
||||
"@types/semver": "6.0.2",
|
||||
"@types/uuid": "3.4.5",
|
||||
"azure-iot-common": "1.10.3",
|
||||
"azure-iothub": "1.8.1",
|
||||
"body-parser": "1.18.3",
|
||||
"core-js": "3.0.0",
|
||||
"brace": "0.11.1",
|
||||
"cors": "2.8.5",
|
||||
"date-fns": "2.14.0",
|
||||
"express": "4.16.4",
|
||||
"i18next": "11.10.1",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"jsonschema": "1.2.4",
|
||||
"moment": "2.24.0",
|
||||
"monaco-editor": "0.15.1",
|
||||
"msal": "1.2.0",
|
||||
"office-ui-fabric-core": "10.1.0",
|
||||
"office-ui-fabric-react": "7.67.0",
|
||||
"react": "16.8.6",
|
||||
"react": "16.13.1",
|
||||
"react-collapsible": "2.3.2",
|
||||
"react-dom": "16.8.6",
|
||||
"react-dom": "16.13.1",
|
||||
"react-i18next": "11.5.0",
|
||||
"react-infinite-scroller": "1.2.2",
|
||||
"react-jsonschema-form": "1.7.0",
|
||||
"react-monaco-editor": "0.30.1",
|
||||
"react-redux": "7.1.3",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-smooth-dnd": "0.11.0",
|
||||
"react-toastify": "4.4.0",
|
||||
"redux": "4.0.4",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "0.16.2",
|
||||
"redux-saga": "1.1.3",
|
||||
"request": "2.88.0",
|
||||
"reselect": "4.0.0",
|
||||
"semver": "6.3.0",
|
||||
"typescript-fsa": "3.0.0-beta-2",
|
||||
"typescript-fsa-reducers": "1.0.0",
|
||||
|
@ -113,7 +104,9 @@
|
|||
"mkdirp": "0.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redux-saga/testing-utils": "1.1.3",
|
||||
"@types/async-lock": "1.1.0",
|
||||
"@types/core-js": "2.5.0",
|
||||
"@types/cors": "2.8.4",
|
||||
"@types/enzyme": "3.10.4",
|
||||
"@types/enzyme-adapter-react-16": "1.0.5",
|
||||
|
@ -121,16 +114,14 @@
|
|||
"@types/i18next": "8.4.4",
|
||||
"@types/jest": "24.0.15",
|
||||
"@types/jest-plugin-context": "2.9.0",
|
||||
"@types/react": "16.8.20",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/react-i18next": "7.8.3",
|
||||
"@types/react-infinite-scroller": "1.0.8",
|
||||
"@types/react": "16.9.35",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@types/react-jsonschema-form": "1.0.10",
|
||||
"@types/react-redux": "7.1.5",
|
||||
"@types/react-router-dom": "4.3.1",
|
||||
"@types/react-router-dom": "5.1.5",
|
||||
"@types/react-toastify": "4.0.1",
|
||||
"@types/redux-logger": "3.0.7",
|
||||
"@types/request": "2.48.1",
|
||||
"@types/semver": "6.0.2",
|
||||
"@types/uuid": "3.4.5",
|
||||
"@types/webpack": "4.4.22",
|
||||
"@types/webpack-dev-server": "3.1.7",
|
||||
"@types/webpack-merge": "4.1.5",
|
||||
|
@ -148,7 +139,6 @@
|
|||
"jest-plugin-context": "2.9.0",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"mini-css-extract-plugin": "0.8.0",
|
||||
"monaco-editor-webpack-plugin": "1.7.0",
|
||||
"node-sass": "4.13.1",
|
||||
"nodemon": "2.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
|
@ -199,7 +189,6 @@
|
|||
"moduleNameMapper": {
|
||||
"^office-ui-fabric-react/lib": "<rootDir>/node_modules/office-ui-fabric-react/lib-commonjs",
|
||||
"^.+\\.(scss)$": "<rootDir>/scss-stub.js",
|
||||
"monaco-editor": "<rootDir>/node_modules/react-monaco-editor",
|
||||
".+\\appconfig.ENV.json": "<rootDir>/src/appConfig/appConfig.dev.json"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as fs from 'fs';
|
||||
|
||||
export interface StringMap<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
export interface SemanticUnit {
|
||||
displayName: string | StringMap<string>;
|
||||
abbreviation: string;
|
||||
}
|
||||
|
||||
export const generateSemanticUnitDigest = () => {
|
||||
const rawSemanticUnitsFileLocation = './src/app/shared/units/semanticUnitsListRaw.json';
|
||||
const rawSemanticUnitsFileContents = fs.readFileSync(rawSemanticUnitsFileLocation, 'utf-8');
|
||||
const rawSemanticUnitsFileObject = JSON.parse(rawSemanticUnitsFileContents);
|
||||
|
||||
const semanticUnits: StringMap<SemanticUnit> = {};
|
||||
|
||||
const minDtmiLength = 4;
|
||||
const extensionTypeIndex = 2;
|
||||
const extensionType = 'unit';
|
||||
const unitNameIndex = 3;
|
||||
|
||||
// tslint:disable-next-line: no-any
|
||||
rawSemanticUnitsFileObject['@graph'].forEach((entry: any) => {
|
||||
const dtmi = entry['@id'].split(':');
|
||||
if (dtmi.length >= minDtmiLength && dtmi[extensionTypeIndex].toLowerCase() === extensionType) {
|
||||
const unitName = dtmi[unitNameIndex].split(';')[0];
|
||||
const displayName = entry.displayName;
|
||||
const abbreviation = entry.abbreviation || entry.symbol;
|
||||
|
||||
semanticUnits[unitName] = {
|
||||
abbreviation,
|
||||
displayName
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const semanticUnitsFileLocation = './src/app/shared/units/semanticUnitsList.json';
|
||||
const semanticUnitsFileContents = JSON.stringify(semanticUnits);
|
||||
fs.writeFileSync(semanticUnitsFileLocation, semanticUnitsFileContents);
|
||||
};
|
||||
|
||||
generateSemanticUnitDigest();
|
|
@ -1,20 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export enum HTTP_OPERATION_TYPES {
|
||||
Delete = 'DELETE',
|
||||
Get = 'GET',
|
||||
Patch = 'PATCH',
|
||||
Post = 'POST',
|
||||
Put = 'PUT'
|
||||
}
|
||||
|
||||
export const MILLISECONDS_PER_SECOND = 1000;
|
||||
export const SECONDS_PER_MINUTE = 60;
|
||||
export const APPLICATION_JSON = 'application/json';
|
||||
export const ERROR_TYPES = {
|
||||
AUTHORIZATION_RULE_NOT_FOUND: 'authorizationRuleNotFound',
|
||||
HTTP: 'http',
|
||||
PORT_IS_IN_USE: 'portIsInUse'
|
||||
};
|
|
@ -37,7 +37,7 @@ describe('utils', () => {
|
|||
expect(transformedDevice.cloudToDeviceMessageCount).toEqual(deviceSummary.cloudToDeviceMessageCount);
|
||||
expect(transformedDevice.connectionState).toEqual(deviceSummary.connectionState);
|
||||
expect(transformedDevice.deviceId).toEqual(deviceSummary.deviceId);
|
||||
const isLocalTime = new RegExp(/\d+:\d+:\d+ [AP]M, July 18, 2019/);
|
||||
const isLocalTime = new RegExp(/\d+:\d+:\d+ [AP]M, 07\/18\/2019/);
|
||||
expect(transformedDevice.iotEdge).toBeFalsy();
|
||||
expect(transformedDevice.lastActivityTime.match(isLocalTime)).toBeTruthy();
|
||||
expect(transformedDevice.status).toEqual(deviceSummary.status);
|
||||
|
|
|
@ -9,7 +9,7 @@ describe('transformHelper', () => {
|
|||
|
||||
describe('parseDateTimeString', () => {
|
||||
it('parses date time string', () => {
|
||||
const isLocalTime = new RegExp(/\d+:\d+:\d+ [AP]M, July 18, 2019/);
|
||||
const isLocalTime = new RegExp(/\d+:\d+:\d+ [AP]M, 07\/18\/2019/);
|
||||
expect(parseDateTimeString('2019-07-18T10:01:20.0568390Z').match(isLocalTime)).toBeTruthy();
|
||||
expect(parseDateTimeString('0001-01-01T00:00:00')).toEqual(null);
|
||||
expect(parseDateTimeString('')).toEqual(null);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as moment from 'moment';
|
||||
import { parseISO, format } from 'date-fns';
|
||||
|
||||
export const parseDateTimeString = (dateTimeString: string): string => {
|
||||
|
||||
|
@ -18,5 +18,5 @@ export const parseDateTimeString = (dateTimeString: string): string => {
|
|||
return null;
|
||||
}
|
||||
|
||||
return moment.utc(dateTimeString).local().format('h:mm:ss A, MMMM DD, YYYY');
|
||||
return format(parseISO(dateTimeString), 'h:mm:ss a, MM/dd/yyyy').toString();
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { ERROR_TYPES } from '../constants';
|
||||
import { ERROR_TYPES } from './../../constants/apiConstants';
|
||||
|
||||
export class AuthorizationRuleNotFoundError extends Error {
|
||||
public requiredPermissions: string[];
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export default interface DeviceQuery {
|
||||
export interface DeviceQuery {
|
||||
deviceId: string;
|
||||
clauses: QueryClause[];
|
||||
continuationTokens: string[];
|
||||
|
@ -17,12 +17,8 @@ export interface QueryClause {
|
|||
}
|
||||
|
||||
export enum ParameterType {
|
||||
// non pnp
|
||||
edge = 'capabilities.iotEdge',
|
||||
status = 'status',
|
||||
// pnp
|
||||
capabilityModelId = 'dcm',
|
||||
interfaceId = 'interface',
|
||||
}
|
||||
|
||||
export enum OperationType {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { ERROR_TYPES } from '../constants';
|
||||
import { ERROR_TYPES } from './../../constants/apiConstants';
|
||||
|
||||
export class HttpError extends Error {
|
||||
public httpCode: number;
|
||||
|
|
|
@ -3,24 +3,27 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export interface ParsedJsonSchema {
|
||||
type: string;
|
||||
required: string[];
|
||||
|
||||
additionalProperties?: boolean; // use this props as a workaround to indicate whether parsed property is map type
|
||||
default?: {};
|
||||
definitions?: any; // tslint:disable-line: no-any
|
||||
description?: string;
|
||||
enum?: number[] ;
|
||||
enum?: Array<number | string>;
|
||||
enumNames?: string[];
|
||||
format?: string;
|
||||
items?: any; // tslint:disable-line: no-any
|
||||
pattern?: string;
|
||||
properties?: {};
|
||||
required?: string[];
|
||||
title?: string;
|
||||
type?: string | string[];
|
||||
$ref?: any; // tslint:disable-line: no-any
|
||||
}
|
||||
|
||||
export interface ParsedCommandSchema {
|
||||
description: string;
|
||||
name: string;
|
||||
|
||||
description?: string;
|
||||
requestSchema?: ParsedJsonSchema;
|
||||
responseSchema?: ParsedJsonSchema;
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ export enum MESSAGE_SYSTEM_PROPERTIES {
|
|||
IOTHUB_CONNECTION_AUTH_GENERATION_ID = 'iothub-connection-auth-generation-id',
|
||||
IOTHUB_CONNECTION_AUTH_METHOD = 'iothub-connection-auth-method',
|
||||
IOTHUB_CONNECTION_DEVICE_ID = 'iothub-connection-device-id',
|
||||
IOTHUB_INTERFACE_ID = 'iothub-interface-id',
|
||||
IOTHUB_INTERFACE_NAME = 'iothub-interface-name',
|
||||
IOTHUB_COMPONENT_NAME = 'dt-subject',
|
||||
IOTHUB_INTERFACE_ID = 'dt-dataschema',
|
||||
IOTHUB_MESSAGE_SOURCE = 'iothub-message-source',
|
||||
IOTHUB_ENQUEUED_TIME = 'iothub-enqueuedtime'
|
||||
}
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
import { ModelDefinition } from './modelDefinition';
|
||||
|
||||
export interface PnPModel {
|
||||
createdOn: string;
|
||||
createdDate: string;
|
||||
etag: string;
|
||||
lastUpdated: string;
|
||||
model: ModelDefinition;
|
||||
modelId: string;
|
||||
publisherId: string;
|
||||
|
|
|
@ -7,9 +7,10 @@ export interface ModelDefinition {
|
|||
'@id': string;
|
||||
'@type': string;
|
||||
comment?: string | object;
|
||||
contents: Array<PropertyContent | CommandContent | TelemetryContent>;
|
||||
contents: Array<PropertyContent | CommandContent | TelemetryContent | ComponentContent>;
|
||||
description?: string | object;
|
||||
displayName?: string | object;
|
||||
schemas?: Array<ObjectSchema | MapSchema | EnumSchema>;
|
||||
}
|
||||
|
||||
export interface PropertyContent extends ContentBase {
|
||||
|
@ -27,6 +28,10 @@ export interface TelemetryContent extends ContentBase{
|
|||
schema: string | EnumSchema | ObjectSchema | MapSchema;
|
||||
}
|
||||
|
||||
export interface ComponentContent extends ContentBase{
|
||||
schema: string;
|
||||
}
|
||||
|
||||
interface ContentBase {
|
||||
'@type': string | string[];
|
||||
name: string;
|
||||
|
@ -34,8 +39,7 @@ interface ContentBase {
|
|||
comment?: string | object;
|
||||
description?: string | object;
|
||||
displayName?: string | object;
|
||||
displayUnit?: string;
|
||||
unit?: any; // tslint:disable-line:no-any
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface Schema {
|
||||
|
@ -45,24 +49,35 @@ export interface Schema {
|
|||
description?: string | object;
|
||||
}
|
||||
|
||||
interface EnumValue {
|
||||
displayName: string | object;
|
||||
name: string;
|
||||
enumValue: number | string;
|
||||
}
|
||||
|
||||
export interface EnumSchema {
|
||||
'@type': string;
|
||||
enumValues: Array<{ displayName: string | object, name: string, enumValue: number}>;
|
||||
valueSchema: string;
|
||||
enumValues: EnumValue[];
|
||||
'@id'?: string;
|
||||
}
|
||||
|
||||
export interface ObjectSchema {
|
||||
'@type': string;
|
||||
fields: Schema[];
|
||||
'@id'?: string;
|
||||
}
|
||||
|
||||
export interface MapSchema {
|
||||
'@type': string;
|
||||
mapKey: Schema;
|
||||
mapValue: Schema;
|
||||
'@id'?: string;
|
||||
}
|
||||
|
||||
export enum ContentType{
|
||||
Command = 'command',
|
||||
Property = 'property',
|
||||
Telemetry = 'telemetry'
|
||||
Telemetry = 'telemetry',
|
||||
Component = 'component'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export class ModelDefinitionNotFound extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
this.name = ModelDefinitionNotFound.name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export class ModelDefinitionNotValidJsonError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
this.name = ModelDefinitionNotValidJsonError.name;
|
||||
}
|
||||
}
|
|
@ -7,5 +7,6 @@ import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationType
|
|||
|
||||
export interface ModelDefinitionWithSource {
|
||||
modelDefinition: ModelDefinition;
|
||||
source?: REPOSITORY_LOCATION_TYPE;
|
||||
source: REPOSITORY_LOCATION_TYPE;
|
||||
isModelValid: boolean;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { ERROR_TYPES } from '../constants';
|
||||
import { ERROR_TYPES } from './../../constants/apiConstants';
|
||||
|
||||
export class PortIsInUseError extends Error {
|
||||
constructor() {
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
|
||||
export enum AccessVerificationState {
|
||||
Verifying,
|
||||
Authorized,
|
||||
Unauthorized,
|
||||
Failed
|
||||
export interface StringMap<T> {
|
||||
[key: string]: T;
|
||||
}
|
|
@ -4,31 +4,27 @@
|
|||
**********************************************************/
|
||||
import { Twin } from '../models/device';
|
||||
import { DeviceIdentity } from '../models/deviceIdentity';
|
||||
import DeviceQuery from '../models/deviceQuery';
|
||||
import { DigitalTwinInterfaces } from '../models/digitalTwinModels';
|
||||
import { CloudToDeviceMessageActionParameters, InvokeMethodActionParameters } from '../../devices/deviceContent/actions';
|
||||
import { DeviceQuery } from '../models/deviceQuery';
|
||||
import { InvokeMethodActionParameters } from '../../devices/directMethod/actions';
|
||||
import { CloudToDeviceMessageActionParameters } from '../../devices/cloudToDeviceMessage/actions';
|
||||
|
||||
export interface DataPlaneParameters {
|
||||
connectionString: string;
|
||||
}
|
||||
|
||||
export interface FetchDeviceTwinParameters extends DataPlaneParameters {
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface UpdateDeviceTwinParameters extends FetchDeviceTwinParameters {
|
||||
export interface UpdateDeviceTwinParameters {
|
||||
deviceTwin: Twin;
|
||||
}
|
||||
|
||||
export type InvokeMethodParameters = InvokeMethodActionParameters & DataPlaneParameters;
|
||||
|
||||
export type CloudToDeviceMessageParameters = CloudToDeviceMessageActionParameters & DataPlaneParameters;
|
||||
|
||||
export interface FetchDeviceParameters extends DataPlaneParameters {
|
||||
export interface FetchDeviceTwinParameters {
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface FetchDevicesParameters extends DataPlaneParameters {
|
||||
export type InvokeMethodParameters = InvokeMethodActionParameters;
|
||||
|
||||
export type CloudToDeviceMessageParameters = CloudToDeviceMessageActionParameters;
|
||||
|
||||
export interface FetchDeviceParameters {
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface FetchDevicesParameters {
|
||||
query?: DeviceQuery;
|
||||
}
|
||||
|
||||
|
@ -40,36 +36,46 @@ export interface MonitorEventsParameters {
|
|||
customEventHubConnectionString?: string;
|
||||
hubConnectionString?: string;
|
||||
|
||||
fetchSystemProperties?: boolean;
|
||||
startTime?: Date;
|
||||
}
|
||||
|
||||
export interface DeleteDevicesParameters extends DataPlaneParameters {
|
||||
export interface DeleteDevicesParameters {
|
||||
deviceIds: string[];
|
||||
}
|
||||
|
||||
export interface AddDeviceParameters extends DataPlaneParameters {
|
||||
export interface AddDeviceParameters {
|
||||
deviceIdentity: DeviceIdentity;
|
||||
}
|
||||
|
||||
export interface UpdateDeviceParameters extends DataPlaneParameters {
|
||||
export interface UpdateDeviceParameters {
|
||||
deviceIdentity: DeviceIdentity;
|
||||
}
|
||||
|
||||
export interface FetchDigitalTwinInterfacePropertiesParameters extends DataPlaneParameters {
|
||||
export interface FetchDigitalTwinParameters {
|
||||
digitalTwinId: string;
|
||||
}
|
||||
|
||||
export enum JsonPatchOperation {
|
||||
ADD = 'add',
|
||||
REMOVE = 'remove'
|
||||
}
|
||||
|
||||
export interface PatchDigitalTwinParameters {
|
||||
digitalTwinId: string; // Format of digitalTwinId is DeviceId[~ModuleId]. ModuleId is optional.
|
||||
payload: PatchPayload[];
|
||||
}
|
||||
|
||||
export interface InvokeDigitalTwinInterfaceCommandParameters extends DataPlaneParameters {
|
||||
export interface PatchPayload {
|
||||
op: JsonPatchOperation;
|
||||
path: string;
|
||||
value?: boolean | number | string | object;
|
||||
}
|
||||
|
||||
export interface InvokeDigitalTwinInterfaceCommandParameters {
|
||||
digitalTwinId: string; // Format of digitalTwinId is DeviceId[~ModuleId]. ModuleId is optional.
|
||||
componentName: string;
|
||||
commandName: string;
|
||||
connectTimeoutInSeconds?: number;
|
||||
payload?: any; // tslint:disable-line:no-any
|
||||
payload?: boolean | number | string | object;
|
||||
responseTimeoutInSeconds?: number;
|
||||
}
|
||||
|
||||
export interface PatchDigitalTwinInterfacePropertiesParameters extends DataPlaneParameters {
|
||||
digitalTwinId: string; // Format of digitalTwinId is DeviceId[~ModuleId]. ModuleId is optional.
|
||||
payload: DigitalTwinInterfaces;
|
||||
}
|
||||
|
|
|
@ -3,22 +3,21 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { ModuleIdentity } from '../models/moduleIdentity';
|
||||
import { DataPlaneParameters } from './deviceParameters';
|
||||
|
||||
export interface FetchModuleIdentitiesParameters extends DataPlaneParameters {
|
||||
export interface FetchModuleIdentitiesParameters {
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface AddModuleIdentityParameters extends DataPlaneParameters {
|
||||
export interface AddModuleIdentityParameters {
|
||||
moduleIdentity: ModuleIdentity;
|
||||
}
|
||||
|
||||
export interface ModuleIdentityTwinParameters extends DataPlaneParameters {
|
||||
export interface ModuleIdentityTwinParameters {
|
||||
deviceId: string;
|
||||
moduleId: string;
|
||||
}
|
||||
|
||||
export interface FetchModuleIdentityParameters extends DataPlaneParameters {
|
||||
export interface FetchModuleIdentityParameters {
|
||||
deviceId: string;
|
||||
moduleId: string;
|
||||
}
|
||||
|
|
|
@ -2,26 +2,8 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { MetaModelType } from '../models/metamodelMetadata';
|
||||
|
||||
export interface RepoParametersBase {
|
||||
repoServiceHostName: string;
|
||||
repositoryId?: string;
|
||||
}
|
||||
|
||||
export interface FetchModelsParameters extends RepoParametersBase {
|
||||
metaModelType?: MetaModelType;
|
||||
pageSize?: number;
|
||||
continuationToken?: string;
|
||||
}
|
||||
|
||||
export interface FetchModelParameters extends RepoParametersBase {
|
||||
export interface FetchModelParameters {
|
||||
id: string;
|
||||
expand?: boolean;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface FetchModelsParameters extends RepoParametersBase {
|
||||
ids?: string[];
|
||||
token: string;
|
||||
token?: string;
|
||||
}
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { DataPlaneParameters } from '../parameters/deviceParameters';
|
||||
import { CONTROLLER_API_ENDPOINT, DATAPLANE, DataPlaneStatusCode } from '../../constants/apiConstants';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
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';
|
||||
|
||||
export const DATAPLANE_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${DATAPLANE}`;
|
||||
|
||||
|
@ -38,25 +37,23 @@ export const request = async (endpoint: string, parameters: any) => { // tslint:
|
|||
);
|
||||
};
|
||||
|
||||
export const dataPlaneConnectionHelper = (parameters: DataPlaneParameters) => {
|
||||
if (!parameters || !parameters.connectionString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = getConnectionInfoFromConnectionString(parameters.connectionString);
|
||||
export const dataPlaneConnectionHelper = async () => {
|
||||
const connectionStrings = await localStorage.getItem(CONNECTION_STRING_NAME_LIST);
|
||||
const connectionString = connectionStrings && connectionStrings.split(',')[0];
|
||||
const connectionInfo = getConnectionInfoFromConnectionString(connectionString);
|
||||
if (!(connectionInfo && connectionInfo.hostName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullHostName = `${connectionInfo.hostName}/devices/query`;
|
||||
const sasToken = generateSasToken({
|
||||
key: connectionInfo.sharedAccessKey,
|
||||
keyName: connectionInfo.sharedAccessKeyName,
|
||||
resourceUri: fullHostName
|
||||
resourceUri: connectionInfo.hostName
|
||||
});
|
||||
|
||||
return {
|
||||
connectionInfo,
|
||||
connectionString,
|
||||
sasToken,
|
||||
};
|
||||
};
|
||||
|
@ -101,5 +98,5 @@ export const dataPlaneResponseHelper = async (response: Response) => {
|
|||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
throw new Error(dataPlaneResponse.status && dataPlaneResponse.status.toString());
|
||||
};
|
||||
|
|
|
@ -5,17 +5,15 @@
|
|||
import 'jest';
|
||||
import * as DevicesService from './devicesService';
|
||||
import * as DataplaneService from './dataplaneServiceHelper';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
import { DIGITAL_TWIN_API_VERSION, HUB_DATA_PLANE_API_VERSION, CONTROLLER_API_ENDPOINT, CLOUD_TO_DEVICE } from '../../constants/apiConstants';
|
||||
import { CONNECTION_TIMEOUT_IN_SECONDS, RESPONSE_TIME_IN_SECONDS } from '../../constants/devices';
|
||||
import { CONTROLLER_API_ENDPOINT, CLOUD_TO_DEVICE, HTTP_OPERATION_TYPES, HUB_DATA_PLANE_API_VERSION} from '../../constants/apiConstants';
|
||||
import { Twin } from '../models/device';
|
||||
import { DeviceIdentity } from './../models/deviceIdentity';
|
||||
import { buildQueryString, getConnectionInfoFromConnectionString } from '../shared/utils';
|
||||
import { DataPlaneParameters, MonitorEventsParameters } from '../parameters/deviceParameters';
|
||||
import { MonitorEventsParameters } from '../parameters/deviceParameters';
|
||||
|
||||
const deviceId = 'deviceId';
|
||||
const connectionString = 'HostName=test-string.azure-devices.net;SharedAccessKeyName=owner;SharedAccessKey=fakeKey=';
|
||||
const componentName = 'componentName';
|
||||
const connectionInfo = getConnectionInfoFromConnectionString(connectionString);
|
||||
const headers = new Headers({
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
|
@ -50,12 +48,7 @@ const deviceIdentity: DeviceIdentity = {
|
|||
};
|
||||
// tslint:enable
|
||||
const sasToken = 'testSasToken';
|
||||
const mockDataPlaneConnectionHelper = (parameters: DataPlaneParameters) => {
|
||||
if (!parameters || !parameters.connectionString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = getConnectionInfoFromConnectionString(parameters.connectionString);
|
||||
const mockDataPlaneConnectionHelper = () => {
|
||||
if (!(connectionInfo && connectionInfo.hostName)) {
|
||||
return;
|
||||
}
|
||||
|
@ -68,22 +61,13 @@ const mockDataPlaneConnectionHelper = (parameters: DataPlaneParameters) => {
|
|||
describe('deviceTwinService', () => {
|
||||
|
||||
context('fetchDeviceTwin', () => {
|
||||
const parameters = {
|
||||
connectionString,
|
||||
deviceId: undefined
|
||||
};
|
||||
it ('returns if deviceId is not specified', () => {
|
||||
expect(DevicesService.fetchDeviceTwin(parameters)).toEqual(emptyPromise);
|
||||
});
|
||||
|
||||
it ('throws if connection string is not valid', async () => {
|
||||
await expect(DevicesService.fetchDeviceTwin({...parameters, deviceId, connectionString: undefined})).rejects.toThrow();
|
||||
await expect(DevicesService.fetchDeviceTwin({...parameters, deviceId, connectionString: 'SharedAccessKeyName=owner;SharedAccessKey=fakeKey='})).rejects.toThrow();
|
||||
expect(DevicesService.fetchDeviceTwin({deviceId: undefined})).toEqual(emptyPromise);
|
||||
});
|
||||
|
||||
it('calls fetch with specified parameters and returns deviceTwin when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo, connectionString, sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const response = {
|
||||
|
@ -96,7 +80,7 @@ describe('deviceTwinService', () => {
|
|||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
|
@ -105,10 +89,7 @@ describe('deviceTwinService', () => {
|
|||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const result = await DevicesService.fetchDeviceTwin({
|
||||
...parameters,
|
||||
deviceId
|
||||
});
|
||||
const result = await DevicesService.fetchDeviceTwin({deviceId});
|
||||
|
||||
const serviceRequestParams = {
|
||||
body: JSON.stringify(dataPlaneRequest),
|
||||
|
@ -125,244 +106,11 @@ describe('deviceTwinService', () => {
|
|||
|
||||
it('throws Error when promise rejects', async done => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error('Not found'));
|
||||
await expect(DevicesService.fetchDeviceTwin({
|
||||
...parameters,
|
||||
deviceId
|
||||
})).rejects.toThrowError('Not found');
|
||||
await expect(DevicesService.fetchDeviceTwin({deviceId})).rejects.toThrowError('Not found');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
context('fetchDigitalTwinInterfaceProperties', () => {
|
||||
const parameters = {
|
||||
connectionString,
|
||||
digitalTwinId: undefined
|
||||
};
|
||||
it ('returns if digitalTwinId is not specified', () => {
|
||||
expect(DevicesService.fetchDigitalTwinInterfaceProperties(parameters)).toEqual(emptyPromise);
|
||||
});
|
||||
|
||||
it('calls fetch with specified parameters and returns digitalTwin interfaces when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
// tslint:disable
|
||||
const digitalTwin = {
|
||||
interfaces: {
|
||||
'urn_azureiot_ModelDiscovery_DigitalTwin': {
|
||||
name: 'urn_azureiot_ModelDiscovery_DigitalTwin',
|
||||
properties: {
|
||||
modelInformation:
|
||||
{
|
||||
reported: {
|
||||
value:{interfaces: {'urn_azureiot_ModelDiscovery_DigitalTwin':'urn:azureiot:ModelDiscovery:DigitalTwin:1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'version':1
|
||||
};
|
||||
const response = {
|
||||
json: () => {
|
||||
return {
|
||||
body: digitalTwin,
|
||||
headers:{}
|
||||
}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await DevicesService.fetchDigitalTwinInterfaceProperties({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `/digitalTwins/${deviceId}/interfaces`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const serviceRequestParams = {
|
||||
body: JSON.stringify(dataPlaneRequest),
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
mode: 'cors',
|
||||
};
|
||||
|
||||
expect(fetch).toBeCalledWith(DataplaneService.DATAPLANE_CONTROLLER_ENDPOINT, serviceRequestParams);
|
||||
expect(result).toEqual(digitalTwin);
|
||||
});
|
||||
|
||||
it('throws Error when promise rejects', async () => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error('Internal server error'));
|
||||
await expect(DevicesService.fetchDigitalTwinInterfaceProperties({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
})).rejects.toThrow('Internal server error');
|
||||
});
|
||||
});
|
||||
|
||||
context('invokeDigitalTwinInterfaceCommand', () => {
|
||||
const parameters = {
|
||||
commandName: 'commandName',
|
||||
componentName,
|
||||
connectionString,
|
||||
digitalTwinId: undefined,
|
||||
payload: undefined
|
||||
};
|
||||
it ('returns if digitalTwinId is not specified', () => {
|
||||
expect(DevicesService.invokeDigitalTwinInterfaceCommand(parameters)).toEqual(emptyPromise);
|
||||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes DigitalTwinInterfaceCommand when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const responseBody = {
|
||||
description: 'Invoked'
|
||||
};
|
||||
const response = {
|
||||
json: () => {
|
||||
return {
|
||||
body: responseBody,
|
||||
headers:{}
|
||||
}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await DevicesService.invokeDigitalTwinInterfaceCommand({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const queryString = `connectTimeoutInSeconds=${CONNECTION_TIMEOUT_IN_SECONDS}&responseTimeoutInSeconds=${RESPONSE_TIME_IN_SECONDS}`;
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: `/digitalTwins/${deviceId}/interfaces/${parameters.componentName}/commands/${parameters.commandName}`,
|
||||
queryString,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const serviceRequestParams = {
|
||||
body: JSON.stringify(dataPlaneRequest),
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
mode: 'cors',
|
||||
};
|
||||
|
||||
expect(fetch).toBeCalledWith(DataplaneService.DATAPLANE_CONTROLLER_ENDPOINT, serviceRequestParams);
|
||||
expect(result).toEqual(responseBody);
|
||||
});
|
||||
|
||||
it('throws Error when promise rejects', async () => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error('Internal server error'));
|
||||
await expect(DevicesService.invokeDigitalTwinInterfaceCommand({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
})).rejects.toThrow('Internal server error');
|
||||
});
|
||||
});
|
||||
|
||||
context('patchDigitalTwinInterfaceProperties', () => {
|
||||
const payload = {
|
||||
interfaces: {
|
||||
Sensor: {
|
||||
properties: {
|
||||
name: {
|
||||
desired: {
|
||||
value: 123
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const parameters = {
|
||||
connectionString,
|
||||
digitalTwinId: undefined,
|
||||
payload
|
||||
};
|
||||
it ('returns if digitalTwinId is not specified', () => {
|
||||
expect(DevicesService.patchDigitalTwinInterfaceProperties(parameters)).toEqual(emptyPromise);
|
||||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes patchDigitalTwinInterfaceProperties when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const responseBody = {
|
||||
...payload
|
||||
};
|
||||
const response = {
|
||||
json: () => {
|
||||
return {
|
||||
body: responseBody,
|
||||
headers:{}
|
||||
}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await DevicesService.patchDigitalTwinInterfaceProperties({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Patch,
|
||||
path: `/digitalTwins/${deviceId}/interfaces`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const serviceRequestParams = {
|
||||
body: JSON.stringify(dataPlaneRequest),
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
mode: 'cors',
|
||||
};
|
||||
|
||||
expect(fetch).toBeCalledWith(DataplaneService.DATAPLANE_CONTROLLER_ENDPOINT, serviceRequestParams);
|
||||
expect(result).toEqual(responseBody);
|
||||
});
|
||||
|
||||
it('throws Error when promise rejects', async () => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error());
|
||||
|
||||
await expect(DevicesService.patchDigitalTwinInterfaceProperties({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
})).rejects.toThrow(new Error());
|
||||
});
|
||||
});
|
||||
|
||||
context('updateDeviceTwin', () => {
|
||||
const parameters = {
|
||||
connectionString,
|
||||
|
@ -374,8 +122,8 @@ describe('deviceTwinService', () => {
|
|||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes updateDeviceTwin when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), connectionString, sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const responseBody = twin;
|
||||
|
@ -391,12 +139,9 @@ describe('deviceTwinService', () => {
|
|||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await DevicesService.updateDeviceTwin({
|
||||
...parameters,
|
||||
deviceId
|
||||
});
|
||||
const result = await DevicesService.updateDeviceTwin(parameters);
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(twin),
|
||||
|
@ -422,8 +167,7 @@ describe('deviceTwinService', () => {
|
|||
it('throws Error when promise rejects', async () => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(DevicesService.updateDeviceTwin({
|
||||
...parameters,
|
||||
deviceId
|
||||
...parameters
|
||||
})).rejects.toThrow(new Error());
|
||||
});
|
||||
});
|
||||
|
@ -442,8 +186,8 @@ describe('deviceTwinService', () => {
|
|||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes invokeDirectMethod when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), connectionString, sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const responseBody = {description: 'invoked'};
|
||||
|
@ -464,7 +208,7 @@ describe('deviceTwinService', () => {
|
|||
deviceId
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify({
|
||||
|
@ -504,7 +248,6 @@ describe('deviceTwinService', () => {
|
|||
context('cloudToDeviceMessage', () => {
|
||||
const parameters = {
|
||||
body: '',
|
||||
connectionString,
|
||||
deviceId: undefined,
|
||||
properties: undefined
|
||||
};
|
||||
|
@ -531,6 +274,7 @@ describe('deviceTwinService', () => {
|
|||
expect(fetch).toBeCalledWith(`${CONTROLLER_API_ENDPOINT}${CLOUD_TO_DEVICE}`, {
|
||||
body: JSON.stringify({
|
||||
...parameters,
|
||||
connectionString,
|
||||
deviceId
|
||||
}),
|
||||
cache: 'no-cache',
|
||||
|
@ -563,8 +307,8 @@ describe('deviceTwinService', () => {
|
|||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes addDevice when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), connectionString, sasToken});
|
||||
// tslint:disable
|
||||
const responseBody = deviceIdentity;
|
||||
const response = {
|
||||
|
@ -584,7 +328,7 @@ describe('deviceTwinService', () => {
|
|||
deviceIdentity
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(deviceIdentity),
|
||||
|
@ -626,8 +370,8 @@ describe('deviceTwinService', () => {
|
|||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes updateDevice when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), connectionString, sasToken});
|
||||
// tslint:disable
|
||||
const responseBody = deviceIdentity;
|
||||
const response = {
|
||||
|
@ -647,7 +391,7 @@ describe('deviceTwinService', () => {
|
|||
deviceIdentity
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(deviceIdentity),
|
||||
|
@ -689,8 +433,8 @@ describe('deviceTwinService', () => {
|
|||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes fetchDevice when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), connectionString, sasToken});
|
||||
// tslint:disable
|
||||
const responseBody = deviceIdentity;
|
||||
const response = {
|
||||
|
@ -710,7 +454,7 @@ describe('deviceTwinService', () => {
|
|||
deviceId
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
|
@ -753,8 +497,8 @@ describe('deviceTwinService', () => {
|
|||
};
|
||||
|
||||
it('calls fetch with specified parameters and invokes fetchDevices when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), connectionString, sasToken});
|
||||
// tslint:disable
|
||||
const responseBody = deviceIdentity;
|
||||
const response = {
|
||||
|
@ -771,7 +515,7 @@ describe('deviceTwinService', () => {
|
|||
|
||||
const result = await DevicesService.fetchDevices(parameters);
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const queryString = buildQueryString(parameters.query);
|
||||
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
|
@ -815,8 +559,8 @@ describe('deviceTwinService', () => {
|
|||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes deleteDevices when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), connectionString, sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const responseBody = {isSuccessful:true, errors:[], warnings:[]};
|
||||
|
@ -839,7 +583,7 @@ describe('deviceTwinService', () => {
|
|||
|
||||
const result = await DevicesService.deleteDevices(parameters);
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const connectionInformation = mockDataPlaneConnectionHelper();
|
||||
const deviceDeletionInstructions = parameters.deviceIds.map(id => {
|
||||
return {
|
||||
etag: '*',
|
||||
|
@ -884,16 +628,15 @@ describe('deviceTwinService', () => {
|
|||
await expect(DevicesService.deleteDevices({
|
||||
...parameters,
|
||||
deviceIds: [deviceId]
|
||||
})).rejects.toThrow(new Error()).catch();
|
||||
})).rejects.toThrow(new Error('500')).catch();
|
||||
});
|
||||
});
|
||||
|
||||
context('monitorEvents', () => {
|
||||
let parameters: MonitorEventsParameters = {
|
||||
const parameters: MonitorEventsParameters = {
|
||||
consumerGroup: '$Default',
|
||||
customEventHubConnectionString: undefined,
|
||||
deviceId,
|
||||
fetchSystemProperties: undefined,
|
||||
hubConnectionString: undefined,
|
||||
startTime: undefined
|
||||
};
|
||||
|
@ -902,6 +645,8 @@ describe('deviceTwinService', () => {
|
|||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes monitorEvents when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(connectionString), connectionString, sasToken});
|
||||
// tslint:disable
|
||||
const responseBody = [{'body':{'temp':0},'enqueuedTime':'2019-09-06T17:47:11.334Z','properties':{'iothub-message-schema':'temp'}}];
|
||||
const response = {
|
||||
|
@ -911,14 +656,11 @@ describe('deviceTwinService', () => {
|
|||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
parameters = {
|
||||
...parameters,
|
||||
hubConnectionString: connectionString
|
||||
};
|
||||
const result = await DevicesService.monitorEvents(parameters);
|
||||
|
||||
const eventHubRequestParameters = {
|
||||
...parameters,
|
||||
hubConnectionString: connectionString,
|
||||
startTime: parameters.startTime && parameters.startTime.toISOString()
|
||||
};
|
||||
|
||||
|
|
|
@ -12,28 +12,22 @@ import {
|
|||
DeleteDevicesParameters,
|
||||
AddDeviceParameters,
|
||||
UpdateDeviceParameters,
|
||||
FetchDigitalTwinInterfacePropertiesParameters,
|
||||
InvokeDigitalTwinInterfaceCommandParameters,
|
||||
PatchDigitalTwinInterfacePropertiesParameters,
|
||||
CloudToDeviceMessageParameters
|
||||
} from '../parameters/deviceParameters';
|
||||
import { CONTROLLER_API_ENDPOINT,
|
||||
EVENTHUB,
|
||||
DIGITAL_TWIN_API_VERSION,
|
||||
HUB_DATA_PLANE_API_VERSION,
|
||||
MONITOR,
|
||||
STOP,
|
||||
HEADERS,
|
||||
CLOUD_TO_DEVICE,
|
||||
DataPlaneStatusCode
|
||||
HTTP_OPERATION_TYPES,
|
||||
DataPlaneStatusCode,
|
||||
HUB_DATA_PLANE_API_VERSION
|
||||
} from '../../constants/apiConstants';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
import { buildQueryString } from '../shared/utils';
|
||||
import { CONNECTION_TIMEOUT_IN_SECONDS, RESPONSE_TIME_IN_SECONDS } from '../../constants/devices';
|
||||
import { Message } from '../models/messages';
|
||||
import { Twin, Device, DataPlaneResponse } from '../models/device';
|
||||
import { DeviceIdentity } from '../models/deviceIdentity';
|
||||
import { DigitalTwinInterfaces } from '../models/digitalTwinModels';
|
||||
import { parseEventHubMessage } from './eventHubMessageHelper';
|
||||
import { dataPlaneConnectionHelper, dataPlaneResponseHelper, request, DATAPLANE_CONTROLLER_ENDPOINT, DataPlaneRequest } from './dataplaneServiceHelper';
|
||||
|
||||
|
@ -54,273 +48,167 @@ export interface DirectMethodResult {
|
|||
}
|
||||
|
||||
export const fetchDeviceTwin = async (parameters: FetchDeviceTwinParameters): Promise<Twin> => {
|
||||
try {
|
||||
if (!parameters.deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `twins/${parameters.deviceId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (!parameters.deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `twins/${parameters.deviceId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const updateDeviceTwin = async (parameters: UpdateDeviceTwinParameters): Promise<Twin> => {
|
||||
try {
|
||||
if (!parameters.deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.deviceTwin),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Patch,
|
||||
path: `twins/${parameters.deviceId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (!parameters.deviceTwin) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchDigitalTwinInterfaceProperties = async (parameters: FetchDigitalTwinInterfacePropertiesParameters): Promise<DigitalTwinInterfaces> => {
|
||||
try {
|
||||
if (!parameters.digitalTwinId) {
|
||||
return;
|
||||
}
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.deviceTwin),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Patch,
|
||||
path: `twins/${parameters.deviceTwin.deviceId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `/digitalTwins/${parameters.digitalTwinId}/interfaces`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
export const invokeDigitalTwinInterfaceCommand = async (parameters: InvokeDigitalTwinInterfaceCommandParameters): Promise<any> => {
|
||||
try {
|
||||
if (!parameters.digitalTwinId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const connectTimeoutInSeconds = parameters.connectTimeoutInSeconds || CONNECTION_TIMEOUT_IN_SECONDS;
|
||||
const responseTimeoutInSeconds = parameters.responseTimeoutInSeconds || RESPONSE_TIME_IN_SECONDS;
|
||||
const queryString = `connectTimeoutInSeconds=${connectTimeoutInSeconds}&responseTimeoutInSeconds=${responseTimeoutInSeconds}`;
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: `/digitalTwins/${parameters.digitalTwinId}/interfaces/${parameters.componentName}/commands/${parameters.commandName}`,
|
||||
queryString,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const patchDigitalTwinInterfaceProperties = async (parameters: PatchDigitalTwinInterfacePropertiesParameters): Promise<DigitalTwinInterfaces> => {
|
||||
try {
|
||||
if (!parameters.digitalTwinId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Patch,
|
||||
path: `/digitalTwins/${parameters.digitalTwinId}/interfaces`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const invokeDirectMethod = async (parameters: InvokeMethodParameters): Promise<DirectMethodResult> => {
|
||||
try {
|
||||
if (!parameters.deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify({
|
||||
connectTimeoutInSeconds: parameters.connectTimeoutInSeconds,
|
||||
methodName: parameters.methodName,
|
||||
payload: parameters.payload,
|
||||
responseTimeoutInSeconds: parameters.responseTimeoutInSeconds,
|
||||
}),
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: `twins/${parameters.deviceId}/methods`,
|
||||
sharedAccessSignature: connectionInfo.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (!parameters.deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify({
|
||||
connectTimeoutInSeconds: parameters.connectTimeoutInSeconds,
|
||||
methodName: parameters.methodName,
|
||||
payload: parameters.payload,
|
||||
responseTimeoutInSeconds: parameters.responseTimeoutInSeconds,
|
||||
}),
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: `twins/${parameters.deviceId}/methods`,
|
||||
sharedAccessSignature: connectionInfo.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const cloudToDeviceMessage = async (parameters: CloudToDeviceMessageParameters) => {
|
||||
try {
|
||||
const cloudToDeviceRequest = {
|
||||
...parameters
|
||||
};
|
||||
const response = await request(`${CONTROLLER_API_ENDPOINT}${CLOUD_TO_DEVICE}`, cloudToDeviceRequest);
|
||||
await dataPlaneResponseHelper(response);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const connectionInfo = await dataPlaneConnectionHelper();
|
||||
const cloudToDeviceRequest = {
|
||||
...parameters,
|
||||
connectionString: connectionInfo.connectionString
|
||||
};
|
||||
|
||||
const response = await request(`${CONTROLLER_API_ENDPOINT}${CLOUD_TO_DEVICE}`, cloudToDeviceRequest);
|
||||
await dataPlaneResponseHelper(response);
|
||||
};
|
||||
|
||||
export const addDevice = async (parameters: AddDeviceParameters): Promise<DeviceIdentity> => {
|
||||
try {
|
||||
if (!parameters.deviceIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.deviceIdentity),
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Put,
|
||||
path: `devices/${parameters.deviceIdentity.deviceId}`,
|
||||
sharedAccessSignature: connectionInfo.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (!parameters.deviceIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.deviceIdentity),
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Put,
|
||||
path: `devices/${parameters.deviceIdentity.deviceId}`,
|
||||
sharedAccessSignature: connectionInfo.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const updateDevice = async (parameters: UpdateDeviceParameters): Promise<DeviceIdentity> => {
|
||||
try {
|
||||
if (!parameters.deviceIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.deviceIdentity),
|
||||
headers: {} as any, // tslint:disable-line: no-any
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Put,
|
||||
path: `devices/${parameters.deviceIdentity.deviceId}`,
|
||||
sharedAccessSignature: connectionInfo.sasToken
|
||||
};
|
||||
|
||||
(dataPlaneRequest.headers as any)[HEADERS.IF_MATCH] = `"${parameters.deviceIdentity.etag}"`; // tslint:disable-line: no-any
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (!parameters.deviceIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.deviceIdentity),
|
||||
headers: {} as any, // tslint:disable-line: no-any
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Put,
|
||||
path: `devices/${parameters.deviceIdentity.deviceId}`,
|
||||
sharedAccessSignature: connectionInfo.sasToken
|
||||
};
|
||||
|
||||
(dataPlaneRequest.headers as any)[HEADERS.IF_MATCH] = `"${parameters.deviceIdentity.etag}"`; // tslint:disable-line: no-any
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const fetchDevice = async (parameters: FetchDeviceParameters): Promise<DeviceIdentity> => {
|
||||
try {
|
||||
if (!parameters.deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${parameters.deviceId}`,
|
||||
sharedAccessSignature: connectionInfo.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (!parameters.deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${parameters.deviceId}`,
|
||||
sharedAccessSignature: connectionInfo.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const fetchDevices = async (parameters: FetchDevicesParameters): Promise<DataPlaneResponse<Device[]>> => {
|
||||
try {
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const queryString = buildQueryString(parameters.query);
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
const queryString = buildQueryString(parameters.query);
|
||||
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify({
|
||||
query: queryString,
|
||||
}),
|
||||
headers: {} as any, // tslint:disable-line: no-any
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: 'devices/query',
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify({
|
||||
query: queryString,
|
||||
}),
|
||||
headers: {} as any, // tslint:disable-line: no-any
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: 'devices/query',
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
(dataPlaneRequest.headers as any)[HEADERS.PAGE_SIZE] = PAGE_SIZE; // tslint:disable-line: no-any
|
||||
(dataPlaneRequest.headers as any)[HEADERS.PAGE_SIZE] = PAGE_SIZE; // tslint:disable-line: no-any
|
||||
|
||||
if (parameters.query && parameters.query.currentPageIndex > 0 && parameters.query.continuationTokens && parameters.query.continuationTokens.length >= parameters.query.currentPageIndex) {
|
||||
(dataPlaneRequest.headers as any)[HEADERS.CONTINUATION_TOKEN] = parameters.query.continuationTokens[parameters.query.currentPageIndex]; // tslint:disable-line: no-any
|
||||
}
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (parameters.query && parameters.query.currentPageIndex > 0 && parameters.query.continuationTokens && parameters.query.continuationTokens.length >= parameters.query.currentPageIndex) {
|
||||
(dataPlaneRequest.headers as any)[HEADERS.CONTINUATION_TOKEN] = parameters.query.continuationTokens[parameters.query.currentPageIndex]; // tslint:disable-line: no-any
|
||||
}
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const deleteDevices = async (parameters: DeleteDevicesParameters) => {
|
||||
|
@ -328,44 +216,45 @@ export const deleteDevices = async (parameters: DeleteDevicesParameters) => {
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const deviceDeletionInstructions = parameters.deviceIds.map(deviceId => (
|
||||
{
|
||||
etag: '*',
|
||||
id: deviceId,
|
||||
importMode: 'deleteIfMatchEtag'
|
||||
}
|
||||
));
|
||||
const deviceDeletionInstructions = parameters.deviceIds.map(deviceId => (
|
||||
{
|
||||
etag: '*',
|
||||
id: deviceId,
|
||||
importMode: 'deleteIfMatchEtag'
|
||||
}
|
||||
));
|
||||
|
||||
const connectionInfo = dataPlaneConnectionHelper(parameters);
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(deviceDeletionInstructions),
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: `devices`,
|
||||
sharedAccessSignature: connectionInfo.sasToken,
|
||||
};
|
||||
const connectionInfo = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(deviceDeletionInstructions),
|
||||
hostName: connectionInfo.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: `devices`,
|
||||
sharedAccessSignature: connectionInfo.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:cyclomatic-complexity
|
||||
export const monitorEvents = async (parameters: MonitorEventsParameters): Promise<Message[]> => {
|
||||
if (!parameters.hubConnectionString && (!parameters.customEventHubConnectionString || !parameters.customEventHubName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestParameters = {
|
||||
let requestParameters = {
|
||||
...parameters,
|
||||
startTime: parameters.startTime && parameters.startTime.toISOString()
|
||||
};
|
||||
|
||||
// if either of the info about custom event hub is not provided, use default hub connection string to connect to event hub
|
||||
if (!parameters.customEventHubConnectionString || !parameters.customEventHubName) {
|
||||
const connectionInfo = await dataPlaneConnectionHelper();
|
||||
requestParameters = {
|
||||
...requestParameters,
|
||||
hubConnectionString: connectionInfo.connectionString
|
||||
};
|
||||
}
|
||||
|
||||
const response = await request(EVENTHUB_MONITOR_ENDPOINT, requestParameters);
|
||||
if (response.status === DataPlaneStatusCode.SuccessLowerBound) {
|
||||
const messages = await response.json() as Message[];
|
||||
|
@ -378,9 +267,5 @@ export const monitorEvents = async (parameters: MonitorEventsParameters): Promis
|
|||
};
|
||||
|
||||
export const stopMonitoringEvents = async (): Promise<void> => {
|
||||
try {
|
||||
await request(EVENTHUB_STOP_ENDPOINT, {});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
await request(EVENTHUB_STOP_ENDPOINT, {});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as DigitalTwinService from './digitalTwinService';
|
||||
import * as DataplaneService from './dataplaneServiceHelper';
|
||||
import { HTTP_OPERATION_TYPES, DIGITAL_TWIN_API_VERSION_PREVIEW } from '../../constants/apiConstants';
|
||||
import { CONNECTION_TIMEOUT_IN_SECONDS, RESPONSE_TIME_IN_SECONDS } from '../../constants/devices';
|
||||
import { getConnectionInfoFromConnectionString } from '../shared/utils';
|
||||
import { DataPlaneParameters, JsonPatchOperation } from '../parameters/deviceParameters';
|
||||
|
||||
const deviceId = 'deviceId';
|
||||
const connectionString = 'HostName=test-string.azure-devices.net;SharedAccessKeyName=owner;SharedAccessKey=fakeKey=';
|
||||
const componentName = 'componentName';
|
||||
const headers = new Headers({
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
// tslint:disable-next-line:no-empty
|
||||
const emptyPromise = new Promise(() => {});
|
||||
const sasToken = 'testSasToken';
|
||||
const mockDataPlaneConnectionHelper = (parameters: DataPlaneParameters) => {
|
||||
if (!parameters || !parameters.connectionString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInfo = getConnectionInfoFromConnectionString(parameters.connectionString);
|
||||
if (!(connectionInfo && connectionInfo.hostName)) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
connectionInfo,
|
||||
sasToken,
|
||||
};
|
||||
};
|
||||
|
||||
describe('digitalTwinService', () => {
|
||||
|
||||
context('invokeDigitalTwinInterfaceCommand', () => {
|
||||
const parameters = {
|
||||
commandName: 'commandName',
|
||||
componentName,
|
||||
connectionString,
|
||||
digitalTwinId: undefined,
|
||||
payload: undefined
|
||||
};
|
||||
it ('returns if digitalTwinId is not specified', () => {
|
||||
expect(DigitalTwinService.invokeDigitalTwinInterfaceCommand(parameters)).toEqual(emptyPromise);
|
||||
});
|
||||
|
||||
it('calls fetch with specified parameters and invokes DigitalTwinInterfaceCommand when response is 200', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const responseBody = {
|
||||
description: 'Invoked'
|
||||
};
|
||||
const response = {
|
||||
json: () => {
|
||||
return {
|
||||
body: responseBody,
|
||||
headers:{}
|
||||
}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await DigitalTwinService.invokeDigitalTwinInterfaceCommand({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const queryString = `connectTimeoutInSeconds=${CONNECTION_TIMEOUT_IN_SECONDS}&responseTimeoutInSeconds=${RESPONSE_TIME_IN_SECONDS}`;
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION_PREVIEW,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: `/digitalTwins/${deviceId}/components/${parameters.componentName}/commands/${parameters.commandName}`,
|
||||
queryString,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const serviceRequestParams = {
|
||||
body: JSON.stringify(dataPlaneRequest),
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
mode: 'cors',
|
||||
};
|
||||
|
||||
expect(fetch).toBeCalledWith(DataplaneService.DATAPLANE_CONTROLLER_ENDPOINT, serviceRequestParams);
|
||||
expect(result).toEqual(responseBody);
|
||||
});
|
||||
|
||||
it('throws Error when promise rejects', async () => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error('Internal server error'));
|
||||
await expect(DigitalTwinService.invokeDigitalTwinInterfaceCommand({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
})).rejects.toThrow('Internal server error');
|
||||
});
|
||||
});
|
||||
|
||||
context('patchDigitalTwin', () => {
|
||||
const payload = {
|
||||
interfaces: {
|
||||
Sensor: {
|
||||
properties: {
|
||||
name: {
|
||||
desired: {
|
||||
value: 123
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const parameters = {
|
||||
connectionString,
|
||||
digitalTwinId: undefined,
|
||||
payload: [
|
||||
{
|
||||
op: JsonPatchOperation.REPLACE,
|
||||
path: '/environmentalSensor/state',
|
||||
value: false
|
||||
}]
|
||||
};
|
||||
it ('returns if digitalTwinId is not specified', () => {
|
||||
expect(DigitalTwinService.patchDigitalTwinAndGetResponseCode(parameters)).toEqual(emptyPromise);
|
||||
});
|
||||
|
||||
it('calls fetch with specified parameters', async () => {
|
||||
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {
|
||||
return {
|
||||
body: {},
|
||||
headers:{}
|
||||
}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await DigitalTwinService.patchDigitalTwinAndGetResponseCode({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
});
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION_PREVIEW,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Patch,
|
||||
path: `/digitalTwins/${deviceId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const serviceRequestParams = {
|
||||
body: JSON.stringify(dataPlaneRequest),
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
mode: 'cors',
|
||||
};
|
||||
|
||||
expect(fetch).toBeCalledWith(DataplaneService.DATAPLANE_CONTROLLER_ENDPOINT, serviceRequestParams);
|
||||
// tslint:disable-next-line: no-magic-numbers
|
||||
expect(result).toEqual(200);
|
||||
});
|
||||
|
||||
it('throws Error when promise rejects', async () => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error());
|
||||
|
||||
await expect(DigitalTwinService.patchDigitalTwinAndGetResponseCode({
|
||||
...parameters,
|
||||
digitalTwinId: deviceId
|
||||
})).rejects.toThrow(new Error());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import {
|
||||
FetchDigitalTwinParameters,
|
||||
InvokeDigitalTwinInterfaceCommandParameters,
|
||||
PatchDigitalTwinParameters
|
||||
} from '../parameters/deviceParameters';
|
||||
import { DIGITAL_TWIN_API_VERSION_PREVIEW, HTTP_OPERATION_TYPES, DataPlaneStatusCode } from '../../constants/apiConstants';
|
||||
import { CONNECTION_TIMEOUT_IN_SECONDS, RESPONSE_TIME_IN_SECONDS, DEFAULT_COMPONENT_FOR_DIGITAL_TWIN } from '../../constants/devices';
|
||||
import { dataPlaneConnectionHelper, dataPlaneResponseHelper, request, DATAPLANE_CONTROLLER_ENDPOINT, DataPlaneRequest } from './dataplaneServiceHelper';
|
||||
|
||||
export const fetchDigitalTwin = async (parameters: FetchDigitalTwinParameters) => {
|
||||
if (!parameters.digitalTwinId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION_PREVIEW,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `/digitalTwins/${parameters.digitalTwinId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const patchDigitalTwinAndGetResponseCode = async (parameters: PatchDigitalTwinParameters): Promise<number> => {
|
||||
if (!parameters.digitalTwinId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION_PREVIEW,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Patch,
|
||||
path: `/digitalTwins/${parameters.digitalTwinId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
return getPatchResultHelper(response);
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const invokeDigitalTwinInterfaceCommand = async (parameters: InvokeDigitalTwinInterfaceCommandParameters) => {
|
||||
if (!parameters.digitalTwinId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
const connectTimeoutInSeconds = parameters.connectTimeoutInSeconds || CONNECTION_TIMEOUT_IN_SECONDS;
|
||||
const responseTimeoutInSeconds = parameters.responseTimeoutInSeconds || RESPONSE_TIME_IN_SECONDS;
|
||||
const queryString = `connectTimeoutInSeconds=${connectTimeoutInSeconds}&responseTimeoutInSeconds=${responseTimeoutInSeconds}`;
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: DIGITAL_TWIN_API_VERSION_PREVIEW,
|
||||
body: JSON.stringify(parameters.payload),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Post,
|
||||
path: parameters.componentName === DEFAULT_COMPONENT_FOR_DIGITAL_TWIN ?
|
||||
`/digitalTwins/${parameters.digitalTwinId}/commands/${parameters.commandName}` :
|
||||
`/digitalTwins/${parameters.digitalTwinId}/components/${parameters.componentName}/commands/${parameters.commandName}`,
|
||||
queryString,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
const getPatchResultHelper = async (response: Response) => {
|
||||
const dataPlaneResponse = await response;
|
||||
const result = await response.json();
|
||||
|
||||
// success case
|
||||
if (DataPlaneStatusCode.Accepted === dataPlaneResponse.status || dataPlaneResponse.status === DataPlaneStatusCode.SuccessLowerBound) {
|
||||
return dataPlaneResponse.status;
|
||||
}
|
||||
|
||||
// error case with message in body
|
||||
if (result && result.body) {
|
||||
if (result.body.Message || result.body.ExceptionMessage) {
|
||||
throw new Error(result.body.Message || result.body.ExceptionMessage);
|
||||
}
|
||||
}
|
||||
throw new Error(dataPlaneResponse.status && dataPlaneResponse.status.toString());
|
||||
};
|
|
@ -0,0 +1,85 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as LocalRepoService from './localRepoService';
|
||||
import { ModelDefinitionNotFound } from '../models/modelDefinitionNotFoundError';
|
||||
describe('localRepoService', () => {
|
||||
|
||||
context('fetchLocalFile', () => {
|
||||
it('returns file content when response is 200', async done => {
|
||||
// tslint:disable
|
||||
const content = {
|
||||
"@id": "urn:FlyYing:EnvironmentalSensor:1",
|
||||
"@type": "Interface",
|
||||
"displayName": "Environmental Sensor",
|
||||
"description": "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties",
|
||||
"comment": "Requires temperature and humidity sensors.",
|
||||
"contents": [
|
||||
{
|
||||
"@type": "Property",
|
||||
"displayName": "Device State",
|
||||
"description": "The state of the device. Two states online/offline are available.",
|
||||
"name": "state",
|
||||
"schema": "boolean"
|
||||
}
|
||||
],
|
||||
"@context": "http://azureiot.com/v1/contexts/IoTModel.json"
|
||||
};
|
||||
const response = {
|
||||
status: 200,
|
||||
json: () => content,
|
||||
headers: {},
|
||||
ok: true
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await LocalRepoService.fetchLocalFile('f:', 'test.json');
|
||||
expect(result).toEqual(content);
|
||||
done();
|
||||
});
|
||||
|
||||
it('throw when response is 404', async done => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new ModelDefinitionNotFound('Not found'));
|
||||
await expect(LocalRepoService.fetchLocalFile('f:', 'test.json')).rejects.toThrowError('Not found');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
context('fetchDirectories', () => {
|
||||
it('returns array of drives when path is not provided', async done => {
|
||||
// tslint:disable
|
||||
const content = `Name \r\n C: \r\n D: \r\n \r\n \r\n `;
|
||||
const response = {
|
||||
status: 200,
|
||||
text: () => content,
|
||||
headers: {},
|
||||
ok: true
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await LocalRepoService.fetchDirectories('');
|
||||
expect(result).toEqual(['C:/', 'D:/']);
|
||||
done();
|
||||
});
|
||||
|
||||
it('returns array of folders when path is provided', async done => {
|
||||
// tslint:disable
|
||||
const content = ["documents", "pictures"];
|
||||
const response = {
|
||||
status: 200,
|
||||
json: () => content,
|
||||
headers: {},
|
||||
ok: true
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await LocalRepoService.fetchDirectories('C:/');
|
||||
expect(result).toEqual(['documents', 'pictures']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { READ_FILE, GET_DIRECTORIES, CONTROLLER_API_ENDPOINT, DataPlaneStatusCode, DEFAULT_DIRECTORY } from './../../constants/apiConstants';
|
||||
import { ModelDefinitionNotFound } from '../models/modelDefinitionNotFoundError';
|
||||
import { ModelDefinitionNotValidJsonError } from '../models/modelDefinitionNotValidJsonError';
|
||||
|
||||
export const fetchLocalFile = async (path: string, fileName: string): Promise<string> => {
|
||||
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${READ_FILE}/${encodeURIComponent(path)}/${encodeURIComponent(fileName)}`);
|
||||
if (await response.status === DataPlaneStatusCode.NoContentSuccess || response.status === DataPlaneStatusCode.InternalServerError) {
|
||||
throw new ModelDefinitionNotFound();
|
||||
}
|
||||
|
||||
if (await response.status === DataPlaneStatusCode.NotFound) {
|
||||
throw new ModelDefinitionNotValidJsonError(await response.text());
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchDirectories = async (path: string): Promise<string[]> => {
|
||||
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${GET_DIRECTORIES}/${encodeURIComponent(path || DEFAULT_DIRECTORY)}`);
|
||||
if (!path) {
|
||||
// only possible when platform is windows, expecting drives to be returned
|
||||
const responseText = await response.text();
|
||||
const drives = responseText.split(/\r\n/).map(drive => drive.trim()).filter(drive => drive !== '');
|
||||
drives.shift(); // remove header
|
||||
return drives.map(drive => `${drive}/`); // add trailing slash for drives
|
||||
}
|
||||
else {
|
||||
return response.json();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { getLocalFolderPath, getRepositoryLocations, setLocalFolderPath, setRepositoryLocations } from './modelRepositoryService';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { REPO_LOCATIONS, LOCAL_FILE_EXPLORER_PATH_NAME } from '../../constants/browserStorage';
|
||||
import { appConfig, HostMode } from '../../../appConfig/appConfig';
|
||||
|
||||
describe('setLocalFolderPath', () => {
|
||||
it('returns local storage item', () => {
|
||||
setLocalFolderPath('localFolderPath');
|
||||
expect(localStorage.getItem(LOCAL_FILE_EXPLORER_PATH_NAME)).toEqual('localFolderPath');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('setRepositoryLocations', () => {
|
||||
it('returns local storage item', () => {
|
||||
setRepositoryLocations([
|
||||
REPOSITORY_LOCATION_TYPE.Local,
|
||||
REPOSITORY_LOCATION_TYPE.Public
|
||||
]);
|
||||
|
||||
expect(localStorage.getItem(REPO_LOCATIONS)).toEqual('LOCAL,PUBLIC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocalFolderPath', () => {
|
||||
it('returns null when HostMode is not electron', () => {
|
||||
appConfig.hostMode = HostMode.Browser;
|
||||
expect(getLocalFolderPath()).toBeNull();
|
||||
});
|
||||
|
||||
it('returns empty string when path name is not set', () => {
|
||||
appConfig.hostMode = HostMode.Electron;
|
||||
localStorage.setItem(LOCAL_FILE_EXPLORER_PATH_NAME, '');
|
||||
expect(getLocalFolderPath()).toEqual('');
|
||||
});
|
||||
|
||||
it('returns expected value when HostMode is electron', () => {
|
||||
appConfig.hostMode = HostMode.Electron;
|
||||
localStorage.setItem(LOCAL_FILE_EXPLORER_PATH_NAME, 'value');
|
||||
expect(getLocalFolderPath()).toEqual('value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRepositoryLocations', () => {
|
||||
it('returns empty array if repo locations is undefined', () => {
|
||||
localStorage.setItem(REPO_LOCATIONS, '');
|
||||
expect(getRepositoryLocations()).toEqual([]);
|
||||
});
|
||||
|
||||
it('return expected value when an unknwon type present', () => {
|
||||
localStorage.setItem(REPO_LOCATIONS, 'PUBLIC,any');
|
||||
expect(getRepositoryLocations()).toEqual([REPOSITORY_LOCATION_TYPE.Public]);
|
||||
});
|
||||
|
||||
it('filters local when app config is not electron', () => {
|
||||
localStorage.setItem(REPO_LOCATIONS, 'PUBLIC,LOCAL');
|
||||
appConfig.hostMode = HostMode.Browser;
|
||||
|
||||
expect(getRepositoryLocations()).toEqual([REPOSITORY_LOCATION_TYPE.Public]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { REPO_LOCATIONS, LOCAL_FILE_EXPLORER_PATH_NAME } from '../../constants/browserStorage';
|
||||
import { appConfig, HostMode } from '../../../appConfig/appConfig';
|
||||
|
||||
export const getLocalFolderPath = () => {
|
||||
return appConfig.hostMode === HostMode.Electron ? localStorage.getItem(LOCAL_FILE_EXPLORER_PATH_NAME) || '' : null;
|
||||
};
|
||||
|
||||
export const getRepositoryLocations = () => {
|
||||
if (localStorage.getItem(REPO_LOCATIONS)) {
|
||||
let locations = localStorage.getItem(REPO_LOCATIONS).split(',')
|
||||
.filter(location => Object.values(REPOSITORY_LOCATION_TYPE).indexOf(location.toUpperCase() as REPOSITORY_LOCATION_TYPE) > -1)
|
||||
.map(location => location.toUpperCase() as REPOSITORY_LOCATION_TYPE);
|
||||
|
||||
if (appConfig.hostMode !== HostMode.Electron) { // do now show local folder option in browser version
|
||||
locations = locations.filter(location => location !== REPOSITORY_LOCATION_TYPE.Local);
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const setLocalFolderPath = (localFolderPath: string) => {
|
||||
localStorage.setItem(LOCAL_FILE_EXPLORER_PATH_NAME, localFolderPath);
|
||||
};
|
||||
|
||||
export const setRepositoryLocations = (locations: REPOSITORY_LOCATION_TYPE[]) => {
|
||||
localStorage.setItem(REPO_LOCATIONS, locations.join(','));
|
||||
};
|
|
@ -5,12 +5,11 @@
|
|||
import 'jest';
|
||||
import * as ModuleService from './moduleService';
|
||||
import * as DataplaneService from './dataplaneServiceHelper';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
import { getConnectionInfoFromConnectionString } from '../shared/utils';
|
||||
import { DataPlaneParameters } from '../parameters/deviceParameters';
|
||||
import { ModuleIdentity } from '../models/moduleIdentity';
|
||||
import { ModuleTwin } from '../models/moduleTwin';
|
||||
import { HUB_DATA_PLANE_API_VERSION } from '../../constants/apiConstants';
|
||||
import { HTTP_OPERATION_TYPES, HUB_DATA_PLANE_API_VERSION } from '../../constants/apiConstants';
|
||||
|
||||
const deviceId = 'deviceId';
|
||||
const moduleId = 'moduleId';
|
||||
|
|
|
@ -8,12 +8,11 @@ import {
|
|||
ModuleIdentityTwinParameters,
|
||||
FetchModuleIdentityParameters
|
||||
} from '../parameters/moduleParameters';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
import { DataPlaneResponse } from '../models/device';
|
||||
import { ModuleIdentity } from '../models/moduleIdentity';
|
||||
import { ModuleTwin } from '../models/moduleTwin';
|
||||
import { dataPlaneConnectionHelper, dataPlaneResponseHelper, request, DATAPLANE_CONTROLLER_ENDPOINT, DataPlaneRequest } from './dataplaneServiceHelper';
|
||||
import { HEADERS, HUB_DATA_PLANE_API_VERSION } from '../../constants/apiConstants';
|
||||
import { HEADERS, HTTP_OPERATION_TYPES, HUB_DATA_PLANE_API_VERSION } from '../../constants/apiConstants';
|
||||
|
||||
export interface IoTHubConnectionSettings {
|
||||
hostName?: string;
|
||||
|
@ -27,104 +26,84 @@ export interface DirectMethodResult {
|
|||
}
|
||||
|
||||
export const fetchModuleIdentities = async (parameters: FetchModuleIdentitiesParameters): Promise<DataPlaneResponse<ModuleIdentity[]>> => {
|
||||
try {
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${parameters.deviceId}/modules`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${parameters.deviceId}/modules`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const addModuleIdentity = async (parameters: AddModuleIdentityParameters): Promise<DataPlaneResponse<ModuleIdentity>> => {
|
||||
try {
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.moduleIdentity),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Put,
|
||||
path: `devices/${parameters.moduleIdentity.deviceId}/modules/${parameters.moduleIdentity.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
body: JSON.stringify(parameters.moduleIdentity),
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Put,
|
||||
path: `devices/${parameters.moduleIdentity.deviceId}/modules/${parameters.moduleIdentity.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const fetchModuleIdentityTwin = async (parameters: ModuleIdentityTwinParameters): Promise<DataPlaneResponse<ModuleTwin>> => {
|
||||
try {
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `twins/${parameters.deviceId}/modules/${parameters.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `twins/${parameters.deviceId}/modules/${parameters.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const fetchModuleIdentity = async (parameters: FetchModuleIdentityParameters): Promise<DataPlaneResponse<ModuleIdentity[]>> => {
|
||||
try {
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${parameters.deviceId}/modules/${parameters.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${parameters.deviceId}/modules/${parameters.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result && result.body;
|
||||
};
|
||||
|
||||
export const deleteModuleIdentity = async (parameters: FetchModuleIdentityParameters): Promise<DataPlaneResponse<ModuleIdentity[]>> => {
|
||||
try {
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
const connectionInformation = await dataPlaneConnectionHelper();
|
||||
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
headers: {} as any, // tslint:disable-line: no-any
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Delete,
|
||||
path: `devices/${parameters.deviceId}/modules/${parameters.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
apiVersion: HUB_DATA_PLANE_API_VERSION,
|
||||
headers: {} as any, // tslint:disable-line: no-any
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Delete,
|
||||
path: `devices/${parameters.deviceId}/modules/${parameters.moduleId}`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
(dataPlaneRequest.headers as any)[HEADERS.IF_MATCH] = '*'; // tslint:disable-line: no-any
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
await dataPlaneResponseHelper(response);
|
||||
return;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
(dataPlaneRequest.headers as any)[HEADERS.IF_MATCH] = '*'; // tslint:disable-line: no-any
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
await dataPlaneResponseHelper(response);
|
||||
return;
|
||||
};
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as DigitalTwinsModelService from './digitalTwinsModelService';
|
||||
import { API_VERSION, DIGITAL_TWIN_API_VERSION } from '../../constants/apiConstants';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
import * as DigitalTwinsModelService from './publicDigitalTwinsModelRepoService';
|
||||
import { API_VERSION, MODEL_REPO_API_VERSION, HTTP_OPERATION_TYPES, PUBLIC_REPO_HOSTNAME } from '../../constants/apiConstants';
|
||||
|
||||
describe('digitalTwinsModelService', () => {
|
||||
|
||||
|
@ -12,8 +11,6 @@ describe('digitalTwinsModelService', () => {
|
|||
const parameters = {
|
||||
expand: undefined,
|
||||
id: 'urn:azureiot:ModelDiscovery:ModelInformation:1',
|
||||
repoServiceHostName: 'canary-repo.azureiotrepository.com',
|
||||
repositoryId: 'repositoryId',
|
||||
token: 'SharedAccessSignature sr=canary-repo.azureiotrepository.com&sig=123&rid=repositoryId'
|
||||
};
|
||||
|
||||
|
@ -59,11 +56,10 @@ describe('digitalTwinsModelService', () => {
|
|||
const result = await DigitalTwinsModelService.fetchModel(parameters);
|
||||
|
||||
const expandQueryString = parameters.expand ? `&expand=true` : ``;
|
||||
const repositoryQueryString = parameters.repositoryId ? `&repositoryId=${parameters.repositoryId}` : '';
|
||||
const apiVersionQuerySTring = `?${API_VERSION}${DIGITAL_TWIN_API_VERSION}`;
|
||||
const queryString = `${apiVersionQuerySTring}${expandQueryString}${repositoryQueryString}`;
|
||||
const apiVersionQuerySTring = `?${API_VERSION}${MODEL_REPO_API_VERSION}`;
|
||||
const queryString = `${apiVersionQuerySTring}${expandQueryString}`;
|
||||
const modelIdentifier = encodeURIComponent(parameters.id);
|
||||
const resourceUrl = `https://${parameters.repoServiceHostName}/models/${modelIdentifier}${queryString}`;
|
||||
const resourceUrl = `https://${PUBLIC_REPO_HOSTNAME}/models/${modelIdentifier}${queryString}`;
|
||||
|
||||
const controllerRequest = {
|
||||
headers: {
|
||||
|
@ -88,9 +84,8 @@ describe('digitalTwinsModelService', () => {
|
|||
|
||||
expect(fetch).toBeCalledWith(DigitalTwinsModelService.CONTROLLER_ENDPOINT, fetchModelParameters);
|
||||
expect(result).toEqual({
|
||||
createdOn: '',
|
||||
createdDate: '',
|
||||
etag: '',
|
||||
lastUpdated: '',
|
||||
model,
|
||||
modelId: '',
|
||||
publisherId: '',
|
||||
|
@ -111,4 +106,46 @@ describe('digitalTwinsModelService', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
context('validateModelDefinitions', () => {
|
||||
const parameters = JSON.stringify([]);
|
||||
|
||||
it('calls fetch with specified parameters and returns true when response is 200', async () => {
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {},
|
||||
ok: true
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const result = await DigitalTwinsModelService.validateModelDefinitions(parameters);
|
||||
|
||||
const apiVersionQueryString = `?${API_VERSION}${MODEL_REPO_API_VERSION}`;
|
||||
const controllerRequest = {
|
||||
body: parameters,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'x-ms-client-request-id': 'azure iot explorer: validate model definition'
|
||||
},
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
uri: `https://${PUBLIC_REPO_HOSTNAME}/models/validate${apiVersionQueryString}`
|
||||
};
|
||||
|
||||
const validateModelParameters = {
|
||||
body: JSON.stringify(controllerRequest),
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
headers: new Headers({
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
method: HTTP_OPERATION_TYPES.Post
|
||||
};
|
||||
|
||||
expect(fetch).toBeCalledWith(DigitalTwinsModelService.CONTROLLER_ENDPOINT, validateModelParameters);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,19 +7,20 @@ import { FetchModelParameters } from '../parameters/repoParameters';
|
|||
import {
|
||||
API_VERSION,
|
||||
CONTROLLER_API_ENDPOINT,
|
||||
DIGITAL_TWIN_API_VERSION,
|
||||
HEADERS,
|
||||
MODELREPO } from '../../constants/apiConstants';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
MODELREPO,
|
||||
MODEL_REPO_API_VERSION,
|
||||
PUBLIC_REPO_HOSTNAME,
|
||||
HTTP_OPERATION_TYPES } from '../../constants/apiConstants';
|
||||
import { PnPModel } from '../models/metamodelMetadata';
|
||||
import { getHeaderValue } from '../shared/fetchUtils';
|
||||
|
||||
export const fetchModel = async (parameters: FetchModelParameters): Promise<PnPModel> => {
|
||||
const expandQueryString = parameters.expand ? `&expand=true` : ``;
|
||||
const repositoryQueryString = parameters.repositoryId ? `&repositoryId=${parameters.repositoryId}` : '';
|
||||
const apiVersionQuerySTring = `?${API_VERSION}${DIGITAL_TWIN_API_VERSION}`;
|
||||
const queryString = `${apiVersionQuerySTring}${expandQueryString}${repositoryQueryString}`;
|
||||
const apiVersionQuerySTring = `?${API_VERSION}${MODEL_REPO_API_VERSION}`;
|
||||
const queryString = `${apiVersionQuerySTring}${expandQueryString}`;
|
||||
const modelIdentifier = encodeURIComponent(parameters.id);
|
||||
const resourceUrl = `https://${parameters.repoServiceHostName}/models/${modelIdentifier}${queryString}`;
|
||||
const resourceUrl = `https://${PUBLIC_REPO_HOSTNAME}/models/${modelIdentifier}${queryString}`;
|
||||
|
||||
const controllerRequest: RequestInitWithUri = {
|
||||
headers: {
|
||||
|
@ -36,33 +37,40 @@ export const fetchModel = async (parameters: FetchModelParameters): Promise<PnPM
|
|||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:cyclomatic-complexity
|
||||
const model = await response.json() as ModelDefinition;
|
||||
const createdOn = response.headers.has(HEADERS.CREATED_ON) ? response.headers.get(HEADERS.CREATED_ON) : '';
|
||||
const etag = response.headers.has(HEADERS.ETAG) ? response.headers.get(HEADERS.ETAG) : '';
|
||||
const lastUpdated = response.headers.has(HEADERS.LAST_UPDATED) ? response.headers.get(HEADERS.LAST_UPDATED) : '';
|
||||
const modelId = response.headers.has(HEADERS.MODEL_ID) ? response.headers.get(HEADERS.MODEL_ID) : '';
|
||||
const publisherId = response.headers.has(HEADERS.PUBLISHER_ID) ? response.headers.get(HEADERS.PUBLISHER_ID) : '';
|
||||
const publisherName = response.headers.has(HEADERS.PUBLISHER_NAME) ? response.headers.get(HEADERS.PUBLISHER_NAME) : '';
|
||||
const pnpModel = {
|
||||
createdOn,
|
||||
etag,
|
||||
lastUpdated,
|
||||
return {
|
||||
createdDate: getHeaderValue(response, HEADERS.MODEL_CREATED_DATE),
|
||||
etag: getHeaderValue(response, HEADERS.ETAG),
|
||||
model,
|
||||
modelId,
|
||||
publisherId,
|
||||
publisherName,
|
||||
modelId: getHeaderValue(response, HEADERS.MODEL_ID),
|
||||
publisherId: getHeaderValue(response, HEADERS.MODEL_PUBLISHER_ID),
|
||||
publisherName: getHeaderValue(response, HEADERS.MODEL_PUBLISHER_NAME)
|
||||
};
|
||||
|
||||
return pnpModel;
|
||||
};
|
||||
|
||||
export const fetchModelDefinition = async (parameters: FetchModelParameters) => {
|
||||
export const fetchModelDefinition = async (parameters: FetchModelParameters): Promise<ModelDefinition> => {
|
||||
const result = await fetchModel({
|
||||
...parameters
|
||||
});
|
||||
|
||||
return result.model;
|
||||
};
|
||||
|
||||
export const validateModelDefinitions = async (modelDefinitions: string) => {
|
||||
try {
|
||||
const result = await fetchModel({
|
||||
...parameters
|
||||
});
|
||||
return result.model;
|
||||
const apiVersionQueryString = `?${API_VERSION}${MODEL_REPO_API_VERSION}`;
|
||||
const controllerRequest: RequestInitWithUri = {
|
||||
body: modelDefinitions,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'x-ms-client-request-id': 'azure iot explorer: validate model definition'
|
||||
},
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
uri: `https://${PUBLIC_REPO_HOSTNAME}/models/validate${apiVersionQueryString}`
|
||||
};
|
||||
|
||||
return (await request(controllerRequest)).ok;
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(error);
|
||||
|
@ -82,6 +90,7 @@ export interface RepoConnectionSettings {
|
|||
}
|
||||
|
||||
export const CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${MODELREPO}`;
|
||||
|
||||
const request = async (requestInit: RequestInitWithUri) => {
|
||||
return fetch(
|
||||
CONTROLLER_ENDPOINT,
|
|
@ -0,0 +1,50 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { getHeaderValue } from './fetchUtils';
|
||||
|
||||
const headers = {
|
||||
has: (name: string) => false,
|
||||
};
|
||||
|
||||
describe('getHeaderValue', () => {
|
||||
it('returns undefinedValue when response is falsy', () => {
|
||||
expect(getHeaderValue(undefined, 'HEADER_NAME', 'undefinedValue')).toEqual('undefinedValue');
|
||||
});
|
||||
|
||||
it('returns undefinedValue when response.Headers is falsy', () => {
|
||||
expect(getHeaderValue({headers: undefined}, 'HEADER_NAME', 'undefinedValue')).toEqual('undefinedValue');
|
||||
});
|
||||
|
||||
it ('returns undefinedValue when header name is falsy', () => {
|
||||
const response = {
|
||||
headers: {
|
||||
has: (name: string) => false
|
||||
}
|
||||
};
|
||||
// tslint:disable-next-line: no-any
|
||||
expect(getHeaderValue(response as any, undefined, 'undefinedValue')).toEqual('undefinedValue');
|
||||
});
|
||||
|
||||
it('returns undefined value when header not present', () => {
|
||||
const response = {
|
||||
headers: {
|
||||
has: (name: string) => false
|
||||
}
|
||||
};
|
||||
// tslint:disable-next-line: no-any
|
||||
expect(getHeaderValue(response as any, 'HEADER_NAME', 'undefinedValue')).toEqual('undefinedValue');
|
||||
});
|
||||
|
||||
it('returns header', () => {
|
||||
const response = {
|
||||
headers: {
|
||||
get: (name: string) => 'value',
|
||||
has: (name: string) => true
|
||||
}
|
||||
};
|
||||
// tslint:disable-next-line: no-any
|
||||
expect(getHeaderValue(response as any, 'HEADER_NAME')).toEqual('value');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export const getHeaderValue = (response: { headers: Headers }, headerName: string, undefinedValue: string = ''): string => {
|
||||
if (!response || !response.headers || !headerName) {
|
||||
return undefinedValue;
|
||||
}
|
||||
|
||||
return response.headers.has(headerName) ? response.headers.get(headerName) : undefinedValue;
|
||||
};
|
|
@ -44,7 +44,7 @@ describe('getResourceTypeFromHostName', () => {
|
|||
|
||||
describe('tryGetHostNameFromConnectionString', () => {
|
||||
it('returns empty string when falsy value provided', () => {
|
||||
expect(tryGetHostNameFromConnectionString(undefined)).toEqual('');
|
||||
expect(tryGetHostNameFromConnectionString(undefined)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns expected value from getConnectionStringInfo', () => {
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { StateInterface } from '../../shared/redux/state';
|
||||
import { notificationsStateInterfaceInitial } from '../../notifications/state';
|
||||
import { deviceContentStateInitial } from '../../devices/deviceContent/state';
|
||||
import { deviceListStateInitial } from '../../devices/deviceList/state';
|
||||
import { applicationStateInitial } from '../../settings/state';
|
||||
import { azureResourceStateInitial } from '../../azureResource/state';
|
||||
import { connectionStringsStateInitial } from '../../connectionStrings/state';
|
||||
import { moduleStateInitial } from './../../devices/module/state';
|
||||
import { iotHubStateInitial } from '../../../app/iotHub/state';
|
||||
|
||||
export const getInitialState = (): StateInterface => {
|
||||
return {
|
||||
applicationState: applicationStateInitial(),
|
||||
azureResourceState: azureResourceStateInitial(),
|
||||
connectionStringsState: connectionStringsStateInitial(),
|
||||
deviceContentState: deviceContentStateInitial(),
|
||||
deviceListState: deviceListStateInitial(),
|
||||
iotHubState: iotHubStateInitial(),
|
||||
moduleState: moduleStateInitial(),
|
||||
notificationsState: notificationsStateInterfaceInitial
|
||||
};
|
||||
};
|
|
@ -9,20 +9,6 @@ import { LIST_PLUG_AND_PLAY_DEVICES } from '../../constants/devices';
|
|||
|
||||
describe('utils', () => {
|
||||
it('builds query string', () => {
|
||||
expect(utils.buildQueryString(
|
||||
{
|
||||
clauses: [
|
||||
{
|
||||
operation: OperationType.equals,
|
||||
parameterType: ParameterType.capabilityModelId,
|
||||
value: 'enabled'
|
||||
}
|
||||
],
|
||||
continuationTokens: [],
|
||||
currentPageIndex: 1,
|
||||
deviceId: '',
|
||||
}
|
||||
)).toEqual(`${LIST_PLUG_AND_PLAY_DEVICES} WHERE (HAS_CAPABILITYMODEL('enabled'))`);
|
||||
expect(utils.buildQueryString(
|
||||
{
|
||||
clauses: [],
|
||||
|
@ -31,6 +17,7 @@ describe('utils', () => {
|
|||
deviceId: 'device1',
|
||||
}
|
||||
)).toEqual(`${LIST_PLUG_AND_PLAY_DEVICES} WHERE STARTSWITH(devices.deviceId, 'device1')`);
|
||||
|
||||
expect(utils.buildQueryString(
|
||||
{
|
||||
clauses: [],
|
||||
|
@ -39,9 +26,11 @@ describe('utils', () => {
|
|||
deviceId: '',
|
||||
}
|
||||
)).toEqual(LIST_PLUG_AND_PLAY_DEVICES + ' ');
|
||||
|
||||
expect(utils.buildQueryString(
|
||||
null
|
||||
)).toEqual(LIST_PLUG_AND_PLAY_DEVICES);
|
||||
|
||||
expect(utils.buildQueryString(
|
||||
{
|
||||
clauses: [
|
||||
|
@ -56,6 +45,7 @@ describe('utils', () => {
|
|||
deviceId: '',
|
||||
}
|
||||
)).toEqual(`${LIST_PLUG_AND_PLAY_DEVICES} WHERE (${ParameterType.edge}=true)`);
|
||||
|
||||
expect(utils.buildQueryString(
|
||||
{
|
||||
clauses: [
|
||||
|
@ -73,22 +63,6 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
it('converts query object to string', () => {
|
||||
expect(utils.queryToString(
|
||||
{
|
||||
clauses: [
|
||||
{
|
||||
operation: OperationType.equals,
|
||||
parameterType: ParameterType.capabilityModelId,
|
||||
value: 'enabled'
|
||||
},
|
||||
{
|
||||
}
|
||||
],
|
||||
continuationTokens: [],
|
||||
currentPageIndex: 1,
|
||||
deviceId: '',
|
||||
}
|
||||
)).toEqual(`WHERE (HAS_CAPABILITYMODEL('enabled'))`);
|
||||
expect(utils.queryToString(
|
||||
{
|
||||
clauses: [],
|
||||
|
@ -97,6 +71,7 @@ describe('utils', () => {
|
|||
deviceId: 'device1',
|
||||
}
|
||||
)).toEqual(`WHERE STARTSWITH(devices.deviceId, 'device1')`);
|
||||
|
||||
expect(utils.queryToString(
|
||||
{
|
||||
clauses: [],
|
||||
|
@ -121,22 +96,6 @@ describe('utils', () => {
|
|||
value: 'disabled'
|
||||
}
|
||||
])).toEqual(`status='enabled' AND status='disabled'`);
|
||||
|
||||
expect(utils.clauseListToString([
|
||||
{
|
||||
operation: OperationType.equals,
|
||||
parameterType: ParameterType.capabilityModelId,
|
||||
value: 'enabled'
|
||||
}
|
||||
])).toEqual(`HAS_CAPABILITYMODEL('enabled')`);
|
||||
|
||||
expect(utils.clauseListToString([
|
||||
{
|
||||
operation: OperationType.equals,
|
||||
parameterType: ParameterType.interfaceId,
|
||||
value: 'enabled'
|
||||
}
|
||||
])).toEqual(`HAS_INTERFACE('enabled')`);
|
||||
});
|
||||
|
||||
it('creates clause item as string', () => {
|
||||
|
@ -167,14 +126,6 @@ describe('utils', () => {
|
|||
expect(connectionObject.sharedAccessKey = 'key');
|
||||
});
|
||||
|
||||
it('gets connectionObject from repo connection string', () => {
|
||||
const connectionObject = utils.getRepoConnectionInfoFromConnectionString('HostName=test.azureiotrepository.com;RepositoryId=123;SharedAccessKeyName=456;SharedAccessKey=key');
|
||||
expect(connectionObject.hostName = 'test.azureiotrepository.com');
|
||||
expect(connectionObject.repositoryId = '123');
|
||||
expect(connectionObject.sharedAccessKeyName = '456');
|
||||
expect(connectionObject.sharedAccessKey = 'key');
|
||||
});
|
||||
|
||||
it('generates hub sas token ', () => {
|
||||
const token = utils.generateSasToken({
|
||||
key: 'key',
|
||||
|
@ -184,10 +135,4 @@ describe('utils', () => {
|
|||
const regex = new RegExp(/^SharedAccessSignature sr=test\.azureiotrepository\.com&sig=.*&se=.*&skn=iothubowner$/);
|
||||
expect(regex.test(token)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('generates repo sas token ', () => {
|
||||
const token = utils.generatePnpSasToken('123', 'test.azureiotrepository.com', '456', 'key');
|
||||
const regex = new RegExp(/^SharedAccessSignature sr=test\.azureiotrepository\.com&sig=.*&se=.*&rid=123$/);
|
||||
expect(regex.test(token)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,10 +5,8 @@
|
|||
import { createHmac } from 'crypto';
|
||||
import { IoTHubConnectionSettings } from '../services/devicesService';
|
||||
import { LIST_PLUG_AND_PLAY_DEVICES, SAS_EXPIRES_MINUTES } from '../../constants/devices';
|
||||
import DeviceQuery, { QueryClause, ParameterType, OperationType } from '../models/deviceQuery';
|
||||
import { RepoConnectionSettings } from '../services/digitalTwinsModelService';
|
||||
import { AppEnvironment } from '../../constants/shared';
|
||||
import { MILLISECONDS_PER_SECOND, SECONDS_PER_MINUTE } from '../constants';
|
||||
import { DeviceQuery, QueryClause, ParameterType, OperationType } from '../models/deviceQuery';
|
||||
import { MILLISECONDS_PER_SECOND, SECONDS_PER_MINUTE } from '../../constants/shared';
|
||||
|
||||
export const enum PnPQueryPrefix {
|
||||
HAS_CAPABILITY_MODEL = 'HAS_CAPABILITYMODEL',
|
||||
|
@ -54,50 +52,11 @@ export const generateSasToken = (parameters: GenerateSasTokenParameters) => {
|
|||
return token;
|
||||
};
|
||||
|
||||
export const generatePnpSasToken = (repositoryId: string, audience: string, secret: string, keyName: string) => {
|
||||
const now = new Date();
|
||||
const ms = 1000;
|
||||
const expiry = (now.setDate(now.getDate() + 1) / ms).toFixed(0);
|
||||
const encodedServiceEndpoint = encodeURIComponent(audience);
|
||||
const encodedRepoId = encodeURIComponent(repositoryId);
|
||||
const signature = [encodedRepoId, encodedServiceEndpoint, expiry].join('\n').toLowerCase();
|
||||
const sigUTF8 = new Buffer(signature, 'utf8');
|
||||
const secret64bit = new Buffer(secret, 'base64');
|
||||
const hmac = createHmac('sha256', secret64bit);
|
||||
hmac.update(sigUTF8);
|
||||
const hash = encodeURIComponent(hmac.digest('base64'));
|
||||
return `SharedAccessSignature sr=${encodedServiceEndpoint}&sig=${hash}&se=${expiry}&skn=${keyName}&rid=${repositoryId}`;
|
||||
};
|
||||
|
||||
export const getRepoConnectionInfoFromConnectionString = (connectionString: string): RepoConnectionSettings => {
|
||||
const connectionObject: RepoConnectionSettings = {};
|
||||
connectionString.split(';')
|
||||
.forEach((segment: string) => {
|
||||
const keyValue = segment.split('=');
|
||||
switch (keyValue[0]) {
|
||||
case 'HostName':
|
||||
connectionObject.hostName = keyValue[1];
|
||||
break;
|
||||
case 'SharedAccessKeyName':
|
||||
connectionObject.sharedAccessKeyName = keyValue[1];
|
||||
break;
|
||||
case 'SharedAccessKey':
|
||||
connectionObject.sharedAccessKey = keyValue[1];
|
||||
break;
|
||||
case 'RepositoryId':
|
||||
connectionObject.repositoryId = keyValue[1];
|
||||
break;
|
||||
default:
|
||||
// we don't use other parts of connection string
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return connectionObject;
|
||||
};
|
||||
|
||||
export const getConnectionInfoFromConnectionString = (connectionString: string): IoTHubConnectionSettings => {
|
||||
const connectionObject: IoTHubConnectionSettings = {};
|
||||
if (!connectionString) {
|
||||
return connectionObject;
|
||||
}
|
||||
connectionString.split(';')
|
||||
.forEach((segment: string) => {
|
||||
const keyValue = segment.split('=');
|
||||
|
@ -143,10 +102,6 @@ export const clauseListToString = (clauses: QueryClause[]) => {
|
|||
|
||||
export const clauseToString = (clause: QueryClause) => {
|
||||
switch (clause.parameterType) {
|
||||
case ParameterType.capabilityModelId:
|
||||
return toPnPClause(PnPQueryPrefix.HAS_CAPABILITY_MODEL, clause.value);
|
||||
case ParameterType.interfaceId:
|
||||
return toPnPClause(PnPQueryPrefix.HAS_INTERFACE, clause.value);
|
||||
case ParameterType.edge:
|
||||
return toEdgeClause(clause.parameterType, clause.value);
|
||||
case ParameterType.status:
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import {
|
||||
setActiveAzureResourceAction,
|
||||
setActiveAzureResourceByConnectionStringAction,
|
||||
SetActiveAzureResourceByConnectionStringActionParameters,
|
||||
setActiveAzureResourceByHostNameAction,
|
||||
SetActiveAzureResourceByHostNameActionParameters
|
||||
} from './actions';
|
||||
import { AzureResource } from './models/AzureResource';
|
||||
import { AccessVerificationState } from './models/accessVerificationState';
|
||||
|
||||
describe('setActiveAzureResourceAction', () => {
|
||||
it('returns AZURE_RESOURCES/SET action object', () => {
|
||||
const azureResource: AzureResource = {
|
||||
accessVerificationState: AccessVerificationState.Verifying,
|
||||
hostName: 'hostName'
|
||||
};
|
||||
expect(setActiveAzureResourceAction(azureResource)).toEqual({
|
||||
payload: azureResource,
|
||||
type: 'AZURE_RESOURCES/SET'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveAzureResourceByConnectionStringAction', () => {
|
||||
it('returns AZURE_RESOURCES/SET_CONNECTION action object', () => {
|
||||
const parameters: SetActiveAzureResourceByConnectionStringActionParameters = {
|
||||
connectionString: 'connectionstring',
|
||||
hostName: 'hostName'
|
||||
};
|
||||
expect(setActiveAzureResourceByConnectionStringAction(parameters)).toEqual({
|
||||
payload: parameters,
|
||||
type: 'AZURE_RESOURCES/SET_CONNECTION'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveAzureResourceByHostNameAction', () => {
|
||||
it('returns AZURE_RESOURCES/SET_HOST action object', () => {
|
||||
const parameters: SetActiveAzureResourceByHostNameActionParameters = {
|
||||
hostName: 'hostName'
|
||||
};
|
||||
expect(setActiveAzureResourceByHostNameAction(parameters)).toEqual({
|
||||
payload: parameters,
|
||||
type: 'AZURE_RESOURCES/SET_HOST'
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import { SET } from '../constants/actionTypes';
|
||||
import { AzureResource } from './models/azureResource';
|
||||
|
||||
export const AZURE_RESOURCES = 'AZURE_RESOURCES';
|
||||
export const BY_CONNECTION = '_CONNECTION';
|
||||
export const BY_HOSTNAME = '_HOST';
|
||||
|
||||
const actionCreator = actionCreatorFactory(AZURE_RESOURCES);
|
||||
|
||||
export interface SetActiveAzureResourceByConnectionStringActionParameters {
|
||||
connectionString: string;
|
||||
hostName: string;
|
||||
}
|
||||
|
||||
export interface SetActiveAzureResourceByHostNameActionParameters {
|
||||
hostName: string;
|
||||
}
|
||||
|
||||
export const setActiveAzureResourceByConnectionStringAction = actionCreator<SetActiveAzureResourceByConnectionStringActionParameters>(`${SET}${BY_CONNECTION}`);
|
||||
export const setActiveAzureResourceByHostNameAction = actionCreator<SetActiveAzureResourceByHostNameActionParameters>(`${SET}${BY_HOSTNAME}`);
|
||||
export const setActiveAzureResourceAction = actionCreator<AzureResource>(SET);
|
|
@ -1,48 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AzureResourceView matches snapshot when current resource is authorized 1`] = `
|
||||
<Fragment>
|
||||
<Route
|
||||
component={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"displayName": "Connect(withRouter(DeviceListComponent))",
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
exact={true}
|
||||
path="url/devices"
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
exact={true}
|
||||
path="url/devices/add"
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
path="url/devices/deviceDetail/"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`AzureResourceView matches snapshot when current resource is failed 1`] = `
|
||||
<div>
|
||||
azureResource.access.failed
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AzureResourceView matches snapshot when current resource is unauthorized 1`] = `
|
||||
<div>
|
||||
azureResource.access.unauthorized
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AzureResourceView matches snapshot when current resource is undefined 1`] = `<Fragment />`;
|
||||
|
||||
exports[`AzureResourceView matches snapshot when current resource is verifying 1`] = `
|
||||
<div>
|
||||
azureResource.access.verifying
|
||||
</div>
|
||||
`;
|
|
@ -1,10 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AzureResourceViewContainer matches snapshot 1`] = `
|
||||
<Component
|
||||
activeAzureResource="active azure resource"
|
||||
currentHostName="hostName"
|
||||
currentUrl="currentUrl"
|
||||
setActiveAzureResourceByHostName={[Function]}
|
||||
/>
|
||||
`;
|
|
@ -1,105 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { AzureResourceView } from './azureResourceView';
|
||||
import { AccessVerificationState } from '../models/accessVerificationState';
|
||||
|
||||
describe('AzureResourceView', () => {
|
||||
it('matches snapshot when current resource is undefined', () => {
|
||||
expect(shallow(
|
||||
<AzureResourceView
|
||||
activeAzureResource={undefined}
|
||||
currentHostName="hostName"
|
||||
currentUrl="url"
|
||||
setActiveAzureResourceByHostName={jest.fn()}
|
||||
/>
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when current resource is verifying', () => {
|
||||
expect(shallow(
|
||||
<AzureResourceView
|
||||
activeAzureResource={{
|
||||
accessVerificationState: AccessVerificationState.Verifying,
|
||||
hostName: 'hostName'
|
||||
}}
|
||||
currentHostName="hostName"
|
||||
currentUrl="url"
|
||||
setActiveAzureResourceByHostName={jest.fn()}
|
||||
/>
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when current resource is failed', () => {
|
||||
expect(shallow(
|
||||
<AzureResourceView
|
||||
activeAzureResource={{
|
||||
accessVerificationState: AccessVerificationState.Failed,
|
||||
hostName: 'hostName'
|
||||
}}
|
||||
currentHostName="hostName"
|
||||
currentUrl="url"
|
||||
setActiveAzureResourceByHostName={jest.fn()}
|
||||
/>
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when current resource is unauthorized', () => {
|
||||
expect(shallow(
|
||||
<AzureResourceView
|
||||
activeAzureResource={{
|
||||
accessVerificationState: AccessVerificationState.Unauthorized,
|
||||
hostName: 'hostName'
|
||||
}}
|
||||
currentHostName="hostName"
|
||||
currentUrl="url"
|
||||
setActiveAzureResourceByHostName={jest.fn()}
|
||||
/>
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when current resource is authorized', () => {
|
||||
expect(shallow(
|
||||
<AzureResourceView
|
||||
activeAzureResource={{
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
hostName: 'hostName'
|
||||
}}
|
||||
currentHostName="hostName"
|
||||
currentUrl="url"
|
||||
setActiveAzureResourceByHostName={jest.fn()}
|
||||
/>
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls setActiveAzureResourceByHostName when hostName changes', () => {
|
||||
jest.spyOn(React, 'useEffect').mockImplementation(f => f());
|
||||
const setActiveAzureResourceByHostName = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<AzureResourceView
|
||||
activeAzureResource={{
|
||||
accessVerificationState: AccessVerificationState.Unauthorized,
|
||||
hostName: 'hostName'
|
||||
}}
|
||||
currentHostName="hostName"
|
||||
currentUrl="url"
|
||||
setActiveAzureResourceByHostName={setActiveAzureResourceByHostName}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.setProps({
|
||||
activeAzureResource: {
|
||||
accessVerificationState: AccessVerificationState.Unauthorized,
|
||||
hostName: 'hostName'
|
||||
},
|
||||
currentHostName: 'newHostName',
|
||||
currentUrl: 'url',
|
||||
setActiveAzureResourceByHostName
|
||||
});
|
||||
|
||||
expect(setActiveAzureResourceByHostName).toHaveBeenCalledWith('newHostName');
|
||||
});
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import DeviceContentContainer from '../../devices/deviceContent/components/deviceContentContainer';
|
||||
import DeviceListContainer from '../../devices/deviceList/components/deviceListContainer';
|
||||
import AddDeviceContainer from '../../devices/deviceList/components/addDevice/components/addDeviceContainer';
|
||||
import { ROUTE_PARTS } from '../../constants/routes';
|
||||
import { AccessVerificationState } from '../models/accessVerificationState';
|
||||
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
|
||||
import { AzureResource } from '../models/azureResource';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
|
||||
export interface AzureResourceViewProps {
|
||||
activeAzureResource: AzureResource | undefined;
|
||||
currentHostName: string;
|
||||
currentUrl: string;
|
||||
setActiveAzureResourceByHostName(hostName: string): void;
|
||||
}
|
||||
|
||||
export const AzureResourceView: React.FC<AzureResourceViewProps> = props => {
|
||||
const { activeAzureResource, currentHostName, currentUrl, setActiveAzureResourceByHostName } = props;
|
||||
const { t } = useLocalizationContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeAzureResource && activeAzureResource.hostName === currentHostName) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveAzureResourceByHostName(currentHostName);
|
||||
}, [currentHostName]); // tslint:disable-line:align
|
||||
|
||||
if (!activeAzureResource) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
if (activeAzureResource.accessVerificationState === AccessVerificationState.Verifying) {
|
||||
return (<div>{t(ResourceKeys.azureResource.access.verifying)}</div>);
|
||||
}
|
||||
|
||||
if (activeAzureResource.accessVerificationState === AccessVerificationState.Unauthorized) {
|
||||
return (<div>{t(ResourceKeys.azureResource.access.unauthorized)}</div>);
|
||||
}
|
||||
|
||||
if (activeAzureResource.accessVerificationState === AccessVerificationState.Failed) {
|
||||
return (<div>{t(ResourceKeys.azureResource.access.failed)}</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Route path={`${currentUrl}/${ROUTE_PARTS.DEVICES}`} component={DeviceListContainer} exact={true}/>
|
||||
<Route path={`${currentUrl}/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.ADD}`} component={AddDeviceContainer} exact={true} />
|
||||
<Route path={`${currentUrl}/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.DEVICE_DETAIL}/`} component={DeviceContentContainer}/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import * as Redux from 'react-redux';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { AzureResourceViewContainer, AzureResourceViewContainerProps } from './azureResourceViewContainer';
|
||||
|
||||
describe('AzureResourceViewContainer', () => {
|
||||
it('matches snapshot', () => {
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(() => 'active azure resource');
|
||||
jest.spyOn(Redux, 'useDispatch').mockImplementation(jest.fn());
|
||||
|
||||
const routerprops: AzureResourceViewContainerProps = {
|
||||
history: jest.fn() as any, // tslint:disable-line:no-any
|
||||
location: jest.fn() as any, // tslint:disable-line:no-any
|
||||
match: {
|
||||
params: {
|
||||
hostName: 'hostName'
|
||||
},
|
||||
url: 'currentUrl',
|
||||
} as any // tslint:disable-line:no-any
|
||||
};
|
||||
|
||||
expect(shallow(
|
||||
<AzureResourceViewContainer {...routerprops} />
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { setActiveAzureResourceByHostNameAction } from '../actions';
|
||||
import { AzureResourceView, AzureResourceViewProps } from './azureResourceView';
|
||||
import { getActiveAzureResourceSelector } from '../selectors';
|
||||
|
||||
export type AzureResourceViewContainerProps = RouteComponentProps;
|
||||
export const AzureResourceViewContainer: React.FC<AzureResourceViewContainerProps> = props => {
|
||||
const currentUrl = props.match.url;
|
||||
const currentHostName = (props.match.params as { hostName: string}).hostName;
|
||||
const activeAzureResource = useSelector(getActiveAzureResourceSelector);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const setActiveAzureResourceByHostName = (hostName: string) => {
|
||||
dispatch(setActiveAzureResourceByHostNameAction({
|
||||
hostName
|
||||
}));
|
||||
};
|
||||
|
||||
const viewProps: AzureResourceViewProps = {
|
||||
activeAzureResource,
|
||||
currentHostName,
|
||||
currentUrl,
|
||||
setActiveAzureResourceByHostName
|
||||
};
|
||||
|
||||
return (
|
||||
<AzureResourceView {...viewProps} />
|
||||
);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { AccessVerificationState } from './accessVerificationState';
|
||||
import { AzureResourceIdentifier } from '../../azureResourceIdentifier/models/azureResourceIdentifier';
|
||||
|
||||
export interface AzureResource {
|
||||
accessVerificationState: AccessVerificationState;
|
||||
azureResourceIdentifier?: AzureResourceIdentifier;
|
||||
hostName: string;
|
||||
connectionString?: string;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { AzureResourceStateInterface } from './state';
|
||||
import { setActiveAzureResourceAction } from './actions';
|
||||
import { AzureResource } from './models/azureResource';;
|
||||
import reducer from './reducer';
|
||||
import { AccessVerificationState } from './models/accessVerificationState';
|
||||
|
||||
describe('setActiveAzureResourceAction', () => {
|
||||
it('sets entire azure resource', () => {
|
||||
const initialState: AzureResourceStateInterface = {
|
||||
activeAzureResource: undefined
|
||||
};
|
||||
|
||||
const resource: AzureResource = {
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
connectionString: 'connection',
|
||||
hostName: 'hostName'
|
||||
};
|
||||
const action = setActiveAzureResourceAction(resource);
|
||||
const result = reducer(initialState, action);
|
||||
|
||||
expect(result.activeAzureResource).toEqual(resource);
|
||||
});
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
import { azureResourceStateInitial, AzureResourceStateInterface } from './state';
|
||||
import { setActiveAzureResourceAction } from './actions';
|
||||
import { AzureResource } from './models/azureResource';
|
||||
|
||||
const reducer = reducerWithInitialState<AzureResourceStateInterface>(azureResourceStateInitial())
|
||||
.case(setActiveAzureResourceAction, (state: AzureResourceStateInterface, payload: AzureResource) => {
|
||||
state.activeAzureResource = payload;
|
||||
return state;
|
||||
});
|
||||
|
||||
export default reducer;
|
|
@ -1,20 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import rootSaga from './sagas';
|
||||
import { setActiveAzureResourceByConnectionStringAction, setActiveAzureResourceByHostNameAction, setActiveAzureResourceAction } from './actions';
|
||||
import { setActiveAzureResourceByConnectionStringSaga } from './sagas/setActiveAzureResourceByConnectionStringSaga';
|
||||
import { setActiveAzureResourceByHostNameSaga } from './sagas/setActiveAzureResourceByHostNameSaga';
|
||||
import { setActiveAzureResourceSaga } from './sagas/setActiveAzureResourceSaga';
|
||||
|
||||
describe('connectionStrings/saga/rootSaga', () => {
|
||||
it('returns specified sagas', () => {
|
||||
expect(rootSaga).toEqual([
|
||||
takeLatest(setActiveAzureResourceByConnectionStringAction, setActiveAzureResourceByConnectionStringSaga),
|
||||
takeLatest(setActiveAzureResourceByHostNameAction, setActiveAzureResourceByHostNameSaga),
|
||||
takeLatest(setActiveAzureResourceAction, setActiveAzureResourceSaga)
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import { setActiveAzureResourceByConnectionStringAction, setActiveAzureResourceByHostNameAction, setActiveAzureResourceAction } from './actions';
|
||||
import { setActiveAzureResourceByConnectionStringSaga } from './sagas/setActiveAzureResourceByConnectionStringSaga';
|
||||
import { setActiveAzureResourceByHostNameSaga } from './sagas/setActiveAzureResourceByHostNameSaga';
|
||||
import { setActiveAzureResourceSaga } from './sagas/setActiveAzureResourceSaga';
|
||||
|
||||
export default [
|
||||
takeLatest(setActiveAzureResourceByConnectionStringAction, setActiveAzureResourceByConnectionStringSaga),
|
||||
takeLatest(setActiveAzureResourceByHostNameAction, setActiveAzureResourceByHostNameSaga),
|
||||
takeLatest(setActiveAzureResourceAction, setActiveAzureResourceSaga)
|
||||
];
|
|
@ -1,146 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { select, call } from 'redux-saga/effects';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { getActiveAzureResourceConnectionStringSaga, getActiveAzureResource, getAuthMode } from './getActiveAzureResourceConnectionStringSaga';
|
||||
import { appConfig, AuthMode } from '../../../appConfig/appConfig';
|
||||
import { AzureResourceIdentifierType } from '../../azureResourceIdentifier/models/azureResourceIdentifierType';
|
||||
import { getConnectionStringFromIotHubSaga } from '../../iotHub/sagas/getConnectionStringFromIotHubSaga';
|
||||
|
||||
describe('getActiveAzureResourceConnectionStringSaga', () => {
|
||||
const getActiveAzureResourceConnectionStringSagaGenerator = cloneableGenerator(getActiveAzureResourceConnectionStringSaga)();
|
||||
|
||||
it('yields select effect to get active resource', () => {
|
||||
expect(getActiveAzureResourceConnectionStringSagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: select(getActiveAzureResource)
|
||||
});
|
||||
getActiveAzureResourceConnectionStringSagaGenerator.next();
|
||||
});
|
||||
|
||||
describe('azure resource undefined', () => {
|
||||
const saga = getActiveAzureResourceConnectionStringSagaGenerator.clone();
|
||||
it('finishes with return value of empty string', () => {
|
||||
saga.next(); // calls the selector function
|
||||
expect(saga.next(undefined)).toEqual({
|
||||
done: true,
|
||||
value: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('azure resource defined - connection string', () => {
|
||||
const saga = getActiveAzureResourceConnectionStringSagaGenerator.clone();
|
||||
const azureResource = {
|
||||
connectionString: 'connectionString',
|
||||
hostName: 'hub1'
|
||||
};
|
||||
|
||||
saga.next(); // calls the selector function
|
||||
|
||||
it('yields call to getAuthMode', () => {
|
||||
expect(saga.next(azureResource)).toEqual({
|
||||
done: false,
|
||||
value: call(getAuthMode)
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes with return value of connectionString', () => {
|
||||
expect(saga.next(AuthMode.ConnectionString)).toEqual({
|
||||
done: true,
|
||||
value: azureResource.connectionString
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('azure resource defined -- sso auth flow', () => {
|
||||
const saga = getActiveAzureResourceConnectionStringSagaGenerator.clone();
|
||||
const azureResource = {
|
||||
azureResourceIdentifier: {
|
||||
id: 'id1',
|
||||
location: 'location1',
|
||||
name: 'name1',
|
||||
resourceGroup: 'resourceGroup1',
|
||||
subscriptionId: 'sub1',
|
||||
type: AzureResourceIdentifierType.IotHub
|
||||
},
|
||||
hostName: 'hub1'
|
||||
};
|
||||
|
||||
saga.next(); // calls the selector function
|
||||
|
||||
it('yields call to getAuthMode', () => {
|
||||
expect(saga.next(azureResource)).toEqual({
|
||||
done: false,
|
||||
value: call(getAuthMode)
|
||||
});
|
||||
});
|
||||
|
||||
it('yields call to getConnectionStringFromIotHubSaga', () => {
|
||||
expect(saga.next(AuthMode.ImplicitFlow)).toEqual({
|
||||
done: false,
|
||||
value: call(getConnectionStringFromIotHubSaga, azureResource.azureResourceIdentifier)
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes by returning the connection string', () => {
|
||||
expect(saga.next('connectionString1')).toEqual({
|
||||
done: true,
|
||||
value: 'connectionString1'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('azure resource defined - sso but unmapped resource identifier type', () => {
|
||||
const saga = getActiveAzureResourceConnectionStringSagaGenerator.clone();
|
||||
const azureResource = {
|
||||
azureResourceIdentifier: {
|
||||
id: 'id1',
|
||||
location: 'location1',
|
||||
name: 'name1',
|
||||
resourceGroup: 'resourceGroup1',
|
||||
subscriptionId: 'sub1',
|
||||
type: AzureResourceIdentifierType.DeviceProvisioningService
|
||||
},
|
||||
hostName: 'dps1'
|
||||
};
|
||||
|
||||
saga.next(); // calls the selector function
|
||||
|
||||
it('yields call to getAuthMode', () => {
|
||||
expect(saga.next(azureResource)).toEqual({
|
||||
done: false,
|
||||
value: call(getAuthMode)
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes by returning empty connection string', () => {
|
||||
expect(saga.next(AuthMode.ImplicitFlow)).toEqual({
|
||||
done: true,
|
||||
value: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAuthMode', () => {
|
||||
it('returns expected value', () => {
|
||||
const authMode = appConfig.authMode;
|
||||
expect(getAuthMode()).toEqual(authMode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveAzureResource', () => {
|
||||
it('returns expected value', () => {
|
||||
const state = {
|
||||
azureResourceState: {
|
||||
activeAzureResource: 'activeAzureResource'
|
||||
}
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
expect(getActiveAzureResource(state as any)).toEqual('activeAzureResource');
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { select, call } from 'redux-saga/effects';
|
||||
import { StateInterface } from '../../shared/redux/state';
|
||||
import { AzureResource } from '../models/azureResource';
|
||||
import { appConfig, AuthMode } from '../../../appConfig/appConfig';
|
||||
import { AzureResourceIdentifierType } from '../../azureResourceIdentifier/models/azureResourceIdentifierType';
|
||||
import { getConnectionStringFromIotHubSaga } from '../../iotHub/sagas/getConnectionStringFromIotHubSaga';
|
||||
|
||||
export function* getActiveAzureResourceConnectionStringSaga() {
|
||||
const activeAzureResource: AzureResource = yield select(getActiveAzureResource);
|
||||
if (!activeAzureResource) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const authMode = yield call(getAuthMode);
|
||||
if (authMode === AuthMode.ConnectionString) {
|
||||
return activeAzureResource.connectionString;
|
||||
}
|
||||
|
||||
if (activeAzureResource.azureResourceIdentifier &&
|
||||
activeAzureResource.azureResourceIdentifier.type === AzureResourceIdentifierType.IotHub) {
|
||||
const connectionString = yield call(getConnectionStringFromIotHubSaga, activeAzureResource.azureResourceIdentifier);
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export const getActiveAzureResource = (state: StateInterface) => {
|
||||
return state.azureResourceState.activeAzureResource;
|
||||
};
|
||||
|
||||
export const getAuthMode = (): AuthMode => {
|
||||
return appConfig.authMode;
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { setActiveAzureResourceByConnectionStringSaga } from './setActiveAzureResourceByConnectionStringSaga';
|
||||
import { setActiveAzureResourceByConnectionStringAction, SetActiveAzureResourceByConnectionStringActionParameters , setActiveAzureResourceAction } from '../actions';
|
||||
import { AccessVerificationState } from '../models/accessVerificationState';
|
||||
|
||||
describe('setActiveAzureResourceByConnectionStringSaga', () => {
|
||||
const parameters: SetActiveAzureResourceByConnectionStringActionParameters = {
|
||||
connectionString: 'connectionString',
|
||||
hostName: 'hostname'
|
||||
};
|
||||
const setActiveAzureResourceByConnectionStringSagaGenerator = cloneableGenerator(setActiveAzureResourceByConnectionStringSaga)(setActiveAzureResourceByConnectionStringAction(parameters));
|
||||
it('yields put effect to setActiveAzureResourceAction', () => {
|
||||
expect(setActiveAzureResourceByConnectionStringSagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
connectionString: 'connectionString',
|
||||
hostName: 'hostname',
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
it ('finishes', () => {
|
||||
expect(setActiveAzureResourceByConnectionStringSagaGenerator.next()).toEqual({
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { Action } from 'typescript-fsa';
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { AzureResource } from '../models/azureResource';
|
||||
import { setActiveAzureResourceAction, SetActiveAzureResourceByConnectionStringActionParameters } from '../actions';
|
||||
import { AccessVerificationState } from '../models/accessVerificationState';
|
||||
|
||||
export function* setActiveAzureResourceByConnectionStringSaga(action: Action<SetActiveAzureResourceByConnectionStringActionParameters>) {
|
||||
const { connectionString, hostName } = action.payload;
|
||||
const azureResource: AzureResource = {
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
connectionString,
|
||||
hostName
|
||||
};
|
||||
|
||||
yield put(setActiveAzureResourceAction(azureResource));
|
||||
}
|
|
@ -1,311 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put, select } from 'redux-saga/effects';
|
||||
import {
|
||||
setActiveAzureResourceByHostNameSaga,
|
||||
setActiveAzureResourceByHostNameSaga_ConnectionString,
|
||||
setActiveAzureResourceByHostNameSaga_ImplicitFlow,
|
||||
getAuthMode,
|
||||
getAzureResourceManagementEndpoint,
|
||||
getLastUsedConnectionString } from './setActiveAzureResourceByHostNameSaga';
|
||||
import { setActiveAzureResourceByHostNameAction, SetActiveAzureResourceByHostNameActionParameters, setActiveAzureResourceAction } from '../actions';
|
||||
import { AccessVerificationState } from '../models/accessVerificationState';
|
||||
import * as hostNameUtils from '../../api/shared/hostNameUtils';
|
||||
import { appConfig, AuthMode } from '../../../appConfig/appConfig';
|
||||
import { executeAzureResourceManagementTokenRequest } from '../../login/services/authService';
|
||||
import { getAzureSubscriptions } from '../../azureResourceIdentifier/services/azureSubscriptionService';
|
||||
import { getAzureResourceIdentifier } from '../../azureResourceIdentifier/services/azureResourceIdentifierService';
|
||||
import { AzureResourceIdentifierType } from '../../azureResourceIdentifier/models/azureResourceIdentifierType';
|
||||
|
||||
describe('setActiveAzureResourceByHostNameSaga', () => {
|
||||
const parameters: SetActiveAzureResourceByHostNameActionParameters = {
|
||||
hostName: 'hostname'
|
||||
};
|
||||
|
||||
describe('empty host name scenario', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga(setActiveAzureResourceByHostNameAction({ hostName: ''}));
|
||||
|
||||
it('yields put effect to set active azure resource', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: false,
|
||||
value: put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Failed,
|
||||
hostName: ''
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('connection string auth scenario', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga(setActiveAzureResourceByHostNameAction(parameters));
|
||||
|
||||
it('yields call effect to get authMode', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: false,
|
||||
value: call(getAuthMode)
|
||||
});
|
||||
});
|
||||
|
||||
it('yields call effect to setActiveAzureResourceByHostNameSaga_ConnectionString', () => {
|
||||
expect(saga.next(AuthMode.ConnectionString)).toEqual({
|
||||
done: false,
|
||||
value: call(setActiveAzureResourceByHostNameSaga_ConnectionString, parameters.hostName)
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('implicitFlow scenario', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga(setActiveAzureResourceByHostNameAction(parameters));
|
||||
|
||||
it('yields call effect to get authMode', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: false,
|
||||
value: call(getAuthMode)
|
||||
});
|
||||
});
|
||||
|
||||
it('yields call effect to setActiveAzureResourceByHostNameSaga_ImplicitFlow', () => {
|
||||
expect(saga.next(AuthMode.ImplicitFlow)).toEqual({
|
||||
done: false,
|
||||
value: call(setActiveAzureResourceByHostNameSaga_ImplicitFlow, parameters.hostName)
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveAzureResourceByHostNameSaga_ConnectionString', () => {
|
||||
const setActiveAzureResourceByHostNameSagaConnectionString = setActiveAzureResourceByHostNameSaga_ConnectionString('hostName');
|
||||
|
||||
it('yields selector to get current connection string', () => {
|
||||
expect(setActiveAzureResourceByHostNameSagaConnectionString.next()).toEqual({
|
||||
done: false,
|
||||
value: select(getLastUsedConnectionString)
|
||||
});
|
||||
});
|
||||
|
||||
it('yields call to tryGetHostName from connection string', () => {
|
||||
expect(setActiveAzureResourceByHostNameSagaConnectionString.next('connectionString')).toEqual({
|
||||
done: false,
|
||||
value: call(hostNameUtils.tryGetHostNameFromConnectionString, 'connectionString')
|
||||
});
|
||||
});
|
||||
|
||||
describe('host name matches', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga_ConnectionString('hostName');
|
||||
saga.next();
|
||||
saga.next('connectionString');
|
||||
it('yields put effect to set active resource', () => {
|
||||
expect(saga.next('hostname')).toEqual({
|
||||
done: false,
|
||||
value: put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
connectionString: 'connectionString',
|
||||
hostName: 'hostName'
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('host name does not match', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga_ConnectionString('hostName');
|
||||
saga.next();
|
||||
saga.next(undefined);
|
||||
it('yields put effect to set active resource', () => {
|
||||
expect(saga.next('')).toEqual({
|
||||
done: false,
|
||||
value: put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Unauthorized,
|
||||
hostName: 'hostName'
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveAzureResourceByHostNameSaga_ImplicitFlow', () => {
|
||||
const endpoint = 'endpoint1';
|
||||
const authorizationToken = 'token1';
|
||||
const subscriptions = [{ subscriptionId: 'sub1'}, { subscriptionId: 'sub2'}];
|
||||
const resourceNameSpy = jest.spyOn(hostNameUtils, 'getResourceNameFromHostName');
|
||||
const resourceTypeSpy = jest.spyOn(hostNameUtils, 'getResourceTypeFromHostName');
|
||||
const resourceIdentifier = {
|
||||
id: 'id1',
|
||||
location: 'location1',
|
||||
name: 'name1',
|
||||
resourceGroup: 'resourceGroup1',
|
||||
subscriptionId: 'sub1',
|
||||
type: 'type1'
|
||||
};
|
||||
|
||||
describe('succss path', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga_ImplicitFlow('hostName');
|
||||
|
||||
it('yields call effect o get to getAzureResourceManagementEndpoint', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: false,
|
||||
value: call(getAzureResourceManagementEndpoint)
|
||||
});
|
||||
});
|
||||
|
||||
it('yields call effect to executeAzureResourceManagementTokenRequest', () => {
|
||||
expect(saga.next(endpoint)).toEqual({
|
||||
done: false,
|
||||
value: call(executeAzureResourceManagementTokenRequest)
|
||||
});
|
||||
});
|
||||
|
||||
it('yields call effect to getAzureSubscription', () => {
|
||||
expect(saga.next(authorizationToken)).toEqual({
|
||||
done: false,
|
||||
value: call(getAzureSubscriptions, {
|
||||
azureResourceManagementEndpoint: {
|
||||
authorizationToken,
|
||||
endpoint
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it ('yields call effect to getAzureResourceIdentifier', () => {
|
||||
resourceNameSpy.mockReturnValue('resourceName');
|
||||
resourceTypeSpy.mockReturnValue(AzureResourceIdentifierType.IoTHub);
|
||||
expect(saga.next(subscriptions)).toEqual({
|
||||
done: false,
|
||||
value: call(getAzureResourceIdentifier, {
|
||||
azureResourceManagementEndpoint: {
|
||||
authorizationToken,
|
||||
endpoint
|
||||
},
|
||||
resourceName: 'resourceName',
|
||||
resourceType: AzureResourceIdentifierType.IoTHub,
|
||||
subscriptionIds: ['sub1', 'sub2']
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('yields put effect to setActiveAzureResourceAction', () => {
|
||||
expect(saga.next(resourceIdentifier)).toEqual({
|
||||
done: false,
|
||||
value: put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
azureResourceIdentifier: resourceIdentifier,
|
||||
hostName: 'hostName'
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resource not found path', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga_ImplicitFlow('hostName');
|
||||
saga.next();
|
||||
saga.next(endpoint);
|
||||
saga.next(authorizationToken);
|
||||
saga.next(subscriptions);
|
||||
expect(saga.next(undefined)).toEqual({
|
||||
done: false,
|
||||
value: put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Unauthorized,
|
||||
azureResourceIdentifier: undefined,
|
||||
hostName: 'hostName'
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
describe('exception path', () => {
|
||||
const saga = setActiveAzureResourceByHostNameSaga_ImplicitFlow('hostName');
|
||||
|
||||
it('yields call effect o get to getAzureResourceManagementEndpoint', () => {
|
||||
saga.next();
|
||||
expect(saga.throw()).toEqual({
|
||||
done: false,
|
||||
value: put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Failed,
|
||||
hostName: 'hostName'
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(saga.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getLastUsedConnectionString', () => {
|
||||
it('returns expected value when array has one or more entries', () => {
|
||||
const state = {
|
||||
connectionStringsState: {
|
||||
connectionStrings: ['connection1', 'connection2']
|
||||
}
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
expect(getLastUsedConnectionString(state as any)).toEqual('connection1');
|
||||
});
|
||||
|
||||
it('returns expected value when array has no entries', () => {
|
||||
const state = {
|
||||
connectionStringsState: {
|
||||
connectionStrings: []
|
||||
}
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
expect(getLastUsedConnectionString(state as any)).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAuthMode', () => {
|
||||
it('returns auth mode', () => {
|
||||
const authMode = appConfig.authMode;
|
||||
expect(getAuthMode()).toEqual(authMode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAzureResourceManagementEndpoint', () => {
|
||||
it('returns Azure Resource Management Endpoint', () => {
|
||||
const endpoint = appConfig.azureResourceManagementEndpoint;
|
||||
expect(getAzureResourceManagementEndpoint()).toEqual(endpoint);
|
||||
});
|
||||
});
|
|
@ -1,102 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { Action } from 'typescript-fsa';
|
||||
import { call, put, select } from 'redux-saga/effects';
|
||||
import { setActiveAzureResourceAction, SetActiveAzureResourceByHostNameActionParameters } from '../actions';
|
||||
import { AccessVerificationState } from '../models/accessVerificationState';
|
||||
import { StateInterface } from '../../shared/redux/state';
|
||||
import { executeAzureResourceManagementTokenRequest } from '../../login/services/authService';
|
||||
import { appConfig, AuthMode } from '../../../appConfig/appConfig';
|
||||
import { AzureSubscription } from '../../azureResourceIdentifier/models/azureSubscription';
|
||||
import { getAzureSubscriptions } from '../../azureResourceIdentifier/services/azureSubscriptionService';
|
||||
import { AzureResourceIdentifier } from '../../azureResourceIdentifier/models/azureResourceIdentifier';
|
||||
import { getResourceNameFromHostName, getResourceTypeFromHostName, tryGetHostNameFromConnectionString } from '../../api/shared/hostNameUtils';
|
||||
import { getAzureResourceIdentifier } from '../../azureResourceIdentifier/services/azureResourceIdentifierService';
|
||||
|
||||
export function* setActiveAzureResourceByHostNameSaga(action: Action<SetActiveAzureResourceByHostNameActionParameters>) {
|
||||
const { hostName } = action.payload;
|
||||
|
||||
if (!hostName) {
|
||||
yield put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Failed,
|
||||
hostName
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const authMode = yield call(getAuthMode);
|
||||
|
||||
if (authMode === AuthMode.ConnectionString) {
|
||||
yield call(setActiveAzureResourceByHostNameSaga_ConnectionString, hostName);
|
||||
return;
|
||||
}
|
||||
|
||||
yield call(setActiveAzureResourceByHostNameSaga_ImplicitFlow, hostName);
|
||||
}
|
||||
|
||||
export function* setActiveAzureResourceByHostNameSaga_ConnectionString(hostName: string) {
|
||||
const connectionString = yield select(getLastUsedConnectionString);
|
||||
const connectionStringHostName = yield call(tryGetHostNameFromConnectionString, connectionString);
|
||||
|
||||
if (hostName.toLowerCase() === connectionStringHostName.toLowerCase()) {
|
||||
yield put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
connectionString,
|
||||
hostName
|
||||
}));
|
||||
} else {
|
||||
yield put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Unauthorized,
|
||||
hostName
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setActiveAzureResourceByHostNameSaga_ImplicitFlow(hostName: string) {
|
||||
try {
|
||||
const endpoint: string = yield call(getAzureResourceManagementEndpoint);
|
||||
const authorizationToken: string = yield call(executeAzureResourceManagementTokenRequest);
|
||||
const subscriptions: AzureSubscription[] = yield call(getAzureSubscriptions, {
|
||||
azureResourceManagementEndpoint: {
|
||||
authorizationToken,
|
||||
endpoint
|
||||
}
|
||||
});
|
||||
|
||||
const azureResourceIdentifier: AzureResourceIdentifier = yield call(getAzureResourceIdentifier, {
|
||||
azureResourceManagementEndpoint: {
|
||||
authorizationToken,
|
||||
endpoint
|
||||
},
|
||||
resourceName: getResourceNameFromHostName(hostName),
|
||||
resourceType: getResourceTypeFromHostName(hostName),
|
||||
subscriptionIds: subscriptions.map(s => s.subscriptionId)
|
||||
});
|
||||
|
||||
yield put(setActiveAzureResourceAction({
|
||||
accessVerificationState: azureResourceIdentifier ? AccessVerificationState.Authorized : AccessVerificationState.Unauthorized,
|
||||
azureResourceIdentifier,
|
||||
hostName
|
||||
}));
|
||||
|
||||
} catch {
|
||||
yield put(setActiveAzureResourceAction({
|
||||
accessVerificationState: AccessVerificationState.Failed,
|
||||
hostName
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuthMode = (): AuthMode => {
|
||||
return appConfig.authMode;
|
||||
};
|
||||
|
||||
export const getAzureResourceManagementEndpoint = (): string => {
|
||||
return appConfig.azureResourceManagementEndpoint;
|
||||
};
|
||||
|
||||
export const getLastUsedConnectionString = (state: StateInterface): string => {
|
||||
return state.connectionStringsState.connectionStrings.length > 0 ? state.connectionStringsState.connectionStrings[0] : '';
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { setActiveAzureResourceSaga } from './setActiveAzureResourceSaga';
|
||||
import { setActiveAzureResourceAction } from '../actions';
|
||||
import { AzureResource } from '../models/azureResource';
|
||||
import { AccessVerificationState } from '../models/accessVerificationState';
|
||||
import { clearDevicesAction } from '../../devices/deviceList/actions';
|
||||
import { clearModelDefinitionsAction } from '../../devices/deviceContent/actions';
|
||||
|
||||
describe('setActiveAzureResourceSaga', () => {
|
||||
const resource: AzureResource = {
|
||||
accessVerificationState: AccessVerificationState.Authorized,
|
||||
hostName: 'hostname'
|
||||
};
|
||||
const setActiveAzureResourceSagaGenerator = cloneableGenerator(setActiveAzureResourceSaga)(setActiveAzureResourceAction(resource));
|
||||
|
||||
it('returns put effect to clear devices', () => {
|
||||
expect(setActiveAzureResourceSagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: put(clearDevicesAction())
|
||||
});
|
||||
});
|
||||
|
||||
it('returns put effect to clear model definitions', () => {
|
||||
expect(setActiveAzureResourceSagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: put(clearModelDefinitionsAction())
|
||||
});
|
||||
});
|
||||
|
||||
it('finishes', () => {
|
||||
expect(setActiveAzureResourceSagaGenerator.next()).toEqual({
|
||||
done: true
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { Action } from 'typescript-fsa';
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { AzureResource } from '../models/azureResource';
|
||||
import { clearDevicesAction } from '../../devices/deviceList/actions';
|
||||
import { clearModelDefinitionsAction } from '../../devices/deviceContent/actions';
|
||||
|
||||
export function* setActiveAzureResourceSaga(action: Action<AzureResource>) {
|
||||
yield put(clearDevicesAction());
|
||||
yield put(clearModelDefinitionsAction());
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import { Record } from 'immutable';
|
||||
import {
|
||||
getActiveAzureResourceSelector,
|
||||
getActiveAzureResourceHostNameSelector,
|
||||
getActiveAzureResourceConnectionStringSelector
|
||||
} from './selectors';
|
||||
import { getInitialState } from '../api/shared/testHelper';
|
||||
|
||||
describe('getAzureResourceSelector', () => {
|
||||
const state = getInitialState();
|
||||
const hostName = 'testhub.azure-devices.net';
|
||||
state.azureResourceState = Record({
|
||||
activeAzureResource: {
|
||||
accessVerificationState: null,
|
||||
hostName
|
||||
}
|
||||
})();
|
||||
|
||||
it('returns active azure resource', () => {
|
||||
expect(getActiveAzureResourceSelector(state)).toEqual({
|
||||
accessVerificationState: null,
|
||||
hostName
|
||||
});
|
||||
});
|
||||
|
||||
it('returns active azure resource', () => {
|
||||
expect(getActiveAzureResourceHostNameSelector(state)).toEqual(hostName);
|
||||
});
|
||||
|
||||
it('returns active azure connection string', () => {
|
||||
expect(getActiveAzureResourceConnectionStringSelector(state)).toEqual('');
|
||||
});
|
||||
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { createSelector } from 'reselect';
|
||||
import { StateInterface } from '../shared/redux/state';
|
||||
import { AzureResource } from './models/azureResource';
|
||||
|
||||
export const getActiveAzureResourceSelector = (state: StateInterface): AzureResource => {
|
||||
return state.azureResourceState.activeAzureResource;
|
||||
};
|
||||
|
||||
export const getActiveAzureResourceHostNameSelector = createSelector(getActiveAzureResourceSelector, resource => resource && (resource.hostName || ''));
|
||||
|
||||
export const getActiveAzureResourceConnectionStringSelector = createSelector(getActiveAzureResourceSelector, resource => resource && (resource.connectionString || ''));
|
|
@ -1,13 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { AzureResource } from './models/azureResource';
|
||||
|
||||
export interface AzureResourceStateInterface {
|
||||
activeAzureResource?: AzureResource;
|
||||
}
|
||||
|
||||
export const azureResourceStateInitial = (): AzureResourceStateInterface => {
|
||||
return {};
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
import { getAzureResourceIdentifiers, getAzureResourceIdentifier } from './azureResourceIdentifierService';
|
||||
import { AzureResourceIdentifierType } from '../models/azureResourceIdentifierType';
|
||||
import { HttpError } from '../../api/models/httpError';
|
||||
import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants';
|
||||
import { HTTP_OPERATION_TYPES, APPLICATION_JSON } from '../../constants/apiConstants';
|
||||
|
||||
describe('getAzureResourceIdentifiers', () => {
|
||||
it('calls fetch with specificed parameters', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@ 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 '../../api/constants';
|
||||
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';
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { HTTP_OPERATION_TYPES } from './../../constants/apiConstants';
|
||||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { getAzureSubscriptions } from './azureSubscriptionService';
|
||||
import { HttpError } from '../../api/models/httpError';
|
||||
import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants';
|
||||
import { APPLICATION_JSON } from '../../constants/apiConstants';
|
||||
|
||||
describe('getAzureSubscriptions', () => {
|
||||
it('calls fetch with expected parameters', () => {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { AzureResourceManagementEndpoint } from '../models/azureResourceManagementEndpoint';
|
||||
import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants';
|
||||
import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants';
|
||||
import { HttpError } from '../../api/models/httpError';
|
||||
import { AzureSubscription } from '../models/azureSubscription';
|
||||
|
||||
|
|
|
@ -1,16 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`azureResourcesView matches snapshot 1`] = `
|
||||
<Component
|
||||
history={[MockFunction]}
|
||||
location={[MockFunction]}
|
||||
match={
|
||||
Object {
|
||||
"params": Object {
|
||||
"hostName": "hostName",
|
||||
},
|
||||
"url": "currentUrl",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
exports[`azureResourcesView matches snapshot 1`] = `<Component />`;
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { ConnectionStringsViewContainer } from '../../connectionStrings/components/connectionStringsView';
|
||||
import { ConnectionStringsView } from '../../connectionStrings/components/connectionStringsView';
|
||||
|
||||
export const AzureResourcesView: React.FC<RouteComponentProps> = props => {
|
||||
return <ConnectionStringsViewContainer {...props} />;
|
||||
export const AzureResourcesView: React.FC = () => {
|
||||
return <ConnectionStringsView />;
|
||||
};
|
||||
|
|
|
@ -3,44 +3,44 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import {
|
||||
addConnectionStringAction,
|
||||
getConnectionStringAction,
|
||||
deleteConnectionStringAction,
|
||||
setConnectionStringsAction,
|
||||
upsertConnectionStringAction
|
||||
} from './actions';
|
||||
|
||||
describe('addConnectionStringAction', () => {
|
||||
it('returns CONNECTION_STRINGS/ADD action object', () => {
|
||||
expect(addConnectionStringAction('connectionString')).toEqual({
|
||||
payload: 'connectionString',
|
||||
type: 'CONNECTION_STRINGS/ADD'
|
||||
describe('getConnectionStringAction', () => {
|
||||
it('returns CONNECTION_STRINGS/DELETE action object', () => {
|
||||
expect(getConnectionStringAction.started()).toEqual({
|
||||
payload: undefined,
|
||||
type: 'CONNECTION_STRINGS/GET_STARTED'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteConnectionStringAction', () => {
|
||||
it('returns CONNECTION_STRINGS/DELETE action object', () => {
|
||||
expect(deleteConnectionStringAction('connectionString')).toEqual({
|
||||
expect(deleteConnectionStringAction.started('connectionString')).toEqual({
|
||||
payload: 'connectionString',
|
||||
type: 'CONNECTION_STRINGS/DELETE'
|
||||
type: 'CONNECTION_STRINGS/DELETE_STARTED'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConnectionStringAction', () => {
|
||||
it('returns CONNECTION_STRINGS/SET action object', () => {
|
||||
expect(setConnectionStringsAction([])).toEqual({
|
||||
expect(setConnectionStringsAction.started([])).toEqual({
|
||||
payload: [],
|
||||
type: 'CONNECTION_STRINGS/SET'
|
||||
type: 'CONNECTION_STRINGS/SET_STARTED'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertConnectionStringAction', () => {
|
||||
it('returns CONNECTION_STRINGS/UPSERT action object', () => {
|
||||
expect(upsertConnectionStringAction({ newConnectionString: 'new', connectionString: 'old'})).toEqual({
|
||||
expect(upsertConnectionStringAction.started({ newConnectionString: 'new', connectionString: 'old'})).toEqual({
|
||||
payload: { newConnectionString: 'new', connectionString: 'old'},
|
||||
type: 'CONNECTION_STRINGS/UPSERT'
|
||||
type: 'CONNECTION_STRINGS/UPSERT_STARTED'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import { ADD, DELETE, SET, UPSERT } from '../constants/actionTypes';
|
||||
import { DELETE, SET, UPSERT, GET } from '../constants/actionTypes';
|
||||
|
||||
export const CONNECTION_STRINGS = 'CONNECTION_STRINGS';
|
||||
export interface UpsertConnectionStringActionPayload {
|
||||
|
@ -13,7 +13,7 @@ export interface UpsertConnectionStringActionPayload {
|
|||
|
||||
const actionCreator = actionCreatorFactory(CONNECTION_STRINGS);
|
||||
|
||||
export const addConnectionStringAction = actionCreator<string>(ADD);
|
||||
export const deleteConnectionStringAction = actionCreator<string>(DELETE);
|
||||
export const setConnectionStringsAction = actionCreator<string[]>(SET);
|
||||
export const upsertConnectionStringAction = actionCreator<UpsertConnectionStringActionPayload>(UPSERT);
|
||||
export const getConnectionStringAction = actionCreator.async<void, string[]>(GET);
|
||||
export const setConnectionStringsAction = actionCreator.async<string[], string[]>(SET);
|
||||
export const upsertConnectionStringAction = actionCreator.async<UpsertConnectionStringActionPayload, string[]>(UPSERT);
|
||||
export const deleteConnectionStringAction = actionCreator.async<string, string[]>(DELETE);
|
||||
|
|
|
@ -11,9 +11,9 @@ exports[`connectionString matches snapshot 1`] = `
|
|||
className="name"
|
||||
>
|
||||
<StyledLinkBase
|
||||
ariaLabel="connectionStrings.visitConnectionCommand.ariaLabel"
|
||||
className="text"
|
||||
onClick={[Function]}
|
||||
title="test"
|
||||
title="connectionStrings.visitConnectionCommand.ariaLabel"
|
||||
>
|
||||
test
|
||||
</StyledLinkBase>
|
||||
|
@ -52,13 +52,24 @@ exports[`connectionString matches snapshot 1`] = `
|
|||
sharedAccessKey="key"
|
||||
sharedAccessKeyName="iothubowner"
|
||||
/>
|
||||
<Connect(MaskedCopyableTextField)
|
||||
allowMask={false}
|
||||
<Component
|
||||
allowMask={true}
|
||||
ariaLabel="connectionStrings.properties.connectionString.ariaLabel"
|
||||
label="connectionStrings.properties.connectionString.label"
|
||||
readOnly={true}
|
||||
value="HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key"
|
||||
/>
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
title="connectionStrings.visitConnectionCommand.ariaLabel"
|
||||
>
|
||||
connectionStrings.visitConnectionCommand.label
|
||||
</StyledLinkBase>
|
||||
</div>
|
||||
<Component
|
||||
connectionString="HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key"
|
||||
|
|
|
@ -26,9 +26,8 @@ exports[`ConnectionStringDelete matches snapshot hidden 1`] = `
|
|||
cols={40}
|
||||
readOnly={true}
|
||||
rows={8}
|
||||
>
|
||||
connectionString
|
||||
</textarea>
|
||||
value="connectionString"
|
||||
/>
|
||||
</div>
|
||||
<StyledDialogFooterBase>
|
||||
<CustomizedPrimaryButton
|
||||
|
@ -71,9 +70,8 @@ exports[`ConnectionStringDelete matches snapshot visible 1`] = `
|
|||
cols={40}
|
||||
readOnly={true}
|
||||
rows={8}
|
||||
>
|
||||
connectionString
|
||||
</textarea>
|
||||
value="connectionString"
|
||||
/>
|
||||
</div>
|
||||
<StyledDialogFooterBase>
|
||||
<CustomizedPrimaryButton
|
||||
|
|
|
@ -24,6 +24,17 @@ exports[`ConnectionStringEdit matches snapshot in Add Scenario 1`] = `
|
|||
rows={8}
|
||||
value=""
|
||||
/>
|
||||
<StyledLinkBase
|
||||
href="connectivityPane.connectionStringComboBox.link"
|
||||
target="_blank"
|
||||
>
|
||||
connectivityPane.connectionStringComboBox.linkText
|
||||
</StyledLinkBase>
|
||||
<div>
|
||||
<span>
|
||||
connectivityPane.connectionStringComboBox.warning
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</StyledPanelBase>
|
||||
`;
|
||||
|
@ -52,6 +63,17 @@ exports[`ConnectionStringEdit matches snapshot in Edit / invalid scenario 1`] =
|
|||
rows={8}
|
||||
value="connectionString"
|
||||
/>
|
||||
<StyledLinkBase
|
||||
href="connectivityPane.connectionStringComboBox.link"
|
||||
target="_blank"
|
||||
>
|
||||
connectivityPane.connectionStringComboBox.linkText
|
||||
</StyledLinkBase>
|
||||
<div>
|
||||
<span>
|
||||
connectivityPane.connectionStringComboBox.warning
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</StyledPanelBase>
|
||||
`;
|
||||
|
@ -80,6 +102,17 @@ exports[`ConnectionStringEdit matches snapshot in Edit / valid scenario 1`] = `
|
|||
rows={8}
|
||||
value="HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key"
|
||||
/>
|
||||
<StyledLinkBase
|
||||
href="connectivityPane.connectionStringComboBox.link"
|
||||
target="_blank"
|
||||
>
|
||||
connectivityPane.connectionStringComboBox.linkText
|
||||
</StyledLinkBase>
|
||||
<div>
|
||||
<span>
|
||||
connectivityPane.connectionStringComboBox.warning
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</StyledPanelBase>
|
||||
`;
|
||||
|
@ -108,6 +141,17 @@ exports[`ConnectionStringEdit matches snapshot in Edit Scenario 1`] = `
|
|||
rows={8}
|
||||
value="connectionString"
|
||||
/>
|
||||
<StyledLinkBase
|
||||
href="connectivityPane.connectionStringComboBox.link"
|
||||
target="_blank"
|
||||
>
|
||||
connectivityPane.connectionStringComboBox.linkText
|
||||
</StyledLinkBase>
|
||||
<div>
|
||||
<span>
|
||||
connectivityPane.connectionStringComboBox.warning
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</StyledPanelBase>
|
||||
`;
|
||||
|
|
|
@ -2,22 +2,22 @@
|
|||
|
||||
exports[`ConnectionSTringProperties matches snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Connect(MaskedCopyableTextField)
|
||||
<Component
|
||||
allowMask={false}
|
||||
ariaLabel="connectionStrings.properties.hostName.ariaLabel"
|
||||
label="connectionStrings.properties.hostName.label"
|
||||
readOnly={true}
|
||||
value="hostName"
|
||||
/>
|
||||
<Connect(MaskedCopyableTextField)
|
||||
<Component
|
||||
allowMask={false}
|
||||
ariaLabel="connectionStrings.properties.sharedAccessPolicyName.ariaLabel"
|
||||
label="connectionStrings.properties.sharedAccessPolicyName.label"
|
||||
readOnly={true}
|
||||
value="sharedAccessKeyName"
|
||||
/>
|
||||
<Connect(MaskedCopyableTextField)
|
||||
allowMask={false}
|
||||
<Component
|
||||
allowMask={true}
|
||||
ariaLabel="connectionStrings.properties.sharedAccessPolicyKey.ariaLabel"
|
||||
label="connectionStrings.properties.sharedAccessPolicyKey.label"
|
||||
readOnly={true}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConnectionSTringsEmpty matches snapshot 1`] = `
|
||||
<div
|
||||
className="connection-strings-empty"
|
||||
>
|
||||
<h3
|
||||
aria-level={1}
|
||||
role="heading"
|
||||
>
|
||||
connectionStrings.empty.header
|
||||
</h3>
|
||||
<div>
|
||||
<span>
|
||||
connectionStrings.empty.description
|
||||
</span>
|
||||
<NavLink
|
||||
className="embedded-link"
|
||||
to="/"
|
||||
>
|
||||
Home.
|
||||
</NavLink>
|
||||
</div>
|
||||
<h3
|
||||
aria-level={1}
|
||||
role="heading"
|
||||
>
|
||||
settings.questions.headerText
|
||||
</h3>
|
||||
<StyledLinkBase
|
||||
href="connectivityPane.connectionStringComboBox.link"
|
||||
target="_blank"
|
||||
>
|
||||
connectivityPane.connectionStringComboBox.linkText
|
||||
</StyledLinkBase>
|
||||
</div>
|
||||
`;
|
|
@ -25,7 +25,7 @@ exports[`ConnectionStringsView matches snapshot when connection string count exc
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="view-content view-scroll-vertical"
|
||||
className="view-scroll-vertical"
|
||||
>
|
||||
<div
|
||||
className="connection-strings"
|
||||
|
@ -59,7 +59,7 @@ exports[`ConnectionStringsView matches snapshot when connection strings present
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="view-content view-scroll-vertical"
|
||||
className="view-scroll-vertical"
|
||||
>
|
||||
<div
|
||||
className="connection-strings"
|
||||
|
@ -67,9 +67,9 @@ exports[`ConnectionStringsView matches snapshot when connection strings present
|
|||
<Component
|
||||
connectionString="connectionString1"
|
||||
key="connectionString1"
|
||||
onDeleteConnectionString={[MockFunction]}
|
||||
onDeleteConnectionString={[Function]}
|
||||
onEditConnectionString={[Function]}
|
||||
onSelectConnectionString={[MockFunction]}
|
||||
onSelectConnectionString={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -101,11 +101,12 @@ exports[`ConnectionStringsView matches snapshot when no connection strings 1`] =
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="view-content view-scroll-vertical"
|
||||
className="view-scroll-vertical"
|
||||
>
|
||||
<div
|
||||
className="connection-strings"
|
||||
/>
|
||||
<Component />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -28,10 +28,10 @@
|
|||
border: 1px solid themed('borderColor')
|
||||
}
|
||||
@include themify($themes) {
|
||||
border-bottom: 1px solid themed('backgroundColor')
|
||||
border-bottom: 2px solid themed('backgroundColor')
|
||||
}
|
||||
|
||||
font-size: 16px;
|
||||
font-size: 20px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
padding-top: 10px;
|
||||
|
@ -40,7 +40,10 @@
|
|||
margin-bottom: -1px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 400px;
|
||||
width: 300px;
|
||||
.text {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.properties {
|
||||
|
@ -53,4 +56,5 @@
|
|||
padding: 10px;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Link } from 'office-ui-fabric-react/lib/Link';
|
||||
import { IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { IconButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Link } from 'office-ui-fabric-react/lib/components/Link';
|
||||
import { ConnectionString, ConnectionStringProps } from './connectionString';
|
||||
import { ConnectionStringDelete } from './connectionStringDelete';
|
||||
|
||||
|
@ -33,9 +33,24 @@ describe('connectionString', () => {
|
|||
};
|
||||
|
||||
const wrapper = shallow(<ConnectionString {...props}/>);
|
||||
wrapper.find(Link).props().onClick(undefined);
|
||||
wrapper.find(Link).first().props().onClick(undefined);
|
||||
|
||||
expect(onSelectConnectionString).toHaveBeenCalledWith(connectionString, 'test.azure-devices-int.net');
|
||||
expect(onSelectConnectionString).toHaveBeenCalledWith(connectionString);
|
||||
});
|
||||
|
||||
it('calls onSelectConnectionString when convenience link clicked', () => {
|
||||
const onSelectConnectionString = jest.fn();
|
||||
const props: ConnectionStringProps = {
|
||||
connectionString,
|
||||
onDeleteConnectionString: jest.fn(),
|
||||
onEditConnectionString: jest.fn(),
|
||||
onSelectConnectionString
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ConnectionString {...props}/>);
|
||||
wrapper.find(Link).last().props().onClick(undefined);
|
||||
|
||||
expect(onSelectConnectionString).toHaveBeenCalledWith(connectionString);
|
||||
});
|
||||
|
||||
it('calls onEditConnectionString when edit button clicked', () => {
|
||||
|
|
|
@ -3,22 +3,23 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Link } from 'office-ui-fabric-react/lib/Link';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Link } from 'office-ui-fabric-react/lib/components/Link';
|
||||
import { getConnectionInfoFromConnectionString } from '../../api/shared/utils';
|
||||
import { getResourceNameFromHostName } from '../../api/shared/hostNameUtils';
|
||||
import { ConnectionStringProperties } from './connectionStringProperties';
|
||||
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { ConnectionStringDelete } from './connectionStringDelete';
|
||||
import MaskedCopyableTextFieldContainer from '../../shared/components/maskedCopyableTextFieldContainer';
|
||||
import { MaskedCopyableTextField } from '../../shared/components/maskedCopyableTextField';
|
||||
import { EDIT, REMOVE } from '../../constants/iconNames';
|
||||
import './connectionString.scss';
|
||||
|
||||
export interface ConnectionStringProps {
|
||||
connectionString: string;
|
||||
onEditConnectionString(connectionString: string): void;
|
||||
onDeleteConnectionString(connectionString: string): void;
|
||||
onSelectConnectionString(connectionString: string, hostName: string): void;
|
||||
onSelectConnectionString(connectionString: string): void;
|
||||
}
|
||||
|
||||
export const ConnectionString: React.FC<ConnectionStringProps> = props => {
|
||||
|
@ -27,7 +28,7 @@ export const ConnectionString: React.FC<ConnectionStringProps> = props => {
|
|||
const { hostName, sharedAccessKey, sharedAccessKeyName } = connectionSettings;
|
||||
const resourceName = getResourceNameFromHostName(hostName);
|
||||
const [ confirmingDelete, setConfirmingDelete ] = React.useState<boolean>(false);
|
||||
const { t } = useLocalizationContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onEditConnectionStringClick = () => {
|
||||
onEditConnectionString(connectionString);
|
||||
|
@ -47,7 +48,7 @@ export const ConnectionString: React.FC<ConnectionStringProps> = props => {
|
|||
};
|
||||
|
||||
const onSelectConnectionStringClick = () => {
|
||||
onSelectConnectionString(connectionString, hostName);
|
||||
onSelectConnectionString(connectionString);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -55,9 +56,9 @@ export const ConnectionString: React.FC<ConnectionStringProps> = props => {
|
|||
<div className="commands">
|
||||
<div className="name">
|
||||
<Link
|
||||
ariaLabel={t(ResourceKeys.connectionStrings.visitConnectionCommand.ariaLabel, {connectionString})}
|
||||
className="text"
|
||||
onClick={onSelectConnectionStringClick}
|
||||
title={resourceName}
|
||||
title={t(ResourceKeys.connectionStrings.visitConnectionCommand.ariaLabel, {connectionString})}
|
||||
>
|
||||
{resourceName}
|
||||
</Link>
|
||||
|
@ -65,7 +66,7 @@ export const ConnectionString: React.FC<ConnectionStringProps> = props => {
|
|||
<div className="actions">
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: 'EditSolid12'
|
||||
iconName: EDIT
|
||||
}}
|
||||
title={t(ResourceKeys.connectionStrings.editConnectionCommand.label)}
|
||||
ariaLabel={t(ResourceKeys.connectionStrings.editConnectionCommand.ariaLabel, {connectionString})}
|
||||
|
@ -73,7 +74,7 @@ export const ConnectionString: React.FC<ConnectionStringProps> = props => {
|
|||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: 'Delete'
|
||||
iconName: REMOVE
|
||||
}}
|
||||
title={t(ResourceKeys.connectionStrings.deleteConnectionCommand.label)}
|
||||
ariaLabel={t(ResourceKeys.connectionStrings.deleteConnectionCommand.ariaLabel, {connectionString})}
|
||||
|
@ -89,13 +90,20 @@ export const ConnectionString: React.FC<ConnectionStringProps> = props => {
|
|||
sharedAccessKey={sharedAccessKey}
|
||||
sharedAccessKeyName={sharedAccessKeyName}
|
||||
/>
|
||||
<MaskedCopyableTextFieldContainer
|
||||
<MaskedCopyableTextField
|
||||
ariaLabel={t(ResourceKeys.connectionStrings.properties.connectionString.ariaLabel)}
|
||||
allowMask={false}
|
||||
allowMask={true}
|
||||
label={t(ResourceKeys.connectionStrings.properties.connectionString.label)}
|
||||
value={connectionString}
|
||||
readOnly={true}
|
||||
/>
|
||||
<Link
|
||||
style={{marginTop: 10}}
|
||||
onClick={onSelectConnectionStringClick}
|
||||
title={t(ResourceKeys.connectionStrings.visitConnectionCommand.ariaLabel, {connectionString})}
|
||||
>
|
||||
{t(ResourceKeys.connectionStrings.visitConnectionCommand.label)}
|
||||
</Link>
|
||||
</div>
|
||||
<ConnectionStringDelete
|
||||
connectionString={connectionString}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { ConnectionStringDelete, ConnectionStringDeleteProps } from './connectionStringDelete';
|
||||
|
||||
describe('ConnectionStringDelete', () => {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Dialog, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dialog, DialogFooter } from 'office-ui-fabric-react/lib/components/Dialog';
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import './connectionStringDelete.scss';
|
||||
|
||||
|
@ -21,7 +21,7 @@ export interface ConnectionStringDeleteProps {
|
|||
|
||||
export const ConnectionStringDelete: React.FC<ConnectionStringDeleteProps> = props => {
|
||||
const { connectionString, hidden, onDeleteCancel, onDeleteConfirm } = props;
|
||||
const { t } = useLocalizationContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -41,9 +41,8 @@ export const ConnectionStringDelete: React.FC<ConnectionStringDeleteProps> = pro
|
|||
aria-label={t(ResourceKeys.connectionStrings.deleteConnection.input)}
|
||||
cols={COLS_FOR_CONNECTION}
|
||||
rows={ROWS_FOR_CONNECTION}
|
||||
>
|
||||
{connectionString}
|
||||
</textarea>
|
||||
value={connectionString}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<PrimaryButton
|
||||
|
|