зеркало из
1
0
Форкнуть 0
This commit is contained in:
Paul Montgomery 2019-07-23 09:51:08 -07:00
Родитель 3666bd1cc6
Коммит 397dc8428e
274 изменённых файлов: 74040 добавлений и 1 удалений

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

@ -328,3 +328,23 @@ ASALocalRun/
# MFractors (Xamarin productivity tool) working folder
.mfractor/
node_modules/
dist/
# any vscode settings
.vscode/
# code coverage
coverage/
# ts complied scripts
scripts/composeLocalizationKeys.js
# test results
jest-test-results.trx
# server.js ( auto-generated from server.ts )
src/server/server.js
# js files compiles in the app directore used by server.ts
src/app/**/*.js

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

@ -1,5 +1,43 @@
# Contributing
# Azure IoT Plug and Play (PnP) Device Explorer
## Table of Contents
- [Overview](#overview)
- [Development Setup](#development-setup)
- [Contributing](#contributing)
## Overview
This application provides users an easy and visualized way to interact with Azure IoT devices.
1. Go to the releases tab, download the installer corresponding to your platform and install.
2. Fill in IoT Hub connection string and that's it.
## Development Setup
### Setup
1. git clone https://github.com/Azure/azure-iot-explorer.git
2. run `npm install`
3. run `npm start`. This step may take a while, and it would automatically open a new tab in the default browser.
3. (optional) stop step 3, run `npm run build` and then run `npm run electron` will start the electron app locally
### Package
#### Windows
Run `npm run package:win` this will create a executable installation, an msi installation, and a stand-alone executable in the dist dirctory based on the version number in the package.json file.
#### Linux
Run `npm run package:linux` this will create snap and AppImage installations as well as tar.gz, tar.lz, and tar.xz formats. Changes to `build.linux.target` array entries can be used to only build specific output types.
#### Macintosh
Run `npm run package:mac` this will create dmg installations suitable for Macintosh OSX as well as mountable application.
#### Linux on Windows via Docker
Ensure your Docker environment is set up appropriately. Run `npm run docker` to pull and launch the electronland/electron-builder image and launch the current working directory as `/`. Some environments require `$(pwd)` to be replaced with the actual host directory. You can make the change as needed in package.json.
Once the electron-builder environment is loaded, run `npm run package:linux` to create the Linux installations as you would in a full Linux environment.
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us

Двоичные данные
icon/icon.icns Normal file

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

Двоичные данные
icon/icon.ico Normal file

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

После

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

Двоичные данные
icon/pnp.png Normal file

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

После

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

12
images.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,12 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
declare module "*.svg" {
const content: any;
export default content;
}
declare module "*.png" {
const value: any;
export default value;
}

15
jestSetup.ts Normal file
Просмотреть файл

@ -0,0 +1,15 @@
/***********************************************************
* 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";
// suppress icon warnings.
setIconOptions({
disableWarnings: true,
});
Enzyme.configure({ adapter: new Adapter() });
document.execCommand = jest.fn(); // copyableMaskField

11
jestTrxProcessor.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
const Builder = require("jest-trx-results-processor");
const processor = Builder({
outputFile: "jest-test-results.trx",
});
module.exports = processor;

12695
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

206
package.json Normal file
Просмотреть файл

@ -0,0 +1,206 @@
{
"name": "pnp-device-explorer",
"version": "0.9.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",
"files": [
"dist/**/*",
"node_modules/**/*",
"package.json",
"public/electron.js",
"src/server/server.js"
],
"directories": {
"buildResources": "icon"
},
"linux": {
"category": "Utility",
"target": [
"snap",
"AppImage",
"tar.gz",
"tar.lz",
"tar.xz"
]
},
"mac": {
"type": "distribution",
"icon": "icon/icon.icns"
},
"nsis": {
"createDesktopShortcut": "always"
},
"dmg": {
"internetEnabled": true
},
"win": {
"target": [
"nsis",
"portable",
"msi"
]
},
"publish": null
},
"scripts": {
"build": "npm run localization && npm run server:compile && webpack --mode production",
"clean": "IF EXIST .\\dist RMDIR /Q /S .\\dist",
"clean:linux": "rm --recursive -f ./dist",
"localization": "tsc ./scripts/composeLocalizationKeys.ts --skipLibCheck && node ./scripts/composeLocalizationKeys.js",
"docker": "docker pull electronuserland/electron-builder && docker run --rm -ti --mount source=$(pwd),target=/project,type=bind electronuserland/electron-builder:latest",
"electron": "electron .",
"package:win": "npm run clean && npm install && npm rebuild node-sass && npm run build && electron-builder -w",
"package:linux": "npm run clean:linux && npm install && npm rebuild node-sass && npm run build && electron-builder -l",
"package:mac": "npm install && npm rebuild node-sass && npm run build && electron-builder -m",
"server:compile": "tsc ./src/server/server.ts --skipLibCheck --lib es2015 --inlineSourceMap",
"start": "concurrently \"npm run start:web\" \"npm run start:server\"",
"start:server": "npm run server:compile && nodemon --inspect ./src/server/server.js",
"start:web": "npm run localization && webpack-dev-server --mode development --hot --open --port 3000 --host 127.0.0.1",
"test": "npm run localization && jest --coverage",
"test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand -i --watch",
"test:updateSnapshot": "jest --updateSnapshot",
"test:watch": "jest --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Azure/azure-iot-pnp-template-management.git"
},
"author": "Microsoft <iotupx@microsoft.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Azure/azure-iot-pnp-template-management/issues"
},
"homepage": "https://github.com/Azure/azure-iot-pnp-template-management#readme",
"dependencies": {
"@azure/event-hubs": "1.0.7",
"@types/core-js": "2.5.0",
"body-parser": "1.18.3",
"core-js": "3.0.0",
"cors": "2.8.5",
"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",
"office-ui-fabric-react": "6.157.0",
"react": "16.8.6",
"react-collapsible": "2.3.2",
"react-dom": "16.8.6",
"react-i18next": "8.1.0",
"react-infinite-scroller": "1.2.2",
"react-json-view": "1.19.1",
"react-jsonschema-form": "1.3.0",
"react-monaco-editor": "0.25.1",
"react-redux": "5.0.7",
"react-router-dom": "4.3.1",
"react-smooth-dnd": "0.11.0",
"react-toastify": "4.4.0",
"redux": "4.0.1",
"redux-logger": "3.0.6",
"redux-saga": "0.16.2",
"request": "2.88.0",
"reselect": "4.0.0",
"typescript-fsa": "3.0.0-beta-2",
"typescript-fsa-reducers": "1.0.0"
},
"devDependencies": {
"@types/async-lock": "1.1.0",
"@types/cors": "2.8.4",
"@types/enzyme": "3.1.14",
"@types/enzyme-adapter-react-16": "1.0.3",
"@types/express": "4.16.0",
"@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-jsonschema-form": "1.0.10",
"@types/react-redux": "6.0.9",
"@types/react-router-dom": "4.3.1",
"@types/react-toastify": "4.0.1",
"@types/redux-logger": "3.0.7",
"@types/request": "2.48.1",
"@types/webpack": "4.4.22",
"awesome-typescript-loader": "5.2.1",
"concurrently": "4.1.0",
"css-loader": "1.0.0",
"electron": "3.0.10",
"electron-builder": "20.40.1",
"enzyme": "3.7.0",
"enzyme-adapter-react-16": "1.6.0",
"enzyme-to-json": "3.3.4",
"file-loader": "2.0.0",
"html-webpack-plugin": "3.2.0",
"jest": "24.8.0",
"jest-plugin-context": "2.9.0",
"jest-trx-results-processor": "0.0.7",
"monaco-editor-webpack-plugin": "1.7.0",
"node-sass": "4.12.0",
"nodemon": "1.18.7",
"sass-loader": "7.1.0",
"source-map-loader": "0.2.4",
"style-loader": "0.23.1",
"ts-jest": "24.0.2",
"tslint": "5.11.0",
"tslint-loader": "3.5.4",
"tslint-origin-ordered-imports-rule": "1.1.2",
"tslint-react": "3.6.0",
"typescript": "3.4.5",
"webpack": "4.20.2",
"webpack-bundle-analyzer": "3.3.2",
"webpack-cli": "3.1.2",
"webpack-dev-server": "3.1.14",
"webpack-shell-plugin": "0.5.0"
},
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.{ts,tsx}"
],
"testEnvironment": "jsdom",
"coverageReporters": [
"html",
"cobertura"
],
"setupFiles": [
"jest-plugin-context/setup",
"./jestSetup.ts"
],
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "ts-jest"
},
"testRegex": "(\\.|/)(spec)\\.(tsx?)$",
"moduleNameMapper": {
"^office-ui-fabric-react/lib": "<rootDir>/node_modules/office-ui-fabric-react/lib-commonjs",
"^.+\\.(scss)$": "<rootDir>/scss-stub.js"
},
"moduleFileExtensions": [
"js",
"json",
"jsx",
"ts",
"tsx"
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"testURL": "http://localhost?trustedAuthority=https://localhost",
"testResultsProcessor": "./jestTrxProcessor.ts",
"globals": {
"__DEV__": true,
"frameSignature": "portalEnvironmentFrameSignature",
"ts-jest": {
"tsConfig": "./tsconfig-jest.json",
"diagnostics": false
}
},
"preset": "ts-jest/presets/js-with-ts",
"testMatch": null
}
}

99
public/electron.js Normal file
Просмотреть файл

@ -0,0 +1,99 @@
const electron = require('electron');
const app = electron.app;
const Menu = electron.Menu;
const BrowserWindow = electron.BrowserWindow;
const server = require('../src/server/server.js');
const path = require('path');
const url = require('url');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({width: 1200, height: 900});
const startUrl = process.env.ELECTRON_START_URL || url.format({
pathname: path.join(__dirname, '/../dist/index.html'),
protocol: 'file:',
slashes: true
});
mainWindow.loadURL(startUrl);
// Open the DevTools.
if (process.env.ELECTRON_START_URL) {
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => mainWindow = null);
// Enable refresh shortcut
const globalShortcut = electron.globalShortcut;
globalShortcut.register('f5', function() {
mainWindow.reload()
})
globalShortcut.register('CommandOrControl+R', function() {
mainWindow.reload()
})
}
app.on('ready', () => {
createWindow()
createMenu()
})
function createMenu() {
const template = [
// { role: 'fileMenu' }
{
label: 'File',
submenu: [
process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' }
]
},
// { role: 'editMenu' }
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' }
]
},
// { role: 'viewMenu' }
{
label: 'View',
submenu: [
{ role: 'reload' },
{ type: 'separator' },
{ role: 'zoomin' },
{ role: 'zoomout' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
// { role: 'windowMenu' }
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'close' }
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});

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

@ -0,0 +1,50 @@
import * as fs from 'fs';
const standardIndent = ' ';
const commentPrefix = '_';
// tslint:disable-next-line:no-any
const getLocalizationNodeString = (parentObject: any, prefix: string = '', indent: string = standardIndent): string => {
const keys = [ ...Object.keys(parentObject)
.filter(s => s.substr(0, 1) !== commentPrefix)];
keys.sort();
const entries = keys.map(key => {
const childEntry = parentObject[key];
if (typeof(childEntry) === 'string') {
return prefix === '' ?
`${indent}public static ${key} = "${key}";\r\n` :
`${indent}${key} : "${prefix}${key}",\r\n`;
} else {
const contents = getLocalizationNodeString(childEntry, prefix + key + '.', indent + standardIndent);
return prefix === '' ?
`${indent}public static ${key} = {\r\n${contents}${indent}};\r\n` :
`${indent}${key} : {\r\n${contents}${indent}},\r\n`;
}
});
return entries.join('');
}
try {
const localeFileLocation = './src/localization/locales/en.json';
const keyDefinitionFile = './src/localization/resourceKeys.ts';
const localeFileContents = fs.readFileSync(localeFileLocation, 'utf-8');
const localeFileObject = JSON.parse(localeFileContents);
const nodes = getLocalizationNodeString(localeFileObject);
const keyDefinitionFileContents = `//// This code is generated by a tool
/* tslint:disable */
export class ResourceKeys {\r\n ${nodes}}
/* tslint:enable */
`;
fs.writeFileSync(keyDefinitionFile, keyDefinitionFileContents);
// tslint:disable-next-line:no-console
console.log('Localization keys transcribed to ResourceKeys.ts');
} catch (exception) {
// tslint:disable-next-line:no-console
console.log(`Failed to generate localization keys ${exception}`);
}

5
scss-stub.js Normal file
Просмотреть файл

@ -0,0 +1,5 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
module.exports = {};

11
src/app/api/constants.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
/***********************************************************
* 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'
}

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

@ -0,0 +1,39 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { DeviceSummary } from '../models/deviceSummary';
import { parseDateTimeString } from './transformHelper';
import { Device } from '../models/device';
import { DeviceIdentity } from '../models/deviceIdentity';
import { SynchronizationStatus } from '../models/synchronizationStatus';
export const transformDevice = (device: Device): DeviceSummary => {
return {
authenticationType: device.AuthenticationType,
cloudToDeviceMessageCount: device.CloudToDeviceMessageCount,
deviceId: device.DeviceId,
deviceSummarySynchronizationStatus: SynchronizationStatus.initialized,
interfaceIds: [],
isEdgeDevice: device.IotEdge,
isPnpDevice: undefined,
lastActivityTime: parseDateTimeString(device.LastActivityTime),
status: device.Status,
statusUpdatedTime: parseDateTimeString(device.StatusUpdatedTime)
};
};
export const transformDeviceIdentity = (device: DeviceIdentity): DeviceSummary => {
return {
authenticationType: device.authentication.type,
cloudToDeviceMessageCount: device.cloudToDeviceMessageCount,
deviceId: device.deviceId,
deviceSummarySynchronizationStatus: SynchronizationStatus.initialized,
interfaceIds: [],
isEdgeDevice: device.capabilities.iotEdge,
isPnpDevice: false, // device created using add device api won't be pnp device
lastActivityTime: parseDateTimeString(device.lastActivityTime),
status: device.status,
statusUpdatedTime: parseDateTimeString(device.statusUpdatedTime)
};
};

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

@ -0,0 +1,22 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import moment from 'moment';
export const parseDateTimeString = (dateTimeString: string): string => {
if (!dateTimeString || dateTimeString === '') {
return null;
}
if (isNaN(Date.parse(dateTimeString))) {
return null;
}
if (dateTimeString.match('0001-01-01.*')) {
return null;
}
return moment(dateTimeString).format('h:mm A, MMMM DD, YYYY');
};

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

@ -0,0 +1,9 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export interface BulkRegistryOperationResult {
isSuccessful: boolean;
errors: string[];
warnings: string[];
}

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

@ -0,0 +1,37 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export interface Device {
DeviceId: string;
Status: string;
StatusUpdatedTime: string;
LastActivityTime: string;
CloudToDeviceMessageCount: string;
AuthenticationType: string;
IotEdge: boolean;
'__iot:interfaces'?: {};
}
export interface Twin {
deviceId: string;
etag: string;
deviceEtag: string;
status: string;
statusUpdateTime: string;
lastActivityTime: string;
x509Thumbprint: object;
version: number;
tags?: object;
capabilities: {
iotEdge: boolean;
};
configurations?: object;
connectionState: string;
cloudToDeviceMessageCount: number;
authenticationType: string;
properties: {
desired?: object,
reported?: object;
};
}

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

@ -0,0 +1,10 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export enum DeviceAuthenticationType {
SelfSigned = 'selfSigned',
SymmetricKey = 'sas',
CACertificate = 'certificateAuthority',
None = 'none'
}

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

@ -0,0 +1,36 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export interface DeviceIdentity {
etag: string;
deviceId: string;
status: string;
lastActivityTime: string;
statusUpdatedTime: string;
statusReason: string;
authentication: AuthenticationCredentials;
capabilities: DeviceCapabilities;
cloudToDeviceMessageCount: string;
deviceScope?: string;
}
export interface DeviceCapabilities {
iotEdge: boolean;
}
export interface AuthenticationCredentials {
symmetricKey: SymmetricKeyAuthenticationProfile;
x509Thumbprint: X509AuthenticationProfile;
type: string;
}
export interface SymmetricKeyAuthenticationProfile {
primaryKey: string;
secondaryKey: string;
}
export interface X509AuthenticationProfile {
primaryThumbprint: string;
secondaryThumbprint: string;
}

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

@ -0,0 +1,13 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { DeviceIdentity } from './deviceIdentity';
import { ErrorResponse } from './errorResponse';
import { SynchronizationStatus } from './synchronizationStatus';
export interface DeviceIdentityWrapper {
deviceIdentity?: DeviceIdentity;
deviceIdentitySynchronizationStatus: SynchronizationStatus;
error?: ErrorResponse;
}

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

@ -0,0 +1,34 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export default interface DeviceQuery {
deviceId: string;
clauses: QueryClause[];
nextLink?: string;
}
export interface QueryClause {
parameterType?: ParameterType;
operation?: OperationType;
value?: string;
}
export enum ParameterType {
capabilityModelId = 'dcm',
interfaceId = 'interface',
// propertyValue = 'properties.reported',
status = 'status',
lastActivityTime = 'lastActivityTime',
statusUpdateTime = 'statusUpdateTime'
}
export enum OperationType {
equals = '=',
notEquals = '!=',
lessThan = '<',
lessThanEquals = '<=',
greaterThan = '>',
greaterThanEquals = '>=',
inequal = '<>'
}

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

@ -0,0 +1,8 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export enum DeviceStatus {
Enabled = 'enabled',
Disabled = 'disabled'
}

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

@ -0,0 +1,21 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { SynchronizationStatus } from './synchronizationStatus';
export interface DeviceSummary {
deviceId: string;
isEdgeDevice?: boolean;
isPnpDevice: boolean;
status: string;
lastActivityTime: string;
statusUpdatedTime: string;
cloudToDeviceMessageCount: string;
authenticationType: string;
// Interfaces
interfaceIds: string[];
deviceSummarySynchronizationStatus: SynchronizationStatus;
}

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

@ -0,0 +1,13 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { List, Map } from 'immutable';
import { ErrorResponse } from './errorResponse';
import { SynchronizationStatus } from './synchronizationStatus';
export interface DeviceSummaryListWrapper {
deviceList?: List<Map<string, any>>; // tslint:disable-line:no-any
deviceListSynchronizationStatus: SynchronizationStatus;
error?: ErrorResponse;
}

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

@ -0,0 +1,13 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { ErrorResponse } from './errorResponse';
import { SynchronizationStatus } from './synchronizationStatus';
import { Twin } from './device';
export interface DeviceTwinWrapper {
deviceTwin?: Twin;
deviceTwinSynchronizationStatus: SynchronizationStatus;
error?: ErrorResponse;
}

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

@ -0,0 +1,13 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { DigitalTwinInterfaces } from './digitalTwinModels';
import { ErrorResponse } from './errorResponse';
import { SynchronizationStatus } from './synchronizationStatus';
export interface DigitalTwinInterfacePropertiesWrapper {
digitalTwinInterfaceProperties?: DigitalTwinInterfaces;
digitalTwinInterfacePropertiesSyncStatus: SynchronizationStatus;
error?: ErrorResponse;
}

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

@ -0,0 +1,192 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
/*
* Code generated by Microsoft (R) AutoRest Code Generator.
* Changes may cause incorrect behavior and will be lost if the code is
* regenerated.
*/
/**
* @interface
* An interface representing DesiredState.
*/
export interface DesiredState {
/**
* @member {number} [code] Status code for the operation.
*/
code?: number;
/**
* @member {number} [subCode] Sub status code for the status.
*/
subCode?: number;
/**
* @member {number} [version] Version of the desired value received.
*/
version?: number;
/**
* @member {string} [description] Description of the status.
*/
description?: string;
}
/**
* @interface
* An interface representing Reported.
*/
export interface Reported {
/**
* @member {any} [value] The current interface property value in a
* digitalTwin.
*/
// tslint:disable-next-line:no-any
value?: any;
/**
* @member {DesiredState} [desiredState]
*/
desiredState?: DesiredState;
}
/**
* @interface
* An interface representing Desired.
*/
export interface Desired {
/**
* @member {any} [value] The desired value of the interface property to set
* in a digitalTwin.
*/
// tslint:disable-next-line:no-any
value?: any;
}
/**
* @interface
* An interface representing Property.
*/
export interface Property {
/**
* @member {Reported} [reported]
*/
reported?: Reported;
/**
* @member {Desired} [desired]
*/
desired?: Desired;
}
/**
* @interface
* An interface representing InterfaceModel.
*/
export interface InterfaceModel {
/**
* @member {string} [name] The name of digital twin interface, e.g.:
* myThermostat.
*/
name?: string;
/**
* @member {{ [propertyName: string]: Property }} [properties] List of all
* properties in an interface.
*/
properties?: { [propertyName: string]: Property };
}
/**
* @interface
* An interface representing DigitalTwinInterfaces.
*/
export interface DigitalTwinInterfaces {
/**
* @member {{ [propertyName: string]: InterfaceModel }} [interfaces]
* Interface(s) data on the digital twin.
*/
interfaces?: { [propertyName: string]: InterfaceModel };
/**
* @member {number} [version] Version of digital twin.
*/
version?: number;
}
/**
* @interface
* An interface representing DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired.
*/
export interface DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired {
/**
* @member {any} [value] The desired value of the interface property to set
* in a digitalTwin.
*/
// tslint:disable-next-line:no-any
value?: any;
}
/**
* @interface
* An interface representing DigitalTwinInterfacesPatchInterfacesValuePropertiesValue.
*/
export interface DigitalTwinInterfacesPatchInterfacesValuePropertiesValue {
/**
* @member {DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired}
* [desired]
*/
desired?: DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired;
}
/**
* @interface
* An interface representing DigitalTwinInterfacesPatchInterfacesValue.
*/
export interface DigitalTwinInterfacesPatchInterfacesValue {
/**
* @member {{ [propertyName: string]:
* DigitalTwinInterfacesPatchInterfacesValuePropertiesValue }} [properties]
* List of properties to update in an interface.
*/
properties?: { [propertyName: string]: DigitalTwinInterfacesPatchInterfacesValuePropertiesValue };
}
/**
* @interface
* An interface representing DigitalTwinInterfacesPatch.
*/
export interface DigitalTwinInterfacesPatch {
/**
* @member {{ [propertyName: string]:
* DigitalTwinInterfacesPatchInterfacesValue }} [interfaces] Interface(s)
* data to patch in the digital twin.
*/
interfaces?: { [propertyName: string]: DigitalTwinInterfacesPatchInterfacesValue };
}
/**
* @interface
* An interface representing DigitalTwinUpdateMultipleInterfacesOptionalParams.
* Optional Parameters.
*
* @extends RequestOptionsBase
*/
export interface DigitalTwinUpdateMultipleInterfacesOptionalParams {
/**
* @member {string} [ifMatch]
*/
ifMatch?: string;
}
/**
* @interface
* An interface representing DigitalTwinInvokeInterfaceCommandOptionalParams.
* Optional Parameters.
*
* @extends RequestOptionsBase
*/
export interface DigitalTwinInvokeInterfaceCommandOptionalParams {
/**
* @member {number} [responseTimeoutInSeconds] Response timeout in seconds.
*/
responseTimeoutInSeconds?: number;
/**
* @member {number} [connectTimeoutInSeconds] Connect timeout in seconds.
*/
connectTimeoutInSeconds?: number;
}

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

@ -0,0 +1,12 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export interface ErrorResponse {
error: {
code?: number,
message: string
};
traceIdentifier?: string;
sourceId?: string;
}

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

@ -0,0 +1,25 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export interface ParsedJsonSchema {
type: string;
additionalProperties?: boolean; // use this props as a workaround to indicate whether parsed property is map type
default?: {};
description?: string;
enum?: number[] ;
enumNames?: string[];
format?: string;
items?: any; // tslint:disable-line: no-any
properties?: {};
required?: string[];
title?: string;
}
export interface ParsedCommandSchema {
description: string;
name: string;
requestSchema?: ParsedJsonSchema;
responseSchema?: ParsedJsonSchema;
}

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

@ -0,0 +1,23 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export interface Message {
body: any; // tslint:disable-line:no-any
enqueuedTime: string;
properties?: any; // tslint:disable-line:no-any
systemProperties?: {[key: string]: string};
}
export enum MESSAGE_PROPERTIES {
IOTHUB_MESSAGE_SCHEMA = 'iothub-message-schema'
}
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_MESSAGE_SOURCE = 'iothub-message-source',
IOTHUB_ENQUEUED_TIME = 'iothub-enqueuedtime'
}

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

@ -0,0 +1,31 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { ModelDefinition } from './modelDefinition';
export interface PnPModel {
createdOn: string;
etag: string;
lastUpdated: string;
model: ModelDefinition;
modelId: string;
publisherId: string;
publisherName: string;
}
export interface LocaleValue {
locale: string;
value: string;
}
export enum MetaModelType {
none = 'none',
interface = 'interface',
capabilityModel = 'capabilityModel'
}
export interface SearchResults {
continuationToken: string;
results: PnPModel[];
}

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

@ -0,0 +1,68 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export interface ModelDefinition {
'@context': string;
'@id': string;
'@type': string;
comment?: string;
contents: Array<PropertyContent | CommandContent | TelemetryContent>;
description?: string;
displayName?: string;
}
export interface PropertyContent extends ContentBase {
schema: string | EnumSchema | ObjectSchema | MapSchema;
writable?: boolean;
}
export interface CommandContent extends ContentBase{
commandType?: string;
response?: Schema;
request?: Schema;
}
export interface TelemetryContent extends ContentBase{
schema: string | EnumSchema | ObjectSchema | MapSchema;
}
interface ContentBase {
'@type': string | string[];
name: string;
comment?: string;
description?: string;
displayName?: string;
displayUnit?: string;
unit?: any; // tslint:disable-line:no-any
}
export interface Schema {
name: string;
schema: string | EnumSchema | ObjectSchema | MapSchema;
displayName?: string;
description?: string;
}
export interface EnumSchema {
'@type': string;
enumValues: Array<{ displayName: string, name: string, enumValue: number}>;
}
export interface ObjectSchema {
'@type': string;
fields: Schema[];
}
export interface MapSchema {
'@type': string;
mapKey: Schema;
mapValue: Schema;
}
export enum ContentType{
Command = 'command',
Property = 'property',
Telemetry = 'telemetry'
}

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

@ -0,0 +1,15 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { ModelDefinition } from './modelDefinition';
import { ErrorResponse } from './errorResponse';
import { SynchronizationStatus } from './synchronizationStatus';
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
export interface ModelDefinitionWithSourceWrapper {
modelDefinition?: ModelDefinition;
source?: REPOSITORY_LOCATION_TYPE;
modelDefinitionSynchronizationStatus: SynchronizationStatus;
error?: ErrorResponse;
}

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

@ -0,0 +1,24 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export enum NotificationType {
info,
warning,
success,
error,
}
export interface Notification {
id?: number;
issued?: string;
title?: {
translationKey: string;
translationOptions?: {}
};
text: {
translationKey: string;
translationOptions?: {};
};
type: NotificationType;
}

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

@ -0,0 +1,13 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export enum SynchronizationStatus {
deleted,
fetched,
initialized,
upserted,
working,
failed,
updating
}

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

@ -0,0 +1,74 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { Twin } from '../models/device';
import { DeviceIdentity } from '../models/deviceIdentity';
import DeviceQuery from '../models/deviceQuery';
import { DigitalTwinInterfaces } from '../models/digitalTwinModels';
export interface DataPlaneParameters {
connectionString: string;
}
export interface FetchDeviceTwinParameters extends DataPlaneParameters {
deviceId: string;
}
export interface UpdateDeviceTwinParameters extends FetchDeviceTwinParameters {
deviceTwin: Twin;
}
export interface InvokeMethodParameters extends DataPlaneParameters {
connectTimeoutInSeconds?: number;
deviceId: string;
methodName: string;
payload?: object;
responseTimeoutInSeconds?: number;
}
export interface FetchDeviceParameters extends DataPlaneParameters {
deviceId: string;
}
export interface FetchDevicesParameters extends DataPlaneParameters {
query?: DeviceQuery;
continuationLink?: string;
}
export interface MonitorEventsParameters {
deviceId: string;
startTime?: Date;
hubConnectionString: string;
fetchSystemProperties?: boolean;
}
export interface DeleteDevicesParameters extends DataPlaneParameters {
deviceIds: string[];
}
export interface AddDeviceParameters extends DataPlaneParameters {
deviceIdentity: DeviceIdentity;
}
export interface UpdateDeviceParameters extends DataPlaneParameters {
deviceIdentity: DeviceIdentity;
}
export interface FetchDigitalTwinInterfacePropertiesParameters extends DataPlaneParameters {
digitalTwinId: string; // Format of digitalTwinId is DeviceId[~ModuleId]. ModuleId is optional.
}
export interface InvokeDigitalTwinInterfaceCommandParameters extends DataPlaneParameters {
digitalTwinId: string; // Format of digitalTwinId is DeviceId[~ModuleId]. ModuleId is optional.
interfaceName: string;
commandName: string;
connectTimeoutInSeconds?: number;
payload?: object;
responseTimeoutInSeconds?: number;
}
export interface PatchDigitalTwinInterfacePropertiesParameters extends DataPlaneParameters {
digitalTwinId: string; // Format of digitalTwinId is DeviceId[~ModuleId]. ModuleId is optional.
payload: DigitalTwinInterfaces;
}

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

@ -0,0 +1,27 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { MetaModelType } from '../models/metamodelMetadata';
export interface RepoParametersBase {
repoServiceEndpoint: string;
repositoryId?: string;
}
export interface FetchModelsParameters extends RepoParametersBase {
metaModelType?: MetaModelType;
pageSize?: number;
continuationToken?: string;
}
export interface FetchModelParameters extends RepoParametersBase {
id: string;
expand?: boolean;
token: string;
}
export interface FetchModelsParameters extends RepoParametersBase {
ids?: string[];
token: string;
}

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

@ -0,0 +1,395 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { BulkRegistryOperationResult } from './../models/bulkRegistryOperationResult';
import { FetchDeviceTwinParameters,
UpdateDeviceTwinParameters,
InvokeMethodParameters,
FetchDevicesParameters,
MonitorEventsParameters,
DataPlaneParameters,
FetchDeviceParameters,
DeleteDevicesParameters,
AddDeviceParameters,
UpdateDeviceParameters,
FetchDigitalTwinInterfacePropertiesParameters,
InvokeDigitalTwinInterfaceCommandParameters,
PatchDigitalTwinInterfacePropertiesParameters } from '../parameters/deviceParameters';
import { CONTROLLER_API_ENDPOINT, DATAPLANE, EVENTHUB, DIGITAL_TWIN_API_VERSION } from '../../constants/apiConstants';
import { HTTP_OPERATION_TYPES } from '../constants';
import { buildQueryString, getConnectionInfoFromConnectionString, generateSasToken } from '../shared/utils';
import { CONNECTION_TIMEOUT_IN_SECONDS, RESPONSE_TIME_IN_SECONDS } from '../../constants/devices';
import { Message } from '../models/messages';
import { Twin, Device } from '../models/device';
import { DeviceIdentity } from '../models/deviceIdentity';
import { DeviceSummary } from '../models/deviceSummary';
import { DigitalTwinInterfaces } from '../models/digitalTwinModels';
import { transformDevice, transformDeviceIdentity } from '../dataTransforms/deviceSummaryTransform';
const DATAPLANE_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${DATAPLANE}`;
const EVENTHUB_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${EVENTHUB}`;
export interface DataPlaneRequest {
apiVersion?: string;
body?: string;
etag?: string;
hostName: string;
httpMethod: string;
path: string;
sharedAccessSignature: string;
queryString?: string;
}
export interface IoTHubConnectionSettings {
hostName?: string;
sharedAccessKey?: string;
sharedAccessKeyName?: string;
}
export interface CloudToDeviceMethodResult {
payload: object;
status: number;
}
// We can do something more sophisticated with agents and a factory
const request = async (endpoint: string, parameters: any) => { // tslint:disable-line
return fetch(
endpoint,
{
body: JSON.stringify(parameters),
cache: 'no-cache',
credentials: 'include',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
method: HTTP_OPERATION_TYPES.Post,
mode: 'cors',
}
);
};
const dataPlaneConnectionHelper = (parameters: DataPlaneParameters) => {
if (!parameters || !parameters.connectionString) {
return;
}
const connectionInfo = getConnectionInfoFromConnectionString(parameters.connectionString);
if (!(connectionInfo && connectionInfo.hostName)) {
return;
}
const fullHostName = `${connectionInfo.hostName}/devices/query`;
const sasToken = generateSasToken(fullHostName, connectionInfo.sharedAccessKeyName, connectionInfo.sharedAccessKey);
return {
connectionInfo,
sasToken,
};
};
// tslint:disable-next-line:cyclomatic-complexity
const dataPlaneResponseHelper = async (response: Response) => {
const result = await response.json();
if (result.ExceptionMessage && result.Message) {
throw new Error(`${result.Message}: ${result.ExceptionMessage}`);
}
else if (!!result.ExceptionMessage || result.Message) {
throw new Error(!!result.ExceptionMessage ? result.ExceptionMessage : result.Message);
}
return result;
};
export const fetchDeviceTwin = async (parameters: FetchDeviceTwinParameters): Promise<Twin> => {
try {
if (!parameters.deviceId) {
return;
}
const connectionInformation = dataPlaneConnectionHelper(parameters);
const dataPlaneRequest: DataPlaneRequest = {
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 as Twin;
} catch (error) {
throw error;
}
};
export const fetchDigitalTwinInterfaceProperties = async (parameters: FetchDigitalTwinInterfacePropertiesParameters): Promise<DigitalTwinInterfaces> => {
try {
if (!parameters.digitalTwinId) {
return;
}
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 as DigitalTwinInterfaces;
} 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 responseTimeInSeconds = parameters.responseTimeoutInSeconds || RESPONSE_TIME_IN_SECONDS;
const queryString = `connectTimeoutInSeconds=${connectTimeoutInSeconds}&responseTimeInSeconds=${responseTimeInSeconds}`;
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.interfaceName}/commands/${parameters.commandName}`,
queryString,
sharedAccessSignature: connectionInformation.sasToken
};
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
const result = await dataPlaneResponseHelper(response);
return result;
} 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 as DigitalTwinInterfaces;
} catch (error) {
throw error;
}
};
export const updateDeviceTwin = async (parameters: UpdateDeviceTwinParameters): Promise<Twin> => {
try {
if (!parameters.deviceId) {
return;
}
const connectionInformation = dataPlaneConnectionHelper(parameters);
const dataPlaneRequest: DataPlaneRequest = {
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 as Twin;
} catch (error) {
throw error;
}
};
export const invokeDeviceMethod = async (parameters: InvokeMethodParameters): Promise<CloudToDeviceMethodResult> => {
try {
if (!parameters.deviceId) {
return;
}
const connectionInfo = dataPlaneConnectionHelper(parameters);
const dataPlaneRequest: DataPlaneRequest = {
body: JSON.stringify({
connectTimeoutInSeconds: parameters.connectTimeoutInSeconds || CONNECTION_TIMEOUT_IN_SECONDS,
methodName: parameters.methodName,
payload: parameters.payload,
responseTimeInSeconds: parameters.responseTimeoutInSeconds || RESPONSE_TIME_IN_SECONDS,
}),
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 as CloudToDeviceMethodResult;
} catch (error) {
throw error;
}
};
export const addDevice = async (parameters: AddDeviceParameters): Promise<DeviceSummary> => {
try {
if (!parameters.deviceIdentity) {
return;
}
const connectionInfo = dataPlaneConnectionHelper(parameters);
const dataPlaneRequest: DataPlaneRequest = {
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 transformDeviceIdentity(result as DeviceIdentity);
} catch (error) {
throw error;
}
};
export const updateDevice = async (parameters: UpdateDeviceParameters): Promise<DeviceIdentity> => {
try {
if (!parameters.deviceIdentity) {
return;
}
const connectionInfo = dataPlaneConnectionHelper(parameters);
const dataPlaneRequest: DataPlaneRequest = {
body: JSON.stringify(parameters.deviceIdentity),
etag: parameters.deviceIdentity.etag,
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 as DeviceIdentity;
} catch (error) {
throw error;
}
};
export const fetchDevice = async (parameters: FetchDeviceParameters): Promise<DeviceIdentity> => {
try {
if (!parameters.deviceId) {
return;
}
const connectionInfo = dataPlaneConnectionHelper(parameters);
const dataPlaneRequest: DataPlaneRequest = {
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 as DeviceIdentity;
} catch (error) {
throw error;
}
};
export const fetchDevices = async (parameters: FetchDevicesParameters): Promise<DeviceSummary[]> => {
try {
const connectionInformation = dataPlaneConnectionHelper(parameters);
const queryString = buildQueryString(parameters.query);
const dataPlaneRequest: DataPlaneRequest = {
body: JSON.stringify({
query: queryString,
}),
hostName: connectionInformation.connectionInfo.hostName,
httpMethod: HTTP_OPERATION_TYPES.Post,
path: 'devices/query',
sharedAccessSignature: connectionInformation.sasToken,
};
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
const result = await dataPlaneResponseHelper(response);
return result.map((device: Device) => transformDevice(device));
} catch (error) {
throw error;
}
};
export const deleteDevices = async (parameters: DeleteDevicesParameters) => {
if (!parameters.deviceIds) {
return;
}
try {
const deviceDeletionInstructions = parameters.deviceIds.map(deviceId => {
return {
etag: '*',
id: deviceId,
importMode: 'deleteIfMatchEtag'
};
});
const connectionInfo = dataPlaneConnectionHelper(parameters);
const dataPlaneRequest: DataPlaneRequest = {
body: JSON.stringify(deviceDeletionInstructions),
hostName: connectionInfo.connectionInfo.hostName,
httpMethod: 'post',
path: `devices`,
sharedAccessSignature: connectionInfo.sasToken,
};
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
const result = await dataPlaneResponseHelper(response);
return result as BulkRegistryOperationResult[];
} catch (error) {
throw error;
}
};
export const monitorEvents = async (parameters: MonitorEventsParameters): Promise<Message[]> => {
try {
if (!parameters.hubConnectionString) {
return;
}
const requestParameters = {
connectionString: parameters.hubConnectionString,
deviceId: parameters.deviceId,
fetchSystemProperties: parameters.fetchSystemProperties,
startTime: parameters.startTime && parameters.startTime.toISOString()
};
const response = await request(EVENTHUB_CONTROLLER_ENDPOINT, requestParameters);
return await response.json() as Message[];
} catch (error) {
throw error;
}
};

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

@ -0,0 +1,107 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { ModelDefinition } from '../models/modelDefinition';
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';
import { PnPModel } from '../models/metamodelMetadata';
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 modelIdentifier = encodeURIComponent(parameters.id);
const resourceUrl = `${parameters.repoServiceEndpoint}/models/${modelIdentifier}${queryString}`;
const controllerRequest: RequestInitWithUri = {
headers: {
'Accept': 'application/json',
'Authorization': parameters.token || '',
'Content-Type': 'application/json'
},
method: HTTP_OPERATION_TYPES.Get,
uri: resourceUrl
};
const response = await request(controllerRequest);
if (!response.ok) {
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,
model,
modelId,
publisherId,
publisherName,
};
return pnpModel;
};
export const fetchModelDefinition = async (parameters: FetchModelParameters) => {
try {
const result = await fetchModel({
...parameters
});
return result.model;
}
catch (error) {
throw new Error(error);
}
};
export interface RequestInitWithUri extends RequestInit {
uri: string;
headers?: Record<string, string>;
}
export interface RepoConnectionSettings {
hostName?: string;
sharedAccessKey?: string;
sharedAccessKeyName?: string;
repositoryId?: string;
}
const CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${MODELREPO}`;
const CONTROLLER_PASSTHROUGH = true;
const request = async (requestInit: RequestInitWithUri) => {
if (CONTROLLER_PASSTHROUGH) {
return fetch(
CONTROLLER_ENDPOINT,
{
body: JSON.stringify(requestInit),
cache: 'no-cache',
credentials: 'include',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json'
}),
method: HTTP_OPERATION_TYPES.Post
}
);
} else {
return fetch(
requestInit.uri,
requestInit
);
}
};

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

@ -0,0 +1,20 @@
/***********************************************************
* 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 { connectionStateInitial } from '../../login/state';
import { applicationStateInitial } from '../../settings/state';
export const getInitialState = (): StateInterface => {
return {
applicationState: applicationStateInitial(),
connectionState: connectionStateInitial(),
deviceContentState: deviceContentStateInitial(),
deviceListState: deviceListStateInitial(),
notificationsState: notificationsStateInterfaceInitial
};
};

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

@ -0,0 +1,125 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as utils from './utils';
import { ParameterType, OperationType } from '../models/deviceQuery';
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'
}
],
deviceId: '',
}
)).toEqual(`${LIST_PLUG_AND_PLAY_DEVICES} WHERE (HAS_CAPABILITYMODEL('enabled'))`);
expect(utils.buildQueryString(
{
clauses: [],
deviceId: 'device1',
}
)).toEqual(`${LIST_PLUG_AND_PLAY_DEVICES} WHERE (deviceId = 'device1')`);
expect(utils.buildQueryString(
{
clauses: [],
deviceId: '',
}
)).toEqual(LIST_PLUG_AND_PLAY_DEVICES + ' ');
expect(utils.buildQueryString(
null
)).toEqual(LIST_PLUG_AND_PLAY_DEVICES);
});
it('converts query object to string', () => {
expect(utils.queryToString(
{
clauses: [
{
operation: OperationType.equals,
parameterType: ParameterType.capabilityModelId,
value: 'enabled'
},
{
}
],
deviceId: '',
}
)).toEqual(`WHERE (HAS_CAPABILITYMODEL('enabled'))`);
expect(utils.queryToString(
{
clauses: [],
deviceId: 'device1',
}
)).toEqual(`WHERE (deviceId = 'device1')`);
expect(utils.queryToString(
{
clauses: [],
deviceId: '',
}
)).toEqual('');
});
it('converts clauses to string', () => {
expect(utils.clauseListToString(null)).toEqual('');
expect(utils.clauseListToString([
{
operation: OperationType.equals,
parameterType: ParameterType.status,
value: 'enabled'
}
])).toEqual(`status = 'enabled'`);
expect(utils.clauseListToString([
{
operation: OperationType.equals,
parameterType: ParameterType.status,
value: 'enabled'
},
{
operation: OperationType.equals,
parameterType: ParameterType.status,
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', () => {
expect(utils.clauseItemToString('foo', OperationType.equals, 'bar')).toEqual(`foo = 'bar'`);
expect(utils.clauseItemToString('foo', OperationType.greaterThan, 'bar')).toEqual(`foo > 'bar'`);
expect(utils.clauseItemToString('foo', OperationType.greaterThanEquals, 'bar')).toEqual(`foo >= 'bar'`);
expect(utils.clauseItemToString('foo', OperationType.inequal, 'bar')).toEqual(`foo <> 'bar'`);
expect(utils.clauseItemToString('foo', OperationType.lessThan, 'bar')).toEqual(`foo < 'bar'`);
expect(utils.clauseItemToString('foo', OperationType.lessThanEquals, 'bar')).toEqual(`foo <= 'bar'`);
expect(utils.clauseItemToString('foo', OperationType.notEquals, 'bar')).toEqual(`foo != 'bar'`);
});
it('handles escaping strings appropriately', () => {
expect(utils.escapeValue('1')).toEqual('1');
expect(utils.escapeValue(`'enabled'`)).toEqual(`'enabled'`);
expect(utils.escapeValue('enabled')).toEqual(`'enabled'`);
expect(utils.escapeValue(`o'really`)).toEqual(`'o'really'`);
});
});

149
src/app/api/shared/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,149 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
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';
const MILLISECONDS_PER_SECOND = 1000;
const SECONDS_PER_MINUTE = 60;
export const parseConnectionString = (connectionString: string) => {
const connectionObject: any = {}; // tslint:disable-line: no-any
connectionString.split(';')
.forEach((segment: string) => {
const keyValue = segment.split('=');
connectionObject[keyValue[0]] = keyValue[1];
});
return connectionObject;
};
export const generateSasToken = (resourceUri: string, sharedAccessKeyName: string, sharedAccessKey: string) => {
const encodedUri = encodeURIComponent(resourceUri);
const expires = Math.ceil((Date.now() / MILLISECONDS_PER_SECOND) + SAS_EXPIRES_MINUTES * SECONDS_PER_MINUTE);
const toSign = encodedUri + '\n' + expires;
const hmac = createHmac('sha256', new Buffer(sharedAccessKey, 'base64'));
hmac.update(toSign);
const base64UriEncoded = encodeURIComponent(hmac.digest('base64'));
const token = `SharedAccessSignature sr=${encodedUri}&sig=${base64UriEncoded}&se=${expires}&skn=${sharedAccessKeyName}`;
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 = {};
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;
default:
// we don't use other parts of connection string
break;
}
});
return connectionObject;
};
export const buildQueryString = (query: DeviceQuery) => {
return query ?
`${LIST_PLUG_AND_PLAY_DEVICES} ${queryToString(query)}` :
LIST_PLUG_AND_PLAY_DEVICES;
};
// tslint:disable-next-line:cyclomatic-complexity
export const queryToString = (query: DeviceQuery) => {
const deviceIdQuery = query.deviceId && `deviceId = '${query.deviceId}'` || '';
const clauseQuery = clauseListToString(query.clauses.filter(clause => !!clause.parameterType));
if ('' !== deviceIdQuery || '' !== clauseQuery) {
return `WHERE (${deviceIdQuery || clauseQuery })`;
}
return '';
};
export const clauseListToString = (clauses: QueryClause[]) => {
return clauses && clauses.map(clause => clauseToString(clause)).join(' AND ') || '';
};
export const clauseToString = (clause: QueryClause) => {
switch (clause.parameterType) {
case ParameterType.capabilityModelId:
return `HAS_CAPABILITYMODEL(${escapeValue(clause.value)})`;
case ParameterType.interfaceId:
return `HAS_INTERFACE(${escapeValue(clause.value)})`;
default:
return clauseItemToString(clause.parameterType, clause.operation, clause.value);
}
};
export const clauseItemToString = (fieldName: string, operation: OperationType, value: unknown) => {
return `${fieldName} ${operation} ${escapeValue(value as string)}`;
};
export const escapeValue = (value: string) => {
const isString = new RegExp(/[^0-9.]/);
const hasQuotes = new RegExp(/^'.*'$/);
if (value.match(isString) && !value.match(hasQuotes)) {
return `'${value}'`;
}
return value;
};

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

@ -0,0 +1,9 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export const DEVICELISTS = 'DEVICE_LISTS';
export const DEVICECONTENT = 'DEVICE_CONTENT';
export const LOGIN = 'LOGIN';
export const APPLICATION = 'APPLICATION';
export const NOTIFICATIONS = 'NOTIFICATIONS';

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

@ -0,0 +1,39 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
// login
export const SET_CONNECTION_STRING = 'SET_CONNECTION_STRING';
export const SET_REMEMBER_CONNECTION_STRING_VALUE = 'SET_REMEMBER_CONNECTION_STRING_VALUE';
// interfaces
export const CLEAR_MODEL_DEFINITIONS = 'CLEAR_MODEL_DEFINITIONS';
export const FETCH_MODEL_DEFINITION = 'FETCH_MODEL_DEFINITION';
export const SET_ISPNP = 'SET_ISPNP';
export const FETCH_INTERFACES = 'FETCH_INTERFACES';
// devices
export const ADD_DEVICE = 'ADD_DEVICE';
export const CLEAR_DEVICES = 'CLEAR_DEVICES';
export const DELETE_DEVICES = 'DELETE_DEVICES';
export const EXECUTE_METHOD = 'EXECUTE_METHOD';
export const GET_DEVICE_IDENTITY = 'GET_DEVICE_IDENTITY';
export const GET_DIGITAL_TWIN_INTERFACE_PROPERTIES = 'GET_DIGITAL_TWIN_INTERFACE_PROPERTIES';
export const GET_TWIN = 'GET_TWIN';
export const LIST_DEVICES = 'LIST_DEVICES';
export const INVOKE_DEVICE_METHOD = 'INVOKE_DEVICE_METHOD';
export const INVOKE_DIGITAL_TWIN_INTERFACE_COMMAND = 'INVOKE_DIGITAL_TWIN_INTERFACE_COMMAND';
export const PATCH_DIGITAL_TWIN_INTERFACE_PROPERTIES = 'PATCH_DIGITAL_TWIN_INTERFACE_PROPERTIES';
export const SET_INTERFACE_ID = 'SET_INTERFACE_ID';
export const UPDATE_TWIN = 'UPDATE_TWIN';
export const UPDATE_DEVICE_IDENTITY = 'UPDATE_DEVICE_IDENTITY';
// settings
export const SET_SETTINGS_VISIBILITY = 'SET_SETTINGS_VISIBILITY';
export const SET_REPOSITORY_LOCATIONS = 'SET_REPOSITORY_LOCATIONS';
export const UPDATE_REPO_TOKEN = 'UPDATE_REPO_TOKEN';
// common
export const CLEAR = 'CLEAR';
export const ADD = 'ADD';
export const READ = 'READ';

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

@ -0,0 +1,33 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
declare const _CONTROLLER_ENDPOINT: string;
// express server
export const CONTROLLER_API_ENDPOINT = (`${_CONTROLLER_ENDPOINT}/api`).replace(/([^:]\/)\/+/, '$1');
export const DATAPLANE = '/DataPlane';
export const EVENTHUB = '/EventHub';
export const MODELREPO = '/ModelRepo';
// model repo .net controller
export const INTERFACE_ID = '?interfaceId=';
export const OBJECTMODEL_PARAMETER = '/objectmodel?interfaceId=';
export const API_MODEL = '/Models';
export const MODEL_ID_REF = '/ref:modelId?';
export const MODEL_ID = 'modelId=';
export const API_VERSION = 'api-version=';
export const AND = '&';
// digital twin api version
export const DIGITAL_TWIN_API_VERSION = '2019-07-01-preview';
export const HEADERS = {
CREATED_ON: 'x-ms-model-createdon',
ETAG: 'Etag',
LAST_UPDATED: 'x-ms-model-lastupdated',
MODEL_ID: 'x-ms-model-id',
PUBLISHER_ID: 'x-ms-model-publisher-id',
PUBLISHER_NAME: 'x-ms-model-publisher-name',
REQUEST_ID: 'x-ms-request-id',
};

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

@ -0,0 +1,8 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export const PRIVATE_REPO_CONNECTION_STRING_NAME = 'PRIVATE_REPO_CONNECTION_STRING_NAME';
export const CONNECTION_STRING_NAME = 'CONNECTION_STRING_NAME';
export const REPO_LOCATIONS = 'REPO_LOCATIONS'; // store repo locations in localStorage separated by comma
export const REMEMBER_CONNECTION_STRING = 'REMEMBER_CONNECTION_STRING';

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

@ -0,0 +1,13 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export const CODE = 'code';
export const LAST_UPDATED_VERSION = '$lastUpdatedVersion';
export const METADATA = '$metadata';
export const STATUS = 'status';
export const VALUE = 'value';
export const VERSION = 'version';
export const FAILURE_CODE = 500;
export const SUCCESS_CODE = 200;

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

@ -0,0 +1,21 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export const LIST_PLUG_AND_PLAY_DEVICES = `
SELECT deviceId as DeviceId,
status as Status,
lastActivityTime as LastActivityTime,
statusUpdatedTime as StatusUpdatedTime,
authenticationType as AuthenticationType,
cloudToDeviceMessageCount as CloudToDeviceMessageCount,
capabilities.iotEdge as IotEdge,
properties.reported.[[__iot:interfaces]] FROM devices`;
export const DEVICE_TWIN_QUERY_STRING = ' SELECT * FROM devices WHERE deviceId = {deviceId}';
export const SAS_EXPIRES_MINUTES = 5;
export const CONNECTION_TIMEOUT_IN_SECONDS = 20;
export const RESPONSE_TIME_IN_SECONDS = 20;
export const DEVICE_LIST_WIDE_COLUMN_WIDTH = 350;
export const DEVICE_LIST_COLUMN_WIDTH = 150;

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

@ -0,0 +1,50 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export const ACCEPT = 'Accept';
export const CHECK = 'SkypeCheck';
export const CHECKED_CHECKBOX = 'CheckboxComposite';
export const CLEAR = 'Clear';
export const CLOSE = 'ChromeClose';
export const CODE = 'Code';
export const EMPTY_CHECKBOX = 'Checkbox';
export const INFO = 'Info';
export const NAV_OPEN = 'DoubleChevronLeft8';
export const NAV_CLOSED = 'DoubleChevronRight8';
export const REFRESH = 'Refresh';
export const RETURN = 'ReturnKey';
export const SAVE = 'Save';
export const SUBMIT = 'CloudUpload';
export const SYNCH = 'SyncOccurence';
export const WARNING = 'Warning';
export enum GroupedList {
OPEN = 'ChevronUp',
CLOSE = 'ChevronDown'
}
export enum Accordion {
OPEN = 'CaretSolidRight',
CLOSE = 'CaretSolidDown',
OPEN_OBJECT = 'ChevronRight',
CLOSE_OBJECT = 'ChevronDown',
OPEN_ARRAY = 'CaretSolidRight',
CLOSE_ARRAY = 'CaretSolidDown'
}
export enum ArrayOperation {
ADD = 'BoxAdditionSolid',
REMOVE = 'Delete'
}
export enum Heading {
SETTINGS_OPEN = 'PlugDisconnected',
SETTINGS_CLOSED = 'PlugConnected',
MESSAGE = 'SkypeMessage',
HELP = 'Help'
}
export enum InterfaceDetailCard {
OPEN = 'ChevronDown',
CLOSE = 'ChevronUp'
}

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

@ -0,0 +1,8 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export const modelDiscoveryInterfaceName = 'urn_azureiot_ModelDiscovery_DigitalTwin';
export const modelDefinitionInterfaceId = 'urn:azureiot:ModelDiscovery:ModelDefinition:1';
export const modelDefinitionInterfaceName = 'urn_azureiot_ModelDiscovery_ModelDefinition';
export const modelDefinitionCommandName = 'getModelDefinition';

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

@ -0,0 +1,9 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export enum REPOSITORY_LOCATION_TYPE {
Public = 'PUBLIC',
Private = 'PRIVATE',
Device = 'DEVICE'
}

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

@ -0,0 +1,19 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export enum SYNC_STATUS {
Failed,
None,
Synced,
Syncing
}
export enum DesiredStateStatus{
Success = 200,
Synching = 202,
Error = 500
}
export const MILLISECONDS_IN_MINUTE = 60000;
export const PUBLIC_REPO_ENDPOINT = 'https://canary-repo.azureiotrepository.com';

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

@ -0,0 +1,34 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.add-device{
.form {
padding: 20px;
.authentication{
padding-top: 30px;
.autoGenerateButton {
padding-top: 10px;
}
.ms-ChoiceFieldGroup-flexContainer {
display: flex;
.ms-ChoiceField-wrapper {
padding-right: 10px;
}
}
}
.connectivity {
padding-top: 30px;
}
.button-groups {
position: fixed;
bottom: 30px;
width: 100%;
.ms-Button {
padding: 0 10px 0 10px;
margin-right: 20px;
}
}
}
}

35
src/app/css/_app.scss Normal file
Просмотреть файл

@ -0,0 +1,35 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.app {
font-family: "Segoe UI" !important;
font-size: 14px !important;
display: grid;
width: 100%;
height: 100vh;
grid-template-columns: 1fr;
grid-template-rows: $mastheadHeight 1fr;
display: -ms-grid;
-ms-grid-columns: 1fr;
-ms-grid-rows: 40px 1fr;
.header {
grid-row: 1;
grid-column: 1;
-ms-grid-row: 1;
-ms-grid-column: 1;
}
.content {
grid-row: 2;
grid-column: 1;
-ms-grid-row: 2;
-ms-grid-column: 1;
}
}
body {
margin: 0;
}

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

@ -0,0 +1,47 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@import 'mixins';
.connectivity-pane {
top: 0;
bottom: 0;
position: absolute;
left: 0;
right: 0;
height: 100vh;
background-color: $opaque-blocking;
display: flex;
align-items: center;
justify-content: center;
.main {
background: #ffffff;
padding-top: 20px;
padding-bottom: 20px;
padding-left: 50px;
padding-right: 50px;
width: 650px;
.remember-connection-string {
.ms-Checkbox {
float: left;
padding-top: 7px;
}
}
.notes {
padding-top: 10px;
padding-bottom: 10px;
}
.connection-button {
display: flex;
flex-direction: row-reverse;
width: 100%;
padding-top: 50px;
}
}
}

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

@ -0,0 +1,57 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.copyableMaskField {
margin-bottom: 5px;
.labelSection {
text-overflow: ellipsis;
}
.copySection {
margin-right: 5px;
}
.controlSection {
display: flex;
}
.borderedSection {
display: flex;
flex-grow: 1;
border-width: 1px;
border-style: solid;
border-color: $textfield-border;
margin-right: 5px;
& > input {
border: none;
&:hover {
border: none;
}
&:focus {
border:none;
};
padding-left: 12px;
padding-right: 12px;
margin-top: 0px;
margin-bottom: 0px;
font-size: $textSize;
width: 100%;
text-overflow: ellipsis;
background-color: transparent;
}
}
.readOnly {
background-color: $textfield-disabled-background;
}
.error {
border-color: $errorBorder;
}
.errorSection {
color: $errorText;
font-size: $textSize;
}
}

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

@ -0,0 +1,11 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
.json-editor {
padding: 10px;
}
.preview-payload-button {
margin-left: 20px;
margin-top:10px;
}

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

@ -0,0 +1,131 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@import 'mixins';
$nav-collapsed-width: 40px;
$nav-width: 20%;
.device-content {
display: inline-block;
width: 100%;
.device-content-nav-bar.collapsed {
width:$nav-collapsed-width;
.nav-links {
display:none;
}
}
.device-content-nav-bar {
overflow-y: auto;
@include nav-bar;
display: flex;
flex-Direction: column;
width: $nav-width;
height: calc(100vh - 140px);
float: left;
.nav-links {
margin-left: 10px;
}
.nav-bar-item {
@include top-nav-item;
text-align: left;
}
.selected-nav-bar-item {
@include selected-nav-item
}
border: {
right: {
width: 1px;
style: solid;
color: $navBarBorderColor;
}
bottom: none;
}
}
.device-content-detail {
margin: 0;
margin-left: $nav-width;
.form-group {
.fieldChildren {
display: flex;
flex-Direction: row;
width: 100%;
.ms-ComboBox-container {
flex: 1;
}
.ms-DatePicker {
flex: 1;
}
.form-control {
flex: 1;
}
.form-control {
height: 26px;
padding: 1px 12px 1px 12px;
}
.form-control:disabled {
border: none;
color: rgb(166, 166, 166);
background-color: rgb(244, 244, 244);
height: 32px;
}
}
}
.each-property {
margin-bottom: 10px;
}
.ms-CommandBar {
background: $white;
border-bottom: 1px solid $light-gray;
.ms-Button--commandBar {
background: $white;
}
}
h3 {
font-size: 20px;
padding-left: 26px;
}
}
.device-content-detail.collapsed {
margin-left: $nav-collapsed-width;
}
.device-property {
margin-bottom: 50px;
.commandBar {
@include commandBar;
.syncBlock {
padding-left: 30px;
margin-top: -10px;
.labelFont {
font-weight: bold;
}
}
}
.ms-Overlay {
background-color: $blocking;
}
}
.device-command {
margin: 0px 0px 50px 3px;
.commandBar {
@include commandBar;
.commandTypeBlock {
padding-left: 30px;
.labelFont {
font-weight: bold;
}
}
}
}
}
.ms-Shimmer-container {
margin-top: 20px;
}

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

@ -0,0 +1,10 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
.nav-label{
font-weight: bold;
}
.navToggle {
margin-left:5px;
}

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

@ -0,0 +1,7 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
.device-detail {
padding: 20px;
}

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

@ -0,0 +1,28 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.device-events {
.device-events-container {
.events-loader {
padding: 0 25px;
.ms-Spinner {
float: left;
padding-right: 5px;
}
}
.device-events-content {
border-top: 1px solid $light-gray;
padding: 0 25px;
}
}
.scrollable {
overflow: auto;
margin-top: 10px;
height: calc(100vh - 250px);
}
}

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

@ -0,0 +1,44 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@import 'mixins';
.deviceId-label {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
}
.status-label {
font-size: 14px;
}
.pnp-icon {
width: 17px;
vertical-align: middle;
margin-right: 6px;
}
.delete-dialog {
.ms-Dialog-main {
min-width: 35vw;
max-width: 356px;
min-height: 30vh;
max-height: 392px;
}
.ms-Overlay {
background-color: $blocking;
}
ul.deleting-devices {
list-style: none;
min-height: 14vh;
max-height: 185px;
overflow: auto;
padding-left: 0px;
margin: {
top: 0px;
bottom: 0px;
}
}
}

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

@ -0,0 +1,37 @@
@import 'variables';
@import 'mixins';
.device-list-cell-container {
display: grid;
grid-template-columns: 2% auto;
column-gap: 5px;
grid-template-rows: 1fr 1fr 1fr;
}
.device-list-cell-container-content {
grid-column-start: 2;
font-size: 12px;
.device-list-cell-item {
padding: 0px 23px;
border: 0;
border-right: 1px solid $light-medium-gray;
}
.first {
padding-left: 0px;
}
.last {
padding-right: 0px;
border: 0;
}
.no-border {
border: 0;
}
.data {
font-weight: bold;
}
}

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

@ -0,0 +1,155 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@import 'mixins';
.deviceList-query {
width: calc(100vw - 24px);
padding-left: 24px;
overflow: auto;
word-wrap: break-word;
display: inline-block;
margin-top: 10px;
margin-bottom: 10px;
.deviceId-search {
float: left;
height: 32px;
border: {
width: 1px;
style: solid;
color: $gray;
right: none;
radius: 2px;
radius: 2px;
}
.search-box {
float: left;
width: calc(33vw - 45px);
}
.search-button {
padding-top: 1px;
max-width: 32px;
background-color: $blue;
color: $white;
padding-bottom: 1px;
}
.search-button:disabled {
background-color: $gray;
}
}
.clauses {
width: calc(67vw - 24px);
float: right;
.search-pill {
float:left;
padding: {
top: 0px;
bottom: 0px;
left: 4px;
right: 4px;
}
margin: {
left: 3px;
bottom: 3px;
right: 3px;
top: 0px;
}
line-height: 26px;
height: 32px;
border: {
width: 1px;
style: solid;
color: $gray;
radius: 16px;
}
.parameter-type {
float: left;
max-width: 170px;
line-height: 20px;
font-size: 10px;
}
.operation-type {
float: left;
max-width: 60px;
line-height: 20px;
font-size: 12px;
}
.clause-value {
float: left;
max-width: 150px;
line-height: 20px;
font-size: 10px;
}
.remove-pill {
width: 8px;
height: 26px;
float: right;
}
}
.search-pill:hover {
border-color: $blue;
}
.search-pill.error {
border-color: $red;
}
.search-pill.active {
padding-left: 10px;
padding-top: 3px;
padding-bottom: 1px;
padding-right: 6px;
min-width: 400px;
border-color: $blue;
max-height: 27px;
.ms-Button-icon {
font-size: 10px;
}
.ms-ComboBox-container {
height: 24px;
.ms-ComboBox {
height: 24px;
.ms-ComboBox-input{
height: 20px;
}
}
}
.ms-TextField {
height: 26px;
line-height: 24px;
.ms-TextField-fieldGroup {
height: 24px;
}
}
.ms-Dropdown {
height: 24px;
.ms-Dropdown-title {
height: 24px;
vertical-align: top;
line-height: 24px;
}
.ms-Dropdown-caretDownWrapper {
height: 26px;
line-height: 24px;
}
}
}
.search-pill.no-operator {
.parameter-type {
float: left;
max-width: 190px;
line-height: 20px;
font-size: 10px;
}
.clause-value {
float: left;
max-width: 190px;
line-height: 20px;
font-size: 10px;
}
}
}
}

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

@ -0,0 +1,255 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@mixin column-item-padding {
margin-top: 10px;
padding-left: 26px;
}
@mixin column-name-lg {
float: left;
width: 45%;
}
@mixin column-value-lg {
float: left;
width: 45%;
}
@mixin column-name-sm {
float: left;
width: 30%;
}
@mixin column-schema-sm {
width: 15%;
float: left;
}
@mixin column-unit-sm {
width: 15%;
float: left;
}
@mixin column-xs {
float: left;
width: 15%;
}
@mixin default-button {
background: $white;
color: $blue;
padding: 0;
}
@mixin validation-error {
padding-top: 10px;
color: $red;
font-size: 16px;
}
.pnp-detail-list {
padding-top: 20px;
.list-header {
font-weight: bold;
padding-bottom: 10px;
border-bottom: 3px solid $gray;
.column-name-xl {
padding-left: 26px;
}
.column-name {
@include column-name-lg;
padding-left: 26px;
}
.column-value {
width: 45%;
}
.column-toggle {
@include default-button;
float: right;
top: -8px;
}
.column-name-sm {
@include column-name-sm;
padding-left: 26px;
}
.column-schema-sm {
@include column-schema-sm;
}
.column-unit-sm {
@include column-unit-sm;
}
.column-value-sm {
width: 50%;
}
.column-timestamp-xs {
@include column-xs;
padding-left: 26px;
}
.column-name-xs {
@include column-xs;
}
}
}
.list-content{
width: calc(100% - 2px);
.list-item {
.item-oneline {
overflow: auto;
width: 100%;
border-bottom: 1px solid $light-gray;
.column-name {
@include column-name-sm;
@include column-item-padding;
}
.column-schema {
@include column-schema-sm;
@include column-item-padding;
}
.column-unit {
@include column-unit-sm;
@include column-item-padding;
}
.column-value-text {
float: left;
max-width: 35%;
@include column-item-padding;
.column-value-button {
@include default-button;
float: left;
margin: 10px 0 5px 5px;
}
.value-validation-error {
@include validation-error;
}
}
.column-timestamp-xs {
@include column-xs;
@include column-item-padding;
}
.column-name-xs {
@include column-xs;
@include column-item-padding;
}
}
.item-summary {
overflow: auto;
width: 100%;
.column-name {
@include column-name-lg;
@include column-item-padding;
}
.column-value-text {
@include column-value-lg;
@include column-item-padding;
.column-value-button {
@include default-button;
}
.value-validation-error {
@include validation-error;
}
}
.column-toggle {
padding-right: 2%;
float: right;
margin-top: 10px;
}
}
.item-summary-uncollapsed {
background: $light-gray;
}
.item-detail {
border-bottom: 1px solid $light-gray;
width: 100%;
float: left;
}
.item-detail-uncollapsed {
box-sizing: border-box;
padding: 0 10px 10px 18px;
background: $light-gray;
border-bottom: 1px solid $gray;
.schema-info-section {
padding-left: 8px;
}
.value-section {
padding: 20px 0 0 7px;
}
.submit-button {
margin-top: 10px;
}
}
.close-dialog-icon{
float: right;
}
dialog {
background-color: $white;
padding: 10px;
width: 50%;
position: fixed;
top: 0;
float: right;
height: 100%;
left: 49%;
border: none;
box-shadow: 0px 0px 50px $gray;
overflow-y: auto;
margin-top: 40px;
.panel-title {
margin-bottom: 40px;
padding-bottom: 20px;
h1 {
padding-top: 10px;
margin: 0px;
font-size: 18px;
}
}
}
}
}
.pnp-interface-info {
padding: 0 25px;
.source {
float:left;
.no-source-error {
color: $red;
}
}
.configure-button {
@include default-button;
padding-bottom: 3px;
}
.interface-definition
{
padding-top: 20px;
overflow-y: auto;
}
}
.ms-Panel-main {
margin-top: 43px;
}
.scrollable-lg {
height: calc(100vh - 300px);
overflow-y: auto;
}
.scrollable-sm {
height: calc(100vh - 350px);
overflow-y: auto;
}

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

@ -0,0 +1,32 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.status-success {
color: $successText;
}
.status-error {
color: $errorText;
}
.status-synching {
color: $infoText;
}
.status-unknown {
color: $black;
}
.status-icon {
font-size: 16px !important;
padding: 0px 10px;
position: relative;
top: 2px;
}
span[class^='status-'] {
font-size: 16px;
vertical-align: top;
position: relative;
bottom: 2px;
}
.reported-property {
max-width: 30%;
}

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

@ -0,0 +1,69 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@import 'mixins';
.grouped-list {
margin: 0px 24px;
.ms-GroupedList-group {
&:hover {
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.16), 0px 4px 8px rgba(0, 0, 0, 0.12);
}
}
}
.grouped-list-group-header {
border: 0;
border-top: 1px solid $gray;
.expanded {
background-color: $cell-gray;
}
}
.grouped-list-header-border {
border-top: 1px solid $gray;
left: 0;
right: 0;
}
.grouped-list-group-checkbox {
justify-content: center;
}
.grouped-list-group-cell {
background-color: $cell-gray;
}
.grouped-list-group-content {
#collapse {
margin-left: auto;
}
& i {
font-size: 12px;
}
a:link {
text-decoration: none;
color: $link-blue;
}
a:visited {
text-decoration: none;
color: $link-blue;
}
a:hover {
text-decoration: none;
color: $link-blue;
}
a:active {
text-decoration: none;
color: $link-blue;
}
}

32
src/app/css/_header.scss Normal file
Просмотреть файл

@ -0,0 +1,32 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.header {
display: grid;
grid-template-areas: 'title search notifications settings questions';
grid-template-columns: 1fr 150px 100px;
background-color: $dark-gray;
color: white;
line-height: 20px;
width: 100%;
h1 {
font-size: 16px;
line-height: 22px;
grid-area: title;
padding-left: 10px;
margin-right: 10px;
}
.ms-Button {
background: transparent;
color:$white;
padding-top: 10px;
}
.active {
background: {
color: $medium-gray;
}
}
}

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

@ -0,0 +1,19 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.collapse {
flex-direction: row;
display: flex;
background-color: $navSelected;
padding: 1px;
.label {
color: $blue;
}
.button {
margin-right: 5px;
color: $blue;
}
}

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

@ -0,0 +1,12 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.ms-MessageBar {
width: calc(100% - 2px);
.ms-Button{
background: $white;
}
}

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

@ -0,0 +1,19 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.labelWithTooltip {
display: flex;
align-items: center;
& label {
font-size: $textSize;
color: $textColor;
}
& i {
font-size: 12px;
}
}

67
src/app/css/_layouts.scss Normal file
Просмотреть файл

@ -0,0 +1,67 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.view {
width: 100%;
display: grid;
height: calc(100vh - #{$mastheadHeight});
grid-template-rows: auto auto 1fr;
grid-template-columns: 1fr;
display: -ms-grid;
-ms-grid-columns: 1fr;
-ms-grid-rows: auto auto 1fr;
row-gap: 0;
}
.view-header {
min-height: 90px;
width: 100%;
grid-row: 1;
grid-column: 1;
-ms-grid-row: 1;
-ms-grid-column: 1;
}
.view-command {
min-height: 40px;
width: 100%;
grid-row: 2;
grid-column: 1;
-ms-grid-row: 2;
-ms-grid-column: 1;
}
.view-content {
grid-row: 3;
grid-column: 1;
-ms-grid-row: 3;
-ms-grid-column: 1;
border-top: 1px $light-gray solid;
}
.edit {
width: 100%;
display: grid;
height: calc(100vh - #{$mastheadHeight});
grid-template-rows: auto 1fr;
grid-template-columns: 1fr;
display: -ms-grid;
-ms-grid-columns: 1fr;
-ms-grid-rows: auto 1fr;
row-gap: 0;
}
.edit-content {
grid-row: 2;
grid-column: 1;
-ms-grid-row: 2;
-ms-grid-column: 1;
border-top: 1px $light-gray solid;
}
.view-scroll {
overflow: auto;
}

38
src/app/css/_navBar.scss Normal file
Просмотреть файл

@ -0,0 +1,38 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@import 'mixins';
.top-nav-bar {
position: relative;
left: 0px;
right: 0px;
top: 0px;
background-color: white;
z-index: 10;
@include nav-bar;
.nav-bar-item {
@include top-nav-item;
}
.selected-nav-bar-item {
@include selected-nav-item;
}
.ms-Dropdown-container {
@media screen and (min-width: $screenSize) {
width: 200px;
float: right;
height: 50px;
.nav-bar-language-dropdown {
margin: 8px 8px;
}
.ms-Dropdown-items {
width: 200px;
}
}
@media screen and (max-width: $screenSize) {
display: none;
}
}
}

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

@ -0,0 +1,16 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
.no-match-error {
position: relative;
top: 300px;
.no-match-error-description {
text-align: center;
}
.no-match-error-button {
text-align: center;
}
}

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

@ -0,0 +1,91 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.new-notifications::after {
content: '';
position: absolute;
top: 7.5px;
right: 106px;
width: 10px;
height: 10px;
color: transparent;
border-radius: 50%;
text-align: center;
background: $blue;
}
.notification-list-divider {
height: 0px;
border: 0;
border-top: 1px solid $grey-border
}
.notification-list-header {
margin-left: 40px;
margin-right: 40px;
.commands {
text-align: right;
}
}
.notification-list-entry {
display: flex;
width: 250px;
margin-top: 10px;
margin-right: 5px;
padding-top: 5px;
padding-bottom: 5px;
.success {
color: $successText;
}
.error {
color: $errorText;
}
.warning {
color: $warningText;
}
.info {
color: $infoText;
}
.body {
width: 225px;
padding-left: 10px;
text-overflow: ellipsis;
.title {
font-weight: 550;
text-overflow: ellipsis;
color: $medium-gray;
padding-bottom: 7px;
}
.message {
padding-bottom: 7px;
color: $textColor;
text-overflow: ellipsis;
word-wrap:break-word;
}
.time {
color: $textColor
}
}
}
.notification-toast-body {
word-break: break-word;
}
.Toastify__toast-container {
margin-top:25px;
}
.notification-toast-progress-bar {
background: $light-medium-gray !important;
height: 2px;
}

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

@ -0,0 +1,106 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
.settingsPane {
header.panel-header {
h2 {
height: 28px;
font: {
size: 20px;
}
line-height: 24px;
}
}
h3[role="heading"] {
font: {
size: 14px;
}
line-height: 20px;
color: #212121;
}
.helptext {
font-size: 12px;
line-height: 19px;
text-decoration-line: underline;
color: #666666;
}
.remember-connection-string {
.ms-Checkbox {
float: left;
padding-top: 7px;
}
}
.location-list {
.item {
display: grid;
grid-template-columns: 40px 1fr;
grid-template-areas: "numbering item";
.numbering {
font-size: 32px;
line-height: 37px;
color: #A6A6A6;
margin-top: 10px;
margin-right: 33px;
grid-area: numbering;
}
.location-item {
grid-area: item;
position: relative;
list-style: none;
margin-bottom: 8px;
margin-top: 8px;
border: 1px solid $navBarBorderColor;
border-radius: 2px;
box-shadow: 0px 4px 4px $blocking;
padding: {
bottom: 14px;
left: 22px;
top: 14px;
right: 12px;
}
.remove-button {
height: 16px;
width: 16px;
position: absolute;
right: 12px;
top: 15px;
}
.connection-string {
width: 70%;
max-width: 325px;
}
}
}
}
.faq {
padding: {
left: 0px;
}
.faq-item {
list-style-type: none;
}
}
.settings-footer {
.footer-buttons {
h3[role="heading"] {
font-weight: normal;
}
button[type="submit"] {
margin-right: 18px;
}
}
padding: {
left: 32px;
bottom: 40px;
}
margin-bottom: 30px;
}
}

71
src/app/css/mixins.scss Normal file
Просмотреть файл

@ -0,0 +1,71 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import 'variables';
@mixin nav-bar {
min-height: 50px;
@media screen and (min-width: $screenSize) {
border: {
bottom: {
width: 1px;
style: solid;
color: $navBarBorderColor;
}
}
}
}
@mixin nav-item($selected-class: 'selected-nav-bar-item', $hover-color: $light-gray) {
height: 50px;
display: inline-block;
color: black;
text-decoration: none;
line-height: 50px;
&:hover:not(.#{$selected-class}) {
background-color: $hover-color;
}
}
@mixin top-nav-item($selected-class: 'selected-nav-bar-item', $hover-color: $light-gray, $hover-border-color: $gray) {
@include nav-item($selected-class, $hover-color);
min-width: 100px;
text-align: center;
padding: {
left: 10px;
right: 10px;
}
&:hover:not(.#{$selected-class}) {
color: $blue;
font: {
weight: bold;
}
}
}
@mixin selected-nav-item {
font-weight: bold;
color: $blue;
border: {
bottom: {
style: solid;
color: $blue;
width: 2px;
}
}
}
@mixin form-control-readonly {
border-width: 0px;
color: $textfield-disabled-font;
background-color: $light-gray;
cursor: not-allowed;
}
@mixin commandBar {
display: flex;
height: 40px;
}

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

@ -0,0 +1,39 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
// Palette
$black: #000000;
$red: #e81123;
$white: rgb(255, 255, 255);
$blue: rgb(0, 120, 212);
$link-blue: #0065D9;
$skyblue: #5bbde1;
$gray: rgb(206, 206, 206);
$light-gray: #f4f4f4;
$light-medium-gray: #a6a6a6;
$dark-gray: #333333;
$opaque-blocking: #cccccc;
$medium-gray: #666666;
$grey-border: #D9D9D9;
// semantic styles
$blocking: rgba(0,0,0,0.2);
$navBarBorderColor: rgb(231, 231, 231);
$successText: #7fba00;
$errorText: #e00b1c;
$errorBorder: #a80000;
$warningText: #A50606;
$infoText: #0058ad;
$textColor: $medium-gray;
$textfield-border: $light-gray;
$textfield-disabled-font: rgb(51, 51, 51);
$textfield-disabled-background: $light-gray;
$textSize: 14px;
$navSelected: rgb(244, 244, 244);
$cell-gray: #f8f9f9;
$list-column-header-color: #666666;
// sizing constants
$mastheadHeight: 40px;
$screenSize: 800px;

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

@ -0,0 +1,68 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import actionCreatorFactory from 'typescript-fsa';
import * as actionPrefixes from '../../constants/actionPrefixes';
import * as actionTypes from '../../constants/actionTypes';
import { ModelDefinition } from '../../api/models/modelDefinition';
import { InvokeMethodParameters } from '../../api/parameters/deviceParameters';
import { Twin } from '../../api/models/device';
import { DeviceIdentity } from '../../api/models/deviceIdentity';
import { DigitalTwinInterfaces } from './../../api/models/digitalTwinModels';
import { REPOSITORY_LOCATION_TYPE } from './../../constants/repositoryLocationTypes';
const deviceContentCreator = actionCreatorFactory(actionPrefixes.DEVICECONTENT);
const clearModelDefinitionsAction = deviceContentCreator(actionTypes.CLEAR_MODEL_DEFINITIONS);
const getDeviceIdentityAction = deviceContentCreator.async<string, DeviceIdentity> (actionTypes.GET_DEVICE_IDENTITY);
const getDigitalTwinInterfacePropertiesAction = deviceContentCreator.async<string, DigitalTwinInterfaces>(actionTypes.GET_DIGITAL_TWIN_INTERFACE_PROPERTIES);
const getTwinAction = deviceContentCreator.async<string, Twin>(actionTypes.GET_TWIN);
const getModelDefinitionAction = deviceContentCreator.async<GetModelDefinitionActionParameters, ModelDefinitionActionResult>(actionTypes.FETCH_MODEL_DEFINITION);
const invokeDeviceMethodAction = deviceContentCreator.async<InvokeMethodParameters, string>(actionTypes.INVOKE_DEVICE_METHOD);
const invokeDigitalTwinInterfaceCommandAction = deviceContentCreator.async<InvokeDigitalTwinInterfaceCommandActionParameters, string>(actionTypes.INVOKE_DIGITAL_TWIN_INTERFACE_COMMAND);
const patchDigitalTwinInterfacePropertiesAction = deviceContentCreator.async<PatchDigitalTwinInterfacePropertiesActionParameters, DigitalTwinInterfaces>(actionTypes.PATCH_DIGITAL_TWIN_INTERFACE_PROPERTIES);
const setInterfaceIdAction = deviceContentCreator<string>(actionTypes.SET_INTERFACE_ID);
const updateDeviceIdentityAction = deviceContentCreator.async<DeviceIdentity, DeviceIdentity> (actionTypes.UPDATE_DEVICE_IDENTITY);
const updateTwinAction = deviceContentCreator.async<UpdateTwinActionParameters, Twin>(actionTypes.UPDATE_TWIN);
export {
clearModelDefinitionsAction,
getDeviceIdentityAction,
getDigitalTwinInterfacePropertiesAction,
getTwinAction,
getModelDefinitionAction,
invokeDeviceMethodAction,
invokeDigitalTwinInterfaceCommandAction,
patchDigitalTwinInterfacePropertiesAction,
setInterfaceIdAction,
updateTwinAction,
updateDeviceIdentityAction
};
export interface PatchDigitalTwinInterfacePropertiesActionParameters {
digitalTwinId: string;
interfacesPatchData: any; // tslint:disable-line:no-any
propertyKey: string;
}
export interface InvokeDigitalTwinInterfaceCommandActionParameters {
digitalTwinId: string;
commandName: string;
commandPayload: any; // tslint:disable-line:no-any
propertyKey?: string;
}
export interface UpdateTwinActionParameters {
deviceId: string;
twin: Twin;
}
export interface ModelDefinitionActionResult {
modelDefinition: ModelDefinition;
source: REPOSITORY_LOCATION_TYPE;
}
export interface GetModelDefinitionActionParameters {
digitalTwinId: string;
interfaceId: string;
}

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

@ -0,0 +1,201 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/devices/deviceContentNav matches snapshot when there device is not pnp 1`] = `
<div
className="view-scroll"
>
<div
role="navigation"
>
<StyledNavBase
groups={
Array [
Object {
"links": Array [
Object {
"key": "identity",
"name": undefined,
"url": "#/devices/detail/identity/?id=test",
},
Object {
"key": "twin",
"name": undefined,
"url": "#/devices/detail/twin/?id=test",
},
Object {
"key": "events",
"name": undefined,
"url": "#/devices/detail/events/?id=test",
},
],
"name": undefined,
},
]
}
onRenderGroupHeader={[Function]}
/>
</div>
</div>
`;
exports[`components/devices/deviceContentNav matches snapshot when there is no interface selected 1`] = `
<div
className="view-scroll"
>
<div
role="navigation"
>
<StyledNavBase
groups={
Array [
Object {
"links": Array [
Object {
"key": "identity",
"name": undefined,
"url": "#/devices/detail/identity/?id=test",
},
Object {
"key": "twin",
"name": undefined,
"url": "#/devices/detail/twin/?id=test",
},
Object {
"key": "events",
"name": undefined,
"url": "#/devices/detail/events/?id=test",
},
],
"name": undefined,
},
Object {
"links": Array [
Object {
"isExpanded": undefined,
"links": Array [
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/interfaces/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/settings/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/properties/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/commands/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/events/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
],
"name": "urn:azureiot:com:DeviceInformation:1",
"onClick": [Function],
"url": "",
},
],
"name": undefined,
},
]
}
onRenderGroupHeader={[Function]}
/>
</div>
</div>
`;
exports[`components/devices/deviceContentNav redirects snapshot when there is a interface selected 1`] = `
<div
className="view-scroll"
>
<div
role="navigation"
>
<StyledNavBase
groups={
Array [
Object {
"links": Array [
Object {
"key": "identity",
"name": undefined,
"url": "#/devices/detail/identity/?id=test",
},
Object {
"key": "twin",
"name": undefined,
"url": "#/devices/detail/twin/?id=test",
},
Object {
"key": "events",
"name": undefined,
"url": "#/devices/detail/events/?id=test",
},
],
"name": undefined,
},
Object {
"links": Array [
Object {
"isExpanded": true,
"links": Array [
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/interfaces/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/settings/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/properties/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/commands/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
Object {
"name": undefined,
"onClick": [Function],
"parentId": "urn:azureiot:com:DeviceInformation:1",
"url": "#/devices/detail/digitalTwins/events/?id=test&interfaceId=urn:azureiot:com:DeviceInformation:1",
},
],
"name": "urn:azureiot:com:DeviceInformation:1",
"onClick": [Function],
"url": "",
},
],
"name": undefined,
},
]
}
onRenderGroupHeader={[Function]}
/>
</div>
</div>
`;

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

@ -0,0 +1,90 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { Shimmer, CommandBar } from 'office-ui-fabric-react';
import { RouteComponentProps } from 'react-router-dom';
import DeviceCommandPerInterface from './deviceCommandsPerInterface';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../../localization/resourceKeys';
import { InvokeDigitalTwinInterfaceCommandActionParameters } from '../../actions';
import { getDeviceIdFromQueryString, getInterfaceIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
import { CommandSchema } from './deviceCommandsPerInterfacePerCommand';
import InterfaceNotFoundMessageBoxContainer from '../shared/interfaceNotFoundMessageBarContainer';
import { REFRESH } from '../../../../constants/iconNames';
export interface DeviceCommandsProps extends DeviceInterfaceWithSchema{
isLoading: boolean;
}
export interface DeviceInterfaceWithSchema {
interfaceName: string;
commandSchemas: CommandSchema[];
}
export interface DeviceCommandDispatchProps {
refresh: (deviceId: string, interfaceId: string) => void;
invokeDigitalTwinInterfaceCommand: (parameters: InvokeDigitalTwinInterfaceCommandActionParameters) => void;
setInterfaceId: (id: string) => void;
}
export default class DeviceCommands
extends React.Component<DeviceCommandsProps & DeviceCommandDispatchProps & RouteComponentProps> {
constructor(props: DeviceCommandsProps & DeviceCommandDispatchProps & RouteComponentProps) {
super(props);
}
public render(): JSX.Element {
if (this.props.isLoading) {
return (
<Shimmer/>
);
}
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
<>
<CommandBar
className="command"
items={[
{
ariaLabel: context.t(ResourceKeys.deviceSettings.command.refresh),
iconProps: {iconName: REFRESH},
key: REFRESH,
name: context.t(ResourceKeys.deviceSettings.command.refresh),
onClick: this.handleRefresh
}
]}
/>
{this.renderCommandsPerInterface(context)}
</>
)}
</LocalizationContextConsumer>
);
}
public componentDidMount() {
this.props.setInterfaceId(getInterfaceIdFromQueryString(this.props));
}
private readonly renderCommandsPerInterface = (context: LocalizationContextInterface) => {
return (
<>
<h3>{context.t(ResourceKeys.deviceCommands.headerText)}</h3>
{ this.props.commandSchemas ?
<DeviceCommandPerInterface
{...this.props}
deviceId={getDeviceIdFromQueryString(this.props)}
/> :
<InterfaceNotFoundMessageBoxContainer/>
}
</>
);
}
private readonly handleRefresh = () => {
this.props.refresh(getDeviceIdFromQueryString(this.props), getInterfaceIdFromQueryString(this.props));
}
}

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

@ -0,0 +1,35 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { compose, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { AnyAction } from 'typescript-fsa';
import { StateType } from '../../../../shared/redux/state';
import DeviceCommands, { DeviceCommandDispatchProps, DeviceCommandsProps } from './deviceCommands';
import { getDeviceCommandPairs } from './selectors';
import { invokeDigitalTwinInterfaceCommandAction, setInterfaceIdAction, InvokeDigitalTwinInterfaceCommandActionParameters, getModelDefinitionAction } from '../../actions';
import { getDeviceTwinStateSelector } from '../deviceTwin/selectors';
import { getModelDefinitionSyncStatusSelector, getInterfaceNameSelector } from '../../selectors';
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
const mapStateToProps = (state: StateType): DeviceCommandsProps => {
return {
isLoading: getDeviceTwinStateSelector(state) === SynchronizationStatus.working ||
getModelDefinitionSyncStatusSelector(state) === SynchronizationStatus.working,
...getDeviceCommandPairs(state),
interfaceName: getInterfaceNameSelector(state)
};
};
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>): DeviceCommandDispatchProps => {
return {
invokeDigitalTwinInterfaceCommand:
(parameters: InvokeDigitalTwinInterfaceCommandActionParameters) => dispatch(invokeDigitalTwinInterfaceCommandAction.started(parameters)),
refresh: (deviceId: string, interfaceId: string) => dispatch(getModelDefinitionAction.started({digitalTwinId: deviceId, interfaceId})),
setInterfaceId: (id: string) => dispatch(setInterfaceIdAction(id))
};
};
export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps))(DeviceCommands);

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

@ -0,0 +1,96 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import DeviceCommandsPerInterfacePerCommand, { CommandSchema } from './deviceCommandsPerInterfacePerCommand';
import { InvokeDigitalTwinInterfaceCommandActionParameters } from '../../actions';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../../localization/resourceKeys';
import '../../../../css/_devicePnpDetailList.scss';
export interface DeviceCommandDataProps {
commandSchemas: CommandSchema[];
deviceId: string;
interfaceName: string;
}
export interface DeviceCommandDispatchProps {
invokeDigitalTwinInterfaceCommand: (parameters: InvokeDigitalTwinInterfaceCommandActionParameters) => void;
}
export interface DeviceCommandState {
collapseMap: Map<number, boolean>;
allCollapsed: boolean;
}
export default class DeviceCommandsPerInterface
extends React.Component<DeviceCommandDataProps & DeviceCommandDispatchProps, DeviceCommandState> {
constructor(props: DeviceCommandDataProps & DeviceCommandDispatchProps) {
super(props);
const { commandSchemas } = this.props;
const collapseMap = new Map();
for (let index = 0; index < commandSchemas.length; index ++) {
collapseMap.set(index, true);
}
this.state = {
allCollapsed: true,
collapseMap
};
}
public render(): JSX.Element {
const { commandSchemas } = this.props;
const commands = commandSchemas && commandSchemas.map((schema, index) => (
<DeviceCommandsPerInterfacePerCommand
key={index}
{...this.props}
{...schema}
collapsed={this.state.collapseMap.get(index)}
handleCollapseToggle={this.handleCollapseToggle(index)}
/>
));
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
<div className="pnp-detail-list">
<div className="list-header">
<span className="column-name-xl">{context.t(ResourceKeys.deviceCommands.columns.name)}</span>
<DefaultButton
className="column-toggle"
onClick={this.onToggleCollapseAll}
>
{this.state.allCollapsed ?
context.t(ResourceKeys.deviceCommands.command.expandAll) :
context.t(ResourceKeys.deviceCommands.command.collapseAll)}
</DefaultButton>
</div>
<section role="list" className="list-content scrollable-lg">
{commands}
</section>
</div>
)}
</LocalizationContextConsumer>
);
}
private readonly onToggleCollapseAll = () => {
const allCollapsed = this.state.allCollapsed;
const collapseMap = new Map();
for (let index = 0; index < this.state.collapseMap.size; index ++) {
collapseMap.set(index, !allCollapsed);
}
this.setState({allCollapsed: !allCollapsed, collapseMap});
}
private readonly handleCollapseToggle = (index: number) => () => {
const collapseMap = this.state.collapseMap;
collapseMap.set(index, !collapseMap.get(index));
this.setState({collapseMap});
}
}

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

@ -0,0 +1,150 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { IconButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../../localization/resourceKeys';
import { InterfaceDetailCard, SUBMIT } from '../../../../constants/iconNames';
import { ParsedCommandSchema } from '../../../../api/models/interfaceJsonParserOutput';
import { CommandContent } from '../../../../api/models/modelDefinition';
import DataForm from '../shared/dataForm';
import { InvokeDigitalTwinInterfaceCommandActionParameters } from '../../actions';
import { generateCommandPayload } from '../../sagas/digitalTwinInterfaceCommandSaga';
export interface DeviceCommandDataProps extends CommandSchema {
collapsed: boolean;
deviceId: string;
interfaceName: string;
}
export interface DeviceCommandDispatchProps {
handleCollapseToggle: () => void;
invokeDigitalTwinInterfaceCommand: (parameters: InvokeDigitalTwinInterfaceCommandActionParameters) => void;
}
export interface CommandSchema {
parsedSchema: ParsedCommandSchema;
commandModelDefinition: CommandContent;
}
export default class DeviceCommandsPerInterfacePerCommand
extends React.Component<DeviceCommandDataProps & DeviceCommandDispatchProps> {
public render(): JSX.Element {
const commandForm = (
<article className="list-item" role="listitem">
<LocalizationContextConsumer >
{(context: LocalizationContextInterface) => (
<>
{this.createCollapsedSummary(context)}
{this.createUncollapsedCard(context)}
</>
)}
</LocalizationContextConsumer>
</article>
);
return (commandForm);
}
private readonly createCollapsedSummary = (context: LocalizationContextInterface) => {
return (
<header className={this.props.collapsed ? 'item-summary' : 'item-summary item-summary-uncollapsed'} onClick={this.handleToggleCollapse}>
{this.renderCommandName(context)}
<IconButton
title={context.t(this.props.collapsed ? ResourceKeys.deviceCommands.command.expand : ResourceKeys.deviceCommands.command.collapse)}
className="column-toggle"
iconProps={{iconName: this.props.collapsed ? InterfaceDetailCard.OPEN : InterfaceDetailCard.CLOSE}}
/>
</header>
);
}
private readonly createUncollapsedCard = (context: LocalizationContextInterface) => {
return (
<section className={this.props.collapsed ? 'item-detail' : 'item-detail item-detail-uncollapsed'}>
{!this.props.collapsed &&
<>
{this.renderCommandSchemaInformation(context)}
{this.props.commandModelDefinition.request ? this.createForm() :
<PrimaryButton
className="submit-button"
onClick={this.onSubmit({})}
text={context.t(ResourceKeys.deviceCommands.command.submit)}
iconProps={{ iconName: SUBMIT }}
/>
}
</>
}
</section>
);
}
private readonly createForm = () => {
return (
<DataForm
buttonText={ResourceKeys.deviceCommands.command.submit}
formData={undefined}
interfaceName={this.props.interfaceName}
settingSchema={this.props.parsedSchema.requestSchema}
handleSave={this.onSubmit}
craftPayload={this.craftCommandPayload}
schema={this.getRequestSchema()}
/>
);
}
private readonly craftCommandPayload = (interfaceName: string, propertyKey: string, payload: object) => {
return generateCommandPayload(propertyKey, payload);
}
private readonly renderCommandSchemaInformation = (context: LocalizationContextInterface) => {
const { commandModelDefinition } = this.props;
return (
<section className="schema-info-section">
<Label>
{context.t(ResourceKeys.deviceCommands.details.description)}: {commandModelDefinition.description ? commandModelDefinition.description : '--'}
</Label>
<Label>{context.t(ResourceKeys.deviceCommands.details.schema)}: {this.getRequestSchema()}</Label>
<Label>
{context.t(ResourceKeys.deviceCommands.details.type)}: {commandModelDefinition.commandType ? commandModelDefinition.commandType : '--'}
</Label>
</section>
);
}
private readonly getRequestSchema = () => {
const { commandModelDefinition } = this.props;
const schema = commandModelDefinition.request;
if (!schema) {
return '--';
}
else {
return typeof schema.schema === 'string' ?
schema.schema :
schema.schema['@type'];
}
}
private readonly renderCommandName = (context: LocalizationContextInterface) => {
const ariaLabel = context.t(ResourceKeys.deviceCommands.columns.name);
const displayName = this.props.commandModelDefinition.displayName;
return <Label aria-label={ariaLabel} className="column-name">{this.props.commandModelDefinition.name} ({displayName ? displayName : '--'})</Label>;
}
private readonly onSubmit = (data: any) => () => { // tslint:disable-line:no-any
this.props.invokeDigitalTwinInterfaceCommand({
commandName: this.props.commandModelDefinition.name,
commandPayload: data,
digitalTwinId: this.props.deviceId,
propertyKey: this.props.commandModelDefinition.request && this.props.commandModelDefinition.request.name
});
}
private readonly handleToggleCollapse = () => {
this.props.handleCollapseToggle();
}
}

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

@ -0,0 +1,115 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import { Record } from 'immutable';
import { ModelDefinition } from '../../../../api/models/modelDefinition';
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
import { getDeviceCommandPairs } from './selectors';
import { getInitialState } from '../../../../api/shared/testHelper';
describe('getDeviceCommandPairs', () => {
it('returns interface commands', () => {
const state = getInitialState();
const interfaceId = 'urn:contoso:com:environmentalsensor:1';
/* tslint:disable */
const digitalTwinInterfaceProperties = {
"interfaces": {
"urn_azureiot_ModelDiscovery_DigitalTwin": {
"name": "urn_azureiot_ModelDiscovery_DigitalTwin",
"properties": {
"modelInformation": {
"reported": {
"value": {
"modelId": "urn:contoso:com:dcm:2",
"interfaces": {
"environmentalsensor": interfaceId,
"urn_azureiot_ModelDiscovery_ModelInformation": "urn:azureiot:ModelDiscovery:ModelInformation:1",
"urn_azureiot_ModelDiscovery_DigitalTwin": "urn:azureiot:ModelDiscovery:DigitalTwin:1"
}
}
}
}
}
}
},
"version": 1
};
const modelDefinition: ModelDefinition = {
"@id": interfaceId,
"@type": "Interface",
"displayName": "Digital Twin",
"contents": [
{
"@type": "Command",
"description": "This command will begin blinking the LED for given time interval.",
"name": "blink",
"request": {
"name": "blinkRequest",
"schema": "long"
},
"response": {
"name": "blinkResponse",
"schema": "string"
},
"commandType": "synchronous"
}
],
"@context": "http://azureiot.com/v1/contexts/Interface.json"
}
/* tslint:enable */
state.deviceContentState = Record({
deviceIdentity: null,
deviceTwin: null,
digitalTwinInterfaceProperties: {
digitalTwinInterfaceProperties,
digitalTwinInterfacePropertiesSyncStatus: SynchronizationStatus.fetched
},
interfaceIdSelected: interfaceId,
invokeMethodResponse: '',
modelDefinitionWithSource: {
modelDefinition,
modelDefinitionSynchronizationStatus: SynchronizationStatus.fetched,
source: null
}
})();
const commandSchemas = [
{
commandModelDefinition: {
'@type': 'Command',
'commandType': 'synchronous',
'description': 'This command will begin blinking the LED for given time interval.',
'name': 'blink',
'request': {
name: 'blinkRequest',
schema: 'long'
},
'response': {
name: 'blinkResponse',
schema: 'string'
}
},
parsedSchema: {
description: 'This command will begin blinking the LED for given time interval.',
name: 'blink',
requestSchema: {
description: '',
title: 'blinkRequest',
type: 'number'
},
responseSchema: {
description: '',
title: 'blinkResponse',
type: 'string'
}
}
}
];
expect(getDeviceCommandPairs(state))
.toEqual({commandSchemas, interfaceName: 'environmentalsensor'});
});
});

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

@ -0,0 +1,32 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { ContentType, CommandContent } from '../../../../api/models/modelDefinition';
import { parseInterfaceCommandToJsonSchema } from './../../../../shared/utils/jsonSchemaAdaptor';
import { StateInterface } from '../../../../shared/redux/state';
import { DeviceInterfaceWithSchema } from './deviceCommands';
import { getModelDefinitionSelector, getInterfaceNameSelector } from '../../selectors';
export const getDeviceCommandPairs = (state: StateInterface): DeviceInterfaceWithSchema => {
const modelDefinition = getModelDefinitionSelector(state);
const commands = modelDefinition && modelDefinition.contents && modelDefinition.contents.filter((item: CommandContent) => filterCommand(item));
return {
commandSchemas: commands && commands.map(command => ({
commandModelDefinition: command,
parsedSchema: parseInterfaceCommandToJsonSchema(command),
})),
interfaceName: getInterfaceNameSelector(state)
};
};
const filterCommand = (content: CommandContent) => {
if (typeof content['@type'] === 'string') {
return content['@type'].toLowerCase() === ContentType.Command;
}
else {
return content['@type'].some((entry: string) => entry.toLowerCase() === ContentType.Command);
}
};
export default getDeviceCommandPairs;

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

@ -0,0 +1,111 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { Route } from 'react-router-dom';
import { IconButton } from 'office-ui-fabric-react';
import DeviceIdentityContainer from './deviceIdentity/deviceIdentityContainer';
import DeviceTwinContainer from './deviceTwin/deviceTwinContainer';
import DeviceEventsContainer from './deviceEvents/deviceEventsContainer';
import DeviceMethodsContainer from './deviceMethods/deviceMethodsContainer';
import DeviceContentNavComponent from './deviceContentNav';
import BreadcrumbContainer from '../../../shared/components/breadcrumbContainer';
import DigitalTwinsContentContainer from './digitalTwinContentContainer';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext';
import '../../../css/_deviceContent.scss';
import '../../../css/_layouts.scss';
import { NAV_OPEN, NAV_CLOSED } from '../../../constants/iconNames';
interface DeviceContentState {
appMenuVisible: boolean;
}
export interface DeviceContentDataProps {
interfaceIds: string[];
isLoading: boolean;
isPnPDevice: boolean;
}
export interface DeviceContentProps extends DeviceContentDataProps {
deviceId: string;
interfaceId: string;
}
export interface DeviceContentDispatchProps {
setInterfaceId: (interfaceId: string) => void;
getDigitalTwinInterfaceProperties: (deviceId: string) => void;
}
export class DeviceContentComponent extends React.Component<DeviceContentProps & DeviceContentDispatchProps, DeviceContentState> {
constructor(props: DeviceContentProps & DeviceContentDispatchProps) {
super(props);
this.state = {
appMenuVisible: true
};
}
// tslint:disable: cyclomatic-complexity
public render(): JSX.Element {
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
<div className="edit">
<div className="view-header">
<Route component={BreadcrumbContainer} />
</div>
{this.props.deviceId &&
<div className="view-content view-scroll">
<div className="device-content">
{this.props.deviceId &&
<div className={'device-content-nav-bar' + (!this.state.appMenuVisible ? ' collapsed' : '')} role="tablist">
<nav>
<div className="navToggle">
<IconButton
tabIndex={0}
iconProps={{ iconName: this.state.appMenuVisible ? NAV_OPEN : NAV_CLOSED }}
title={this.state.appMenuVisible ? context.t(ResourceKeys.deviceContent.navBar.collapse) : context.t(ResourceKeys.deviceContent.navBar.expand)}
ariaLabel={this.state.appMenuVisible ? context.t(ResourceKeys.deviceContent.navBar.collapse) : context.t(ResourceKeys.deviceContent.navBar.expand)}
onClick={this.collapseToggle}
/>
</div>
<div className="nav-links">
{!this.props.isLoading && this.createNavLinks()}
</div>
</nav>
</div>
}
<div className={'device-content-detail' + (!this.state.appMenuVisible ? ' collapsed' : '')}>
<Route path="/devices/detail/identity/" component={DeviceIdentityContainer} />
<Route path="/devices/detail/twin/" component={DeviceTwinContainer} />
<Route path="/devices/detail/events/" component={DeviceEventsContainer}/>
<Route path="/devices/detail/methods/" component={DeviceMethodsContainer} />
<Route path="/devices/detail/digitalTwins/" component={DigitalTwinsContentContainer} />
</div>
</div>
</div>
}
</div>
)}
</LocalizationContextConsumer>
);
}
public componentDidMount() {
this.props.getDigitalTwinInterfaceProperties(this.props.deviceId);
}
private readonly collapseToggle = () => {
this.setState({
appMenuVisible: !this.state.appMenuVisible
});
}
private readonly createNavLinks = () => {
return (
<DeviceContentNavComponent
{...this.props}
selectedInterface={this.props.interfaceId}
/>);
}
}

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

@ -0,0 +1,33 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { AnyAction } from 'typescript-fsa';
import { DeviceContentComponent, DeviceContentDispatchProps, DeviceContentDataProps } from './deviceContent';
import { StateType } from '../../../shared/redux/state';
import { getIsDevicePnpSelector, getDigitalTwinInterfaceIdsSelector, getDigitalTwinInterfacePropertiesWrapperSelector } from '../selectors';
import { setInterfaceIdAction, getDigitalTwinInterfacePropertiesAction } from '../actions';
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
const mapStateToProps = (state: StateType): DeviceContentDataProps => {
const digitalTwinInterfacesWrapper = getDigitalTwinInterfacePropertiesWrapperSelector(state);
return {
interfaceIds: getDigitalTwinInterfaceIdsSelector(state),
isLoading: digitalTwinInterfacesWrapper &&
digitalTwinInterfacesWrapper.digitalTwinInterfacePropertiesSyncStatus === SynchronizationStatus.working,
isPnPDevice: getIsDevicePnpSelector(state)
};
};
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>): DeviceContentDispatchProps => {
return {
getDigitalTwinInterfaceProperties: (deviceId: string) => dispatch(getDigitalTwinInterfacePropertiesAction.started(deviceId)),
setInterfaceId: (interfaceId: string) => dispatch(setInterfaceIdAction(interfaceId))
};
};
export default connect(mapStateToProps, mapDispatchToProps, undefined, {
pure: false,
})(DeviceContentComponent);

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

@ -0,0 +1,56 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as React from 'react';
import DeviceContentNavComponent from './deviceContentNav';
import { testWithLocalizationContext } from '../../../shared/utils/testHelpers';
const interfaceIds = [
'urn:azureiot:com:DeviceInformation:1'
];
describe('components/devices/deviceContentNav', () => {
it('matches snapshot when there device is not pnp', () => {
const wrapper = testWithLocalizationContext(
<DeviceContentNavComponent
deviceId="test"
interfaceIds={[]}
isLoading={false}
isPnPDevice={false}
selectedInterface=""
setInterfaceId={jest.fn()}
/>);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot when there is no interface selected', () => {
const wrapper = testWithLocalizationContext(
<DeviceContentNavComponent
deviceId="test"
interfaceIds={interfaceIds}
isLoading={false}
isPnPDevice={true}
selectedInterface=""
setInterfaceId={jest.fn()}
/>);
expect(wrapper).toMatchSnapshot();
});
it('redirects snapshot when there is a interface selected', () => {
const wrapper = testWithLocalizationContext(
<DeviceContentNavComponent
deviceId="test"
interfaceIds={interfaceIds}
isLoading={false}
isPnPDevice={true}
selectedInterface="urn:azureiot:com:DeviceInformation:1"
setInterfaceId={jest.fn()}
/>);
expect(wrapper).toMatchSnapshot();
});
});

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

@ -0,0 +1,121 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
import { Nav, INavLink, INavLinkGroup } from 'office-ui-fabric-react/lib/Nav';
import { Label } from 'office-ui-fabric-react';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import '../../../css/_deviceContentNav.scss';
export interface DeviceContentNavDataProps {
deviceId: string;
interfaceIds: string[];
isLoading: boolean;
isPnPDevice: boolean;
selectedInterface: string;
}
export interface DeviceContentNavDispatchProps {
setInterfaceId: (interfaceId: string) => void;
}
interface DeviceContentNavState {
expandedInterfaceMap: Map<string, boolean>;
}
const NAV_LINK_ITEMS_PNP = ['interfaces', 'settings', 'properties', 'commands', 'events'];
const NAV_LINK_ITEMS_NONPNP = ['identity', 'twin', 'events'];
export default class DeviceContentNavComponent extends React.Component<DeviceContentNavDataProps & DeviceContentNavDispatchProps, DeviceContentNavState> {
constructor(props: DeviceContentNavDataProps & DeviceContentNavDispatchProps) {
super(props);
const expandedInterfaceMap = new Map();
if (this.props.selectedInterface) {
expandedInterfaceMap.set(this.props.selectedInterface, true);
}
this.state = {expandedInterfaceMap};
}
public render(): JSX.Element {
if (this.props.isLoading) {
return (
<Shimmer/>
);
}
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
<div className="view-scroll">
{this.createNavLinks(context)}
</div>
)}
</LocalizationContextConsumer>
);
}
private readonly createNavLinks = (context: LocalizationContextInterface) => {
const { deviceId, isPnPDevice, interfaceIds } = this.props;
const nonPnpNavLinks = NAV_LINK_ITEMS_NONPNP.map((nav: string) => ({
key: nav,
name: context.t((ResourceKeys.deviceContent.navBar as any)[nav]), // tslint:disable-line:no-any
url: `#/devices/detail/${nav}/?id=${encodeURIComponent(deviceId)}`
}));
const pnpNavGroupsLinks = isPnPDevice && interfaceIds && interfaceIds.map((id: string) => ({
isExpanded: this.state.expandedInterfaceMap.get(id),
links: NAV_LINK_ITEMS_PNP.map((nav: string): INavLink => ({
name: context.t((ResourceKeys.deviceContent.navBar as any)[nav]), // tslint:disable-line:no-any
onClick: this.onNestedChildLinkClick,
parentId: id,
url: `#/devices/detail/digitalTwins/${nav}/?id=${encodeURIComponent(deviceId)}&interfaceId=${id}`
})),
name: id,
onClick: this.onChildLinkExpand,
url: ''
}));
const groups = [];
groups.push({
links: nonPnpNavLinks,
name: context.t(ResourceKeys.deviceContent.navBar.nonpnp),
});
if (isPnPDevice) {
groups.push({
links: pnpNavGroupsLinks,
name: context.t(ResourceKeys.deviceContent.navBar.pnp),
});
}
return (
<div role="navigation">
<Nav
onRenderGroupHeader={this.onRenderGroupHeader}
groups={groups}
/>
</div>
);
}
private readonly onRenderGroupHeader = (group: INavLinkGroup) => {
return <Label className="nav-label">{group.name}</Label>;
}
private readonly onNestedChildLinkClick = (ev?: React.MouseEvent<HTMLElement>, item?: INavLink) => {
this.props.setInterfaceId(item.parentId);
}
private readonly onChildLinkExpand = (ev?: React.MouseEvent<HTMLElement>, item?: INavLink) => {
const expandedInterfaceMap = new Map(this.state.expandedInterfaceMap);
expandedInterfaceMap.set(item.name, !item.isExpanded);
this.setState({
expandedInterfaceMap
});
}
}

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

@ -0,0 +1,166 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
import InfiniteScroll from 'react-infinite-scroller';
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
import { RouteComponentProps } from 'react-router-dom';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../../localization/resourceKeys';
import { monitorEvents } from '../../../../api/services/devicesService';
import { Message } from '../../../../api/models/messages';
import { parseDateTimeString } from '../../../../api/dataTransforms/transformHelper';
import { CLEAR, CHECKED_CHECKBOX, EMPTY_CHECKBOX } from '../../../../constants/iconNames';
import { getDeviceIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
import '../../../../css/_deviceEvents.scss';
const JSON_SPACES = 2;
const LOADING_LOCK = 50;
export interface DeviceEventsDataProps {
connectionString: string;
}
interface DeviceEventsState {
events: Message[];
hasMore: boolean;
startTime?: Date; // todo: add a datetime picker
loading?: boolean;
showSystemProperties: boolean;
}
export default class DeviceEventsComponent extends React.Component<DeviceEventsDataProps & RouteComponentProps, DeviceEventsState> {
// tslint:disable-next-line:no-any
private timerID: any;
constructor(props: DeviceEventsDataProps & RouteComponentProps) {
super(props);
this.state = {
events: [],
hasMore: true,
showSystemProperties: false
};
}
public componentWillUnmount() {
clearInterval(this.timerID);
}
public render(): JSX.Element {
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
<div className="device-events" key="device-events">
<CommandBar
className="command"
items={[
{
ariaLabel: context.t(ResourceKeys.deviceEvents.command.clearEvents),
disabled: this.state.events.length === 0,
iconProps: {
iconName: CLEAR
},
key: CLEAR,
name: context.t(ResourceKeys.deviceEvents.command.clearEvents),
onClick: this.onClearData
},
{
ariaLabel: context.t(ResourceKeys.deviceEvents.command.showSystemProperties),
iconProps: {
iconName: this.state.showSystemProperties ? CHECKED_CHECKBOX : EMPTY_CHECKBOX
},
key: CHECKED_CHECKBOX,
name: context.t(ResourceKeys.deviceEvents.command.showSystemProperties),
onClick: this.onShowSystemProperties
}
]}
/>
<h3>{context.t(ResourceKeys.deviceEvents.headerText)}</h3>
{this.renderInfiniteScroll(context)}
</div>
)}
</LocalizationContextConsumer>
);
}
private readonly renderInfiniteScroll = (context: LocalizationContextInterface) => {
const { hasMore } = this.state;
return (
<InfiniteScroll
key="scroll"
className="device-events-container scrollable"
pageStart={0}
loadMore={this.fetchData}
hasMore={hasMore}
loader={this.renderLoader(context)}
role="feed"
isReverse={true}
>
{this.renderEvents(context)}
</InfiniteScroll>
);
}
private readonly renderEvents = (context: LocalizationContextInterface) => {
const { events } = this.state;
return events && events.map((event: Message, index) => {
return (
<article key={index} className="device-events-content">
{<h5>{parseDateTimeString(event.enqueuedTime)}:</h5>}
<pre>{JSON.stringify(event, undefined, JSON_SPACES)}</pre>
</article>
);
});
}
private readonly renderLoader = (context: LocalizationContextInterface): JSX.Element => {
return (
<div key="loading" className="events-loader">
<Spinner/>
<h4>{context.t(ResourceKeys.deviceEvents.infiniteScroll.loading)}</h4>
</div>
);
}
private readonly fetchData = () => {
const { loading } = this.state;
if (!loading) {
this.setState({
loading: true,
});
this.timerID = setTimeout(
() => {
monitorEvents({
deviceId: getDeviceIdFromQueryString(this.props),
fetchSystemProperties: this.state.showSystemProperties,
hubConnectionString: this.props.connectionString,
startTime: this.state.startTime
})
.then(results => {
const messages = results && results.reverse().map((message: Message) => message);
this.setState({
events: [...messages, ...this.state.events],
loading: false,
startTime: new Date()
});
});
},
LOADING_LOCK);
}
}
private readonly onClearData = () => {
this.setState({
events: []
});
}
private readonly onShowSystemProperties = () => {
this.setState({
showSystemProperties: !this.state.showSystemProperties
});
}
}

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

@ -0,0 +1,16 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { connect } from 'react-redux';
import { StateType } from '../../../../shared/redux/state';
import DeviceEventsComponent, { DeviceEventsDataProps } from './deviceEvents';
import { getConnectionStringSelector } from '../../../../login/selectors';
const mapStateToProps = (state: StateType): DeviceEventsDataProps => {
return {
connectionString: getConnectionStringSelector(state)
};
};
export default connect(mapStateToProps)(DeviceEventsComponent);

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

@ -0,0 +1,290 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { Validator } from 'jsonschema';
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
import InfiniteScroll from 'react-infinite-scroller';
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
import { RouteComponentProps } from 'react-router-dom';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../../localization/resourceKeys';
import { monitorEvents } from '../../../../api/services/devicesService';
import { Message, MESSAGE_SYSTEM_PROPERTIES, MESSAGE_PROPERTIES } from '../../../../api/models/messages';
import { parseDateTimeString } from '../../../../api/dataTransforms/transformHelper';
import { CLEAR, REFRESH } from '../../../../constants/iconNames';
import { ParsedJsonSchema } from '../../../../api/models/interfaceJsonParserOutput';
import { TelemetryContent } from '../../../../api/models/modelDefinition';
import { getInterfaceIdFromQueryString, getDeviceIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
import InterfaceNotFoundMessageBoxContainer from '../shared/interfaceNotFoundMessageBarContainer';
import '../../../../css/_deviceEvents.scss';
const JSON_SPACES = 2;
const LOADING_LOCK = 50;
const TELEMETRY_SCHEMA_PROP = MESSAGE_PROPERTIES.IOTHUB_MESSAGE_SCHEMA;
const TELEMETRY_INTERFACE_ID_PROP = MESSAGE_SYSTEM_PROPERTIES.IOTHUB_INTERFACE_ID;
export interface DeviceEventsDataProps {
connectionString: string;
isLoading: boolean;
telemetrySchema: TelemetrySchema[];
}
export interface DeviceEventsDispatchProps {
setInterfaceId: (id: string) => void;
refresh: (deviceId: string, interfaceId: string) => void;
}
export interface TelemetrySchema {
parsedSchema: ParsedJsonSchema;
telemetryModelDefinition: TelemetryContent;
}
interface DeviceEventsState {
events: Message[];
hasMore: boolean;
startTime?: Date; // todo: add a datetime picker
loading?: boolean;
}
export default class DeviceEventsPerInterfaceComponent extends React.Component<DeviceEventsDataProps & DeviceEventsDispatchProps & RouteComponentProps, DeviceEventsState> {
// tslint:disable-next-line:no-any
private timerID: any;
constructor(props: DeviceEventsDataProps & DeviceEventsDispatchProps & RouteComponentProps) {
super(props);
this.state = {
events: [],
hasMore: true
};
}
public componentWillUnmount() {
clearInterval(this.timerID);
}
public render(): JSX.Element {
if (this.props.isLoading) {
return (
<Shimmer/>
);
}
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
<div className="device-events" key="device-events">
{ this.props.telemetrySchema && <CommandBar
className="command"
items={[
{
ariaLabel: context.t(ResourceKeys.deviceEvents.command.refresh),
iconProps: {iconName: REFRESH},
key: REFRESH,
name: context.t(ResourceKeys.deviceEvents.command.refresh),
onClick: this.handleRefresh
},
{
ariaLabel: context.t(ResourceKeys.deviceEvents.command.clearEvents),
disabled: this.state.events.length === 0,
iconProps: {iconName: CLEAR},
key: CLEAR,
name: context.t(ResourceKeys.deviceEvents.command.clearEvents),
onClick: this.onClearData
}
]}
/>}
<h3>{context.t(ResourceKeys.deviceEvents.headerText)}</h3>
{this.props.telemetrySchema ?
this.props.telemetrySchema.length !== 0 && this.renderInfiniteScroll(context) :
<InterfaceNotFoundMessageBoxContainer/>}
</div>
)}
</LocalizationContextConsumer>
);
}
public componentDidMount() {
this.props.setInterfaceId(getInterfaceIdFromQueryString(this.props));
}
private readonly renderInfiniteScroll = (context: LocalizationContextInterface) => {
const { hasMore } = this.state;
return (
<InfiniteScroll
key="scroll"
className="device-events-container"
pageStart={0}
loadMore={this.fetchData}
hasMore={hasMore}
loader={this.renderLoader(context)}
role="feed"
isReverse={true}
>
<section role="list" className="list-content scrollable-sm">
{this.renderEvents(context)}
</section>
</InfiniteScroll>
);
}
private readonly renderEvents = (context: LocalizationContextInterface) => {
const { events } = this.state;
return events && events.map((event: Message, index) => {
const matchingSchema = this.props.telemetrySchema.filter(schema => schema.telemetryModelDefinition.name ===
event.properties[TELEMETRY_SCHEMA_PROP]);
const telemetryModelDefinition = matchingSchema && matchingSchema.length !== 0 && matchingSchema[0].telemetryModelDefinition;
const parsedSchema = matchingSchema && matchingSchema.length !== 0 && matchingSchema[0].parsedSchema;
return (
<article className="list-item" role="listitem" key={index}>
<section className="item-oneline">
{this.renderTimestamp(event, context)}
{this.renderEventName(telemetryModelDefinition, context)}
{this.renderEventSchema(telemetryModelDefinition, context)}
{this.renderEventUnit(telemetryModelDefinition, context)}
{this.renderMessageBody(event, context, event.properties[TELEMETRY_SCHEMA_PROP], parsedSchema)}
</section>
</article>
);
});
}
private readonly renderTimestamp = (event: Message, context: LocalizationContextInterface) => {
return(
<Label className="column-timestamp-xs" aria-label={context.t(ResourceKeys.deviceEvents.columns.timestamp)}>
{parseDateTimeString(event.enqueuedTime)}
</Label>
);
}
private readonly renderEventName = (telemetryModelDefinition: TelemetryContent, context: LocalizationContextInterface) => {
return(
<Label className="column-name-xs" aria-label={context.t(ResourceKeys.deviceEvents.columns.displayName)}>
{telemetryModelDefinition ?
`${telemetryModelDefinition.name} (${telemetryModelDefinition.displayName ? telemetryModelDefinition.displayName : '--'}) ` : '--'}
</Label>
);
}
private readonly renderEventSchema = (telemetryModelDefinition: TelemetryContent, context: LocalizationContextInterface) => {
return(
<Label className="column-schema" aria-label={context.t(ResourceKeys.deviceEvents.columns.schema)}>
{telemetryModelDefinition ?
(typeof telemetryModelDefinition.schema === 'string' ?
telemetryModelDefinition.schema :
telemetryModelDefinition.schema['@type']) : '--'}
</Label>
);
}
private readonly renderEventUnit = (telemetryModelDefinition: TelemetryContent, context: LocalizationContextInterface) => {
return(
<Label className="column-unit" aria-label={context.t(ResourceKeys.deviceEvents.columns.unit)}>
{telemetryModelDefinition ?
telemetryModelDefinition.unit || telemetryModelDefinition.displayUnit : '--'}
</Label>
);
}
// tslint:disable-next-line:cyclomatic-complexity
private readonly renderMessageBody = (event: Message, context: LocalizationContextInterface, key: string, schema: ParsedJsonSchema) => {
if (!key) {
return;
}
const validator = new Validator();
if (Object.keys(event.body) && Object.keys(event.body)[0] !== key) {
return(
<div className="column-value-text">
<Label aria-label={context.t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(event.body, undefined, JSON_SPACES)}
<section className="value-validation-error" aria-label={context.t(ResourceKeys.deviceEvents.columns.error.key.label)}>
<span>{context.t(ResourceKeys.deviceEvents.columns.error.key.label)}</span>
<li key={key}>{context.t(ResourceKeys.deviceEvents.columns.error.key.errorContent, {keyName: key})}</li>
</section>
</Label>
</div>
);
}
const result = validator.validate(event.body[key], schema);
return(
<div className="column-value-text">
<Label aria-label={context.t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(event.body, undefined, JSON_SPACES)}
{result && result.errors && result.errors.length !== 0 &&
<section className="value-validation-error" aria-label={context.t(ResourceKeys.deviceEvents.columns.error.value.label)}>
<span>{context.t(ResourceKeys.deviceEvents.columns.error.value.label)}</span>
{result.errors.map((element, index) =>
<li key={index}>{element.message}</li>
)}
</section>
}
</Label>
</div>
);
}
private readonly renderLoader = (context: LocalizationContextInterface): JSX.Element => {
return (
<>
<div key="loading" className="events-loader">
<Spinner/>
<h4>{context.t(ResourceKeys.deviceEvents.infiniteScroll.loading)}</h4>
</div>
<div className="pnp-detail-list">
<div className="list-header">
<span className="column-timestamp-xs">{context.t(ResourceKeys.deviceEvents.columns.timestamp)}</span>
<span className="column-name-xs">{context.t(ResourceKeys.deviceEvents.columns.displayName)}</span>
<span className="column-schema-sm">{context.t(ResourceKeys.deviceEvents.columns.schema)}</span>
<span className="column-unit-sm">{context.t(ResourceKeys.deviceEvents.columns.unit)}</span>
<span className="column-value-sm">{context.t(ResourceKeys.deviceEvents.columns.value)}</span>
</div>
</div>
</>
);
}
private readonly fetchData = () => {
const { loading } = this.state;
if (!loading) {
this.setState({
loading: true,
});
this.timerID = setTimeout(
() => {
monitorEvents({
deviceId: getDeviceIdFromQueryString(this.props),
fetchSystemProperties: true,
hubConnectionString: this.props.connectionString,
startTime: this.state.startTime})
.then((results: Message[]) => {
const messages = results && results
.filter(result => result && result.systemProperties &&
result.systemProperties[TELEMETRY_INTERFACE_ID_PROP] === getInterfaceIdFromQueryString(this.props))
.reverse().map((message: Message) => message);
this.setState({
events: [...messages, ...this.state.events],
loading: false,
startTime: new Date()});
});
},
LOADING_LOCK);
}
}
private readonly onClearData = () => {
this.setState({
events: []
});
}
private readonly handleRefresh = () => {
this.props.refresh(getDeviceIdFromQueryString(this.props), getInterfaceIdFromQueryString(this.props));
}
}

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

@ -0,0 +1,34 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { connect } from 'react-redux';
import { compose, Dispatch } from 'redux';
import { withRouter } from 'react-router-dom';
import { AnyAction } from 'typescript-fsa';
import { StateType } from '../../../../shared/redux/state';
import DeviceEventsPerInterfaceComponent, { DeviceEventsDataProps, DeviceEventsDispatchProps } from './deviceEventsPerInterface';
import { getConnectionStringSelector } from '../../../../login/selectors';
import { getDeviceTelemetrySelector } from './selectors';
import { getDeviceTwinStateSelector } from '../deviceTwin/selectors';
import { getModelDefinitionSyncStatusSelector } from '../../selectors';
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
import { setInterfaceIdAction, getModelDefinitionAction } from '../../actions';
const mapStateToProps = (state: StateType): DeviceEventsDataProps => {
return {
connectionString: getConnectionStringSelector(state),
isLoading: getDeviceTwinStateSelector(state) === SynchronizationStatus.working ||
getModelDefinitionSyncStatusSelector(state) === SynchronizationStatus.working,
telemetrySchema: getDeviceTelemetrySelector(state)
};
};
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>): DeviceEventsDispatchProps => {
return {
refresh: (deviceId: string, interfaceId: string) => dispatch(getModelDefinitionAction.started({digitalTwinId: deviceId, interfaceId})),
setInterfaceId: (id: string) => dispatch(setInterfaceIdAction(id))
};
};
export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps))(DeviceEventsPerInterfaceComponent);

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

@ -0,0 +1,100 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import { Record } from 'immutable';
import { ModelDefinition } from '../../../../api/models/modelDefinition';
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
import { getDeviceTelemetrySelector } from './selectors';
import { getInitialState } from '../../../../api/shared/testHelper';
describe('getDeviceCommandPairs', () => {
it('returns interface commands', () => {
const state = getInitialState();
const interfaceId = 'urn:contoso:com:environmentalsensor:1';
/* tslint:disable */
const digitalTwinInterfaceProperties = {
"interfaces": {
"urn_azureiot_ModelDiscovery_DigitalTwin": {
"name": "urn_azureiot_ModelDiscovery_DigitalTwin",
"properties": {
"modelInformation": {
"reported": {
"value": {
"modelId": "urn:contoso:com:dcm:2",
"interfaces": {
"environmentalsensor": interfaceId,
"urn_azureiot_ModelDiscovery_ModelInformation": "urn:azureiot:ModelDiscovery:ModelInformation:1",
"urn_azureiot_ModelDiscovery_DigitalTwin": "urn:azureiot:ModelDiscovery:DigitalTwin:1"
}
}
}
}
}
}
},
"version": 1
};
const modelDefinition: ModelDefinition = {
"@id": interfaceId,
"@type": "Interface",
"displayName": "Digital Twin",
"contents": [
{
"@type": [
"Telemetry",
"SemanticType/Temperature"
],
"description": "Current temperature on the device",
"displayName": "Temperature",
"name": "temp",
"schema": "double",
"unit": "Units/Temperature/fahrenheit"
}
],
"@context": "http://azureiot.com/v1/contexts/Interface.json"
}
/* tslint:enable */
state.deviceContentState = Record({
deviceIdentity: null,
deviceTwin: null,
digitalTwinInterfaceProperties: {
digitalTwinInterfaceProperties,
digitalTwinInterfacePropertiesSyncStatus: SynchronizationStatus.fetched
},
interfaceIdSelected: interfaceId,
invokeMethodResponse: '',
modelDefinitionWithSource: {
modelDefinition,
modelDefinitionSynchronizationStatus: SynchronizationStatus.fetched,
source: null
}
})();
const telemetrySchemas = [
{
parsedSchema: {
description: 'Temperature/Current temperature on the device ( Unit: Units/Temperature/fahrenheit )',
title: 'temp',
type: 'number'
},
telemetryModelDefinition: {
'@type': [
'Telemetry',
'SemanticType/Temperature'
],
'description': 'Current temperature on the device',
'displayName': 'Temperature',
'name': 'temp',
'schema': 'double',
'unit': 'Units/Temperature/fahrenheit'
}
}
];
expect(getDeviceTelemetrySelector(state))
.toEqual(telemetrySchemas);
});
});

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

@ -0,0 +1,29 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { StateInterface } from './../../../../shared/redux/state';
import { ContentType, TelemetryContent } from '../../../../api/models/modelDefinition';
import { parseInterfaceTelemetryToJsonSchema } from './../../../../shared/utils/jsonSchemaAdaptor';
import { getModelDefinitionSelector } from '../../selectors';
import { TelemetrySchema } from './deviceEventsPerInterface';
export const getDeviceTelemetrySelector = (state: StateInterface): TelemetrySchema[] => {
const modelDefinition = getModelDefinitionSelector(state);
const telemetryContents = modelDefinition && modelDefinition.contents && modelDefinition.contents.filter((item: TelemetryContent) => filterTelemetry(item)) as TelemetryContent[];
return telemetryContents && telemetryContents.map(telemetry => ({
parsedSchema: parseInterfaceTelemetryToJsonSchema(telemetry),
telemetryModelDefinition: telemetry
}));
};
const filterTelemetry = (content: TelemetryContent) => {
if (typeof content['@type'] === 'string') {
return content['@type'].toLowerCase() === ContentType.Telemetry;
}
else {
return content['@type'].some((entry: string) => entry.toLowerCase() === ContentType.Telemetry);
}
};
export default getDeviceTelemetrySelector;

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

@ -0,0 +1,269 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import React from 'react';
import { Label, Shimmer, Toggle, Overlay } from 'office-ui-fabric-react';
import { RouteComponentProps } from 'react-router-dom';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../../localization/resourceKeys';
import { DeviceIdentity } from '../../../../api/models/deviceIdentity';
import { getDeviceAuthenticationType, generateConnectionString } from './deviceIdentityHelper';
import DeviceIdentityCommandBar from './deviceIdentityCommandBar';
import { DeviceAuthenticationType } from '../../../../api/models/deviceAuthenticationType';
import { DeviceStatus } from '../../../../api/models/deviceStatus';
import { generateKey } from '../../../../shared/utils/utils';
import { DeviceIdentityWrapper } from '../../../../api/models/deviceIdentityWrapper';
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
import { getDeviceIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
import { CopyableMaskField } from '../../../../shared/components/copyableMaskField';
import '../../../../css/_deviceDetail.scss';
export interface DeviceIdentityDispatchProps {
updateDeviceIdentity: (deviceIdentity: DeviceIdentity) => void;
getDeviceIdentity: (deviceId: string) => void;
}
export interface DeviceIdentityDataProps {
identityWrapper: DeviceIdentityWrapper;
connectionString: string;
}
export interface DeviceIdentityState {
identity: DeviceIdentity;
isDirty: boolean;
requestMade: boolean;
}
export default class DeviceIdentityInformation
extends React.Component<DeviceIdentityDataProps & DeviceIdentityDispatchProps & RouteComponentProps, DeviceIdentityState> {
constructor(props: DeviceIdentityDataProps & DeviceIdentityDispatchProps & RouteComponentProps) {
super(props);
this.state = {
identity: this.props.identityWrapper && this.props.identityWrapper.deviceIdentity,
isDirty: false,
requestMade: false
};
}
public render(): JSX.Element {
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
<>
{this.showCommandBar()}
{this.props.identityWrapper && this.renderInformationSection(context)}
</>
)}
</LocalizationContextConsumer>
);
}
public componentDidMount() {
this.props.getDeviceIdentity(getDeviceIdFromQueryString(this.props));
}
// tslint:disable-next-line:cyclomatic-complexity
public static getDerivedStateFromProps(props: DeviceIdentityDataProps & DeviceIdentityDispatchProps & RouteComponentProps, state: DeviceIdentityState): Partial<DeviceIdentityState> | null {
if (props.identityWrapper) {
if (state.isDirty && state.requestMade && props.identityWrapper.deviceIdentitySynchronizationStatus === SynchronizationStatus.upserted) {
return {
identity: props.identityWrapper.deviceIdentity,
isDirty: false,
requestMade: false
};
}
else if (!state.isDirty) {
return {
identity: props.identityWrapper.deviceIdentity
};
}
}
return null;
}
private readonly showCommandBar = () => {
let onSwapKeys;
let onGeneratePrimaryKey;
let onGenerateSecondaryKey;
if (this.props.identityWrapper &&
this.props.identityWrapper.deviceIdentity &&
this.props.identityWrapper.deviceIdentity.authentication.type === DeviceAuthenticationType.SymmetricKey) {
onSwapKeys = this.swapKeys;
onGeneratePrimaryKey = this.generatePrimaryKey;
onGenerateSecondaryKey = this.generateSecondaryKey;
}
return (
<DeviceIdentityCommandBar
disableSave={!this.state.isDirty}
handleSave={this.handleSave}
onRegeneratePrimaryKey={onGeneratePrimaryKey}
onRegenerateSecondaryKey={onGenerateSecondaryKey}
onSwapKeys={onSwapKeys}
/>
);
}
private readonly handleSave = () => {
this.props.updateDeviceIdentity(this.state.identity);
this.setState({
requestMade: true
});
}
private readonly renderInformationSection = (context: LocalizationContextInterface) => {
return (
<div className="device-detail">
{ this.props.identityWrapper.deviceIdentitySynchronizationStatus === SynchronizationStatus.working ?
<Shimmer/> :
<>
<CopyableMaskField
ariaLabel={context.t(ResourceKeys.deviceIdentity.deviceID)}
label={context.t(ResourceKeys.deviceIdentity.deviceID)}
value={this.state.identity && this.state.identity.deviceId}
allowMask={false}
t={context.t}
readOnly={true}
/>
{this.renderDeviceAuthProperties(context)}
<br/>
{this.renderHubRelatedInformation(context)}
</>
}
{this.props.identityWrapper.deviceIdentitySynchronizationStatus === SynchronizationStatus.updating && <Overlay/>}
</div>
);
}
private readonly renderDeviceAuthProperties = (context: LocalizationContextInterface) => {
const { connectionString } = this.props;
const { identity } = this.state;
const authType = getDeviceAuthenticationType(identity);
switch (authType) {
case DeviceAuthenticationType.SelfSigned:
return (<Label>{context.t(ResourceKeys.deviceIdentity.authenticationType.selfSigned.text)}</Label>);
case DeviceAuthenticationType.CACertificate:
return (<Label>{context.t(ResourceKeys.deviceIdentity.authenticationType.ca.text)}</Label>);
case DeviceAuthenticationType.SymmetricKey:
return (
<>
<CopyableMaskField
ariaLabel={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.primaryKey)}
label={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.primaryKey)}
value={this.state.identity.authentication.symmetricKey.primaryKey}
allowMask={true}
t={context.t}
readOnly={false}
onTextChange={this.changePrimaryKey}
/>
<CopyableMaskField
ariaLabel={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.secondaryKey)}
label={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.secondaryKey)}
value={this.state.identity.authentication.symmetricKey.secondaryKey}
allowMask={true}
t={context.t}
readOnly={false}
onTextChange={this.changeSecondaryKey}
/>
<CopyableMaskField
ariaLabel={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.primaryConnectionString)}
label={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.primaryConnectionString)}
value={generateConnectionString(connectionString, identity.deviceId, identity.authentication.symmetricKey.primaryKey)}
allowMask={true}
t={context.t}
readOnly={true}
/>
<CopyableMaskField
ariaLabel={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.secondaryConnectionString)}
label={context.t(ResourceKeys.deviceIdentity.authenticationType.symmetricKey.secondaryConnectionString)}
value={generateConnectionString(connectionString, identity.deviceId, identity.authentication.symmetricKey.secondaryKey)}
allowMask={true}
t={context.t}
readOnly={true}
/>
</>
);
default:
return (<></>);
}
}
private readonly renderHubRelatedInformation = (context: LocalizationContextInterface) => {
return (
<Toggle
checked={this.state.identity && this.state.identity.status === DeviceStatus.Enabled}
label={context.t(ResourceKeys.deviceIdentity.hubConnectivity.label)}
onText={context.t(ResourceKeys.deviceIdentity.hubConnectivity.enable)}
offText={context.t(ResourceKeys.deviceIdentity.hubConnectivity.disable)}
onChange={this.changeToggle}
/>
);
}
private readonly changePrimaryKey = (value: string) => {
const identityDeepCopy: DeviceIdentity = JSON.parse(JSON.stringify(this.state.identity));
identityDeepCopy.authentication.symmetricKey.primaryKey = value;
this.setState({
identity: identityDeepCopy,
isDirty: true
});
}
private readonly changeSecondaryKey = (value: string) => {
const identityDeepCopy: DeviceIdentity = JSON.parse(JSON.stringify(this.state.identity));
identityDeepCopy.authentication.symmetricKey.secondaryKey = value;
this.setState({
identity: identityDeepCopy,
isDirty: true
});
}
private readonly generatePrimaryKey = () => {
const identityDeepCopy: DeviceIdentity = JSON.parse(JSON.stringify(this.state.identity));
identityDeepCopy.authentication.symmetricKey.primaryKey = generateKey();
this.setState({
identity: identityDeepCopy,
isDirty: true
});
}
private readonly generateSecondaryKey = () => {
const identityDeepCopy: DeviceIdentity = JSON.parse(JSON.stringify(this.state.identity));
identityDeepCopy.authentication.symmetricKey.secondaryKey = generateKey();
this.setState({
identity: identityDeepCopy,
isDirty: true
});
}
private readonly swapKeys = () => {
const identityDeepCopy: DeviceIdentity = JSON.parse(JSON.stringify(this.state.identity));
const originalPrimaryKey = identityDeepCopy.authentication.symmetricKey.primaryKey;
const originalSecondaryKey = identityDeepCopy.authentication.symmetricKey.secondaryKey;
identityDeepCopy.authentication.symmetricKey.primaryKey = originalSecondaryKey;
identityDeepCopy.authentication.symmetricKey.secondaryKey = originalPrimaryKey;
this.setState({
identity: identityDeepCopy,
isDirty: true
});
}
private readonly changeToggle = (event: React.MouseEvent<HTMLElement>, checked?: boolean) => {
const identity = {
...this.state.identity,
status: checked ? DeviceStatus.Enabled.toString() : DeviceStatus.Disabled.toString()};
this.setState({
identity,
isDirty: true
});
}
}

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

@ -0,0 +1,101 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { CommandBar } from 'office-ui-fabric-react';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../../localization/resourceKeys';
import { SAVE } from '../../../../constants/iconNames';
export interface DeviceIdentityCommandBarDataProps {
disableSave?: boolean;
}
export interface DeviceIdentityCommandBarActionProps {
handleSave: () => void;
onRegeneratePrimaryKey?(): void;
onRegenerateSecondaryKey?(): void;
onSwapKeys?(): void;
}
export default class DeviceIdentityCommandBar extends React.Component<DeviceIdentityCommandBarDataProps & DeviceIdentityCommandBarActionProps> {
constructor(props: DeviceIdentityCommandBarDataProps & DeviceIdentityCommandBarActionProps) {
super(props);
}
public render() {
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
this.showCommandBar(context)
)}
</LocalizationContextConsumer>
);
}
private readonly showCommandBar = (context: LocalizationContextInterface) => {
const { onRegeneratePrimaryKey, onRegenerateSecondaryKey, onSwapKeys } = this.props;
const allowKeyManagement: boolean = !!onRegeneratePrimaryKey || !!onRegenerateSecondaryKey || !!onSwapKeys;
const items = [
{
ariaLabel: context.t(ResourceKeys.deviceLists.commands.add),
disabled: this.props.disableSave,
iconProps: {
iconName: SAVE
},
key: SAVE,
name: context.t(ResourceKeys.deviceIdentity.commands.save),
onClick: this.props.handleSave
},
{
ariaLabel: context.t(ResourceKeys.deviceIdentity.commands.manageKeys.ariaLabel),
disabled: !allowKeyManagement,
iconProps: {
iconName: 'Permissions'
},
key: 'manageKeys',
name: context.t(ResourceKeys.deviceIdentity.commands.manageKeys.label),
subMenuProps: {
items: [
{
ariaLabel: context.t(ResourceKeys.deviceIdentity.commands.regeneratePrimary.ariaLabel),
disabled: !onRegeneratePrimaryKey,
iconProps: {
iconName: 'AzureKeyVault'
},
key: 'regeneratePrimary',
name: context.t(ResourceKeys.deviceIdentity.commands.regeneratePrimary.label),
onClick: onRegeneratePrimaryKey
},
{
ariaLabel: context.t(ResourceKeys.deviceIdentity.commands.regenerateSecondary.ariaLabel),
disabled: !onRegenerateSecondaryKey,
iconProps: {
iconName: 'AzureKeyVault'
},
key: 'regenerateSecondary',
name: context.t(ResourceKeys.deviceIdentity.commands.regenerateSecondary.label),
onClick: onRegenerateSecondaryKey
},
{
ariaLabel: context.t(ResourceKeys.deviceIdentity.commands.swapKeys.ariaLabel),
disabled: !onSwapKeys,
iconProps: {
iconName: 'SwitcherStartEnd'
},
key: 'swapKeys',
name: context.t(ResourceKeys.deviceIdentity.commands.swapKeys.label),
onClick: onSwapKeys
}
]
}
},
];
return (
<CommandBar items={items} />
);
}
}

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

@ -0,0 +1,29 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { compose, Dispatch } from 'redux';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { StateType } from '../../../../shared/redux/state';
import DeviceIdentityInformation, { DeviceIdentityDataProps, DeviceIdentityDispatchProps } from './deviceIdentity';
import { getDeviceIdentityWrapperSelector } from '../../selectors';
import { getConnectionStringSelector } from '../../../../login/selectors';
import { DeviceIdentity } from '../../../../api/models/deviceIdentity';
import { updateDeviceIdentityAction, getDeviceIdentityAction } from '../../actions';
const mapStateToProps = (state: StateType): DeviceIdentityDataProps => {
return {
connectionString: getConnectionStringSelector(state),
identityWrapper: getDeviceIdentityWrapperSelector(state)
};
};
const mapDispatchToProps = (dispatch: Dispatch): DeviceIdentityDispatchProps => {
return {
getDeviceIdentity: (deviceId: string) => dispatch(getDeviceIdentityAction.started(deviceId)),
updateDeviceIdentity: (deviceIdentity: DeviceIdentity) => dispatch(updateDeviceIdentityAction.started(deviceIdentity)),
};
};
export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps))(DeviceIdentityInformation);

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