зеркало из
1
0
Форкнуть 0
* 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>
This commit is contained in:
YingXue 2020-07-21 13:12:33 -07:00 коммит произвёл GitHub
Родитель 3acebb88f2
Коммит cd248f0707
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
649 изменённых файлов: 24301 добавлений и 26366 удалений

8
.gitignore поставляемый
Просмотреть файл

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

Просмотреть файл

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

Двоичные данные
doc/screenRecords/create_device.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.3 MiB

Двоичные данные
doc/screenRecords/device_features.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 413 KiB

Двоичные данные
doc/screenRecords/login.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.2 MiB

Двоичные данные
doc/screenRecords/pnp_discovery.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.1 MiB

Двоичные данные
doc/screenRecords/pnp_interaction.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 728 KiB

Двоичные данные
doc/screenRecords/pnp_interaction_property.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 MiB

Двоичные данные
doc/screenRecords/pnp_interaction_telemetry.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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

Просмотреть файл

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

1617
package-lock.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

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше