Initial code migration
This commit is contained in:
Родитель
3666bd1cc6
Коммит
397dc8428e
|
@ -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
|
40
README.md
40
README.md
|
@ -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
|
||||
|
|
Двоичный файл не отображается.
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 98 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.1 KiB |
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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;
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
module.exports = {};
|
|
@ -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'`);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче