зеркало из
1
0
Форкнуть 0
* try out websocket

* try out websocket

* websocket for events subscribe

* remove duplicate messages
This commit is contained in:
YingXue 2022-11-16 10:33:51 -08:00 коммит произвёл GitHub
Родитель 9d5fe5e490
Коммит 3f052f3f7f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
51 изменённых файлов: 1437 добавлений и 2380 удалений

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

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

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

@ -69,8 +69,8 @@
},
"homepage": "https://github.com/Azure/azure-iot-explorer#readme",
"dependencies": {
"@azure/core-amqp": "^3.2.0",
"@azure/event-hubs": "1.0.7",
"@azure/core-amqp": "3.2.0",
"@azure/event-hubs": "5.6.0",
"@azure/msal-node": "1.3.0",
"@fluentui/react": "8.20.2",
"@microsoft/applicationinsights-web": "2.8.4",
@ -87,13 +87,12 @@
"immutable": "4.0.0-rc.12",
"jsonschema": "1.2.4",
"msal": "1.2.0",
"protobufjs": "^7.0.0",
"protobufjs": "7.0.0",
"react": "16.14.0",
"react-collapsible": "2.3.2",
"react-dom": "16.13.1",
"react-i18next": "11.11.1",
"react-jsonschema-form": "1.7.0",
"react-qr-code": "^2.0.1",
"react-router-dom": "5.2.0",
"react-smooth-dnd": "0.11.0",
"react-toastify": "4.4.0",
@ -102,7 +101,8 @@
"semver": "6.3.0",
"typescript-fsa": "3.0.0-beta-2",
"typescript-fsa-reducers": "1.0.0",
"uuid": "3.3.3"
"uuid": "3.3.3",
"ws": "5.2.0"
},
"devDependencies": {
"@redux-saga/testing-utils": "1.1.3",
@ -118,7 +118,7 @@
"@types/react": "16.9.35",
"@types/react-dom": "16.9.8",
"@types/react-jsonschema-form": "1.0.10",
"@types/react-router-dom": "^5.3.3",
"@types/react-router-dom": "5.3.3",
"@types/request": "2.48.1",
"@types/semver": "6.0.2",
"@types/uuid": "3.4.5",

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

@ -11,8 +11,6 @@ export const MESSAGE_CHANNELS = {
AUTHENTICATION_LOGIN: 'authentication_login',
AUTHENTICATION_LOGOUT: 'authentication_logout',
DIRECTORY_GET_DIRECTORIES: 'directory_getDirectories',
EVENTHUB_START_MONITORING: 'eventhub_startMonitoring',
EVENTHUB_STOP_MONITORING: 'eventhub_stopMonitoring',
MODEL_REPOSITORY_GET_DEFINITION: 'model_definition',
SETTING_HIGH_CONTRAST: 'setting_highContrast',
};
@ -21,7 +19,6 @@ export const API_INTERFACES = {
AUTHENTICATION: 'api_authentication',
DEVICE: 'api_device',
DIRECTORY: 'api_directory',
EVENTHUB: 'api_eventhub',
MODEL_DEFINITION: 'api_modelDefinition',
SETTINGS: 'api_settings'
};

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

@ -6,12 +6,10 @@ import { contextBridge } from 'electron';
import { generateSettingsInterface } from './factories/settingsInterfaceFactory';
import { generateDirectoryInterface } from './factories/directoryInterfaceFactory';
import { generateModelRepositoryInterface } from './factories/modelRepositoryInterfaceFactory';
import { generateEventHubInterface } from './factories/eventHubInterfaceFactory';
import { generateAuthenticationInterface } from './factories/authenticationInterfaceFactory';
import { API_INTERFACES } from './constants';
contextBridge.exposeInMainWorld(API_INTERFACES.DIRECTORY, generateDirectoryInterface());
contextBridge.exposeInMainWorld(API_INTERFACES.EVENTHUB, generateEventHubInterface());
contextBridge.exposeInMainWorld(API_INTERFACES.MODEL_DEFINITION, generateModelRepositoryInterface());
contextBridge.exposeInMainWorld(API_INTERFACES.SETTINGS, generateSettingsInterface());
contextBridge.exposeInMainWorld(API_INTERFACES.AUTHENTICATION, generateAuthenticationInterface());

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

@ -10,7 +10,6 @@ import { PLATFORMS, MESSAGE_CHANNELS } from './constants';
import { onSettingsHighContrast } from './handlers/settingsHandler';
import { onGetInterfaceDefinition } from './handlers/modelRepositoryHandler';
import { onGetDirectories } from './handlers/directoryHandler';
import { onStartMonitoring, onStopMonitoring } from './handlers/eventHubHandler';
import { formatError } from './utils/errorHelper';
import { AuthProvider } from './utils/authProvider';
import '../dist/server/serverElectron';
@ -33,8 +32,6 @@ class Main {
Main.registerHandler(MESSAGE_CHANNELS.SETTING_HIGH_CONTRAST, onSettingsHighContrast);
Main.registerHandler(MESSAGE_CHANNELS.MODEL_REPOSITORY_GET_DEFINITION, onGetInterfaceDefinition);
Main.registerHandler(MESSAGE_CHANNELS.DIRECTORY_GET_DIRECTORIES, onGetDirectories);
Main.registerHandler(MESSAGE_CHANNELS.EVENTHUB_START_MONITORING, onStartMonitoring);
Main.registerHandler(MESSAGE_CHANNELS.EVENTHUB_STOP_MONITORING, onStopMonitoring);
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_LOGIN, Main.onLogin);
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_LOGOUT, Main.onLogout);
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_GET_PROFILE_TOKEN, Main.onGetProfileToken);

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

@ -1,18 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { MESSAGE_CHANNELS } from '../constants';
import { EventHubInterface, StartEventHubMonitoringParameters, Message } from '../interfaces/eventHubInterface';
import { invokeInMainWorld } from '../utils/invokeHelper';
export const generateEventHubInterface = (): EventHubInterface => {
return {
startEventHubMonitoring: async (params: StartEventHubMonitoringParameters): Promise<Message[]> => {
return invokeInMainWorld<Message[]>(MESSAGE_CHANNELS.EVENTHUB_START_MONITORING, params);
},
stopEventHubMonitoring: async (): Promise<void> => {
return invokeInMainWorld<void>(MESSAGE_CHANNELS.EVENTHUB_STOP_MONITORING);
}
};
};

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

@ -1,46 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { Message as CloudToDeviceMessage } from 'azure-iot-common';
import { MessageProperty } from '../interfaces/deviceInterface';
// tslint:disable-next-line:cyclomatic-complexity
export const addPropertiesToCloudToDeviceMessage = (message: CloudToDeviceMessage, properties: MessageProperty[]) => {
if (!properties || properties.length === 0) {
return;
}
for (const property of properties) {
if (property.isSystemProperty) {
switch (property.key) {
case 'ack':
message.ack = property.value;
break;
case 'contentType':
message.contentType = property.value as any; // tslint:disable-line:no-any
break;
case 'correlationId':
message.correlationId = property.value;
break;
case 'contentEncoding':
message.contentEncoding = property.value as any; // tslint:disable-line:no-any
break;
case 'expiryTimeUtc':
message.expiryTimeUtc = parseInt(property.value); // tslint:disable-line:radix
break;
case 'messageId':
message.messageId = property.value;
break;
case 'lockToken':
message.lockToken = property.value;
break;
default:
message.properties.add(property.key, property.value);
break;
}
}
else {
message.properties.add(property.key, property.value);
}
}
};

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

@ -1,162 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { IpcMainInvokeEvent } from 'electron';
import { EventHubClient, EventPosition, ReceiveHandler } from '@azure/event-hubs';
import { Message, StartEventHubMonitoringParameters } from '../interfaces/eventHubInterface';
import { convertIotHubToEventHubsConnectionString } from '../utils/connStringHelper';
let client: EventHubClient = null;
let messages: Message[] = [];
let receivers: ReceiveHandler[] = [];
let connectionString: string = ''; // would equal `${hubConnectionString}` or `${customEventHubConnectionString}/${customEventHubName}`
let deviceId: string = '';
let moduleId: string = '';
const IOTHUB_CONNECTION_DEVICE_ID = 'iothub-connection-device-id';
const IOTHUB_CONNECTION_MODULE_ID = 'iothub-connection-module-id';
export const onStartMonitoring = async (event: IpcMainInvokeEvent, params: StartEventHubMonitoringParameters): Promise<Message[]>=> {
return eventHubProvider(params).then(result => {
return result;
});
}
export const onStopMonitoring = async (): Promise<void> => {
try {
return stopClient();
} catch (error) {
// swallow the error as we set client to null anyways
}
}
const eventHubProvider = async (params: StartEventHubMonitoringParameters) => {
await initializeEventHubClient(params);
updateEntityIdIfNecessary(params);
return listeningToMessages(client, params);
};
const initializeEventHubClient = async (params: StartEventHubMonitoringParameters) => {
if (needToCreateNewEventHubClient(params))
{
// hub has changed, reinitialize client, receivers and mesages
if (params.customEventHubConnectionString) {
client = await EventHubClient.createFromConnectionString(params.customEventHubConnectionString, params.customEventHubName);
}
else {
try {
client = await EventHubClient.createFromConnectionString(await convertIotHubToEventHubsConnectionString(params.hubConnectionString));
}
catch {
client = await EventHubClient.createFromIotHubConnectionString(params.hubConnectionString);
}
}
connectionString = params.customEventHubConnectionString ?
`${params.customEventHubConnectionString}/${params.customEventHubName}` :
params.hubConnectionString;
receivers = [];
messages = [];
}
};
const listeningToMessages = async (eventHubClient: EventHubClient, params: StartEventHubMonitoringParameters) => {
if (params.startListeners || !receivers) {
const partitionIds = await client.getPartitionIds();
const hubInfo = await client.getHubRuntimeInformation();
const startTime = params.startTime ? Date.parse(params.startTime) : Date.now();
partitionIds && partitionIds.forEach(async (partitionId: string) => {
const receiveOptions = {
consumerGroup: params.consumerGroup,
enableReceiverRuntimeMetric: true,
eventPosition: EventPosition.fromEnqueuedTime(startTime),
name: `${hubInfo.path}_${partitionId}`,
};
const receiver = eventHubClient.receive(
partitionId,
onMessageReceived,
(err: object) => {},
receiveOptions);
receivers.push(receiver);
});
}
return handleMessages();
};
const handleMessages = () => {
let results: Message[] = [];
messages.forEach(message => {
if (!results.some(result => result.systemProperties?.['x-opt-sequence-number'] === message.systemProperties?.['x-opt-sequence-number'])) {
// if user click stop/start too refrequently, it's possible duplicate receivers are created before the cleanup happens as it's async
// remove duplicate messages before proper cleanup is finished
results.push(message);
}
})
messages = []; // empty the array everytime the result is returned
return results;
}
const stopClient = async () => {
return stopReceivers().then(() => {
return client && client.close().catch(error => {
console.log(`client cleanup error: ${error}`); // swallow the error as we will cleanup anyways
});
}).finally (() => {
client = null;
receivers = [];
});
};
const stopReceivers = async () => {
return Promise.all(
receivers.map(receiver => {
if (receiver && (receiver.isReceiverOpen === undefined || receiver.isReceiverOpen)) {
return stopReceiver(receiver);
} else {
return null;
}
})
);
};
const stopReceiver = async (receiver: ReceiveHandler) => {
receiver.stop().catch((err: object) => {
throw new Error(`receivers cleanup error: ${err}`);
});
}
const needToCreateNewEventHubClient = (parmas: StartEventHubMonitoringParameters): boolean => {
return !client ||
parmas.hubConnectionString && parmas.hubConnectionString !== connectionString ||
parmas.customEventHubConnectionString && `${parmas.customEventHubConnectionString}/${parmas.customEventHubName}` !== connectionString;
}
const updateEntityIdIfNecessary = (parmas: StartEventHubMonitoringParameters) => {
if( !deviceId || parmas.deviceId !== deviceId) {
deviceId = parmas.deviceId;
messages = [];
}
if (parmas.moduleId !== moduleId) {
moduleId = parmas.moduleId;
messages = [];
}
}
const onMessageReceived = async (eventData: any) => {
if (eventData && eventData.annotations && eventData.annotations[IOTHUB_CONNECTION_DEVICE_ID] === deviceId) {
if (!moduleId || eventData?.annotations?.[IOTHUB_CONNECTION_MODULE_ID] === moduleId) {
const message: Message = {
body: eventData.body,
enqueuedTime: eventData.enqueuedTimeUtc.toString(),
properties: eventData.applicationProperties
};
message.systemProperties = eventData.annotations;
messages.push(message);
}
}
};

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

@ -7,9 +7,6 @@ export interface StartEventHubMonitoringParameters {
moduleId: string;
consumerGroup: string;
startTime: string;
startListeners: boolean;
customEventHubName?: string;
customEventHubConnectionString?: string;
hubConnectionString?: string;
}
@ -20,8 +17,3 @@ export interface Message {
properties?: any; // tslint:disable-line:no-any
systemProperties?: {[key: string]: string};
}
export interface EventHubInterface {
startEventHubMonitoring(params: StartEventHubMonitoringParameters): Promise<Message[]>;
stopEventHubMonitoring(): Promise<void>;
}

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

@ -0,0 +1,26 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { Message, StartEventHubMonitoringParameters } from '../../../../public/interfaces/eventHubInterface';
import { CONTROLLER_API_ENDPOINT, DataPlaneStatusCode, EVENTHUB, MONITOR, STOP } from '../../constants/apiConstants';
import { request } from '../services/dataplaneServiceHelper';
const EVENTHUB_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${EVENTHUB}`;
export const EVENTHUB_MONITOR_ENDPOINT = `${EVENTHUB_CONTROLLER_ENDPOINT}${MONITOR}`;
export const EVENTHUB_STOP_ENDPOINT = `${EVENTHUB_CONTROLLER_ENDPOINT}${STOP}`;
export const startEventHubMonitoring = async (params: StartEventHubMonitoringParameters): Promise<Message[]> => {
const response = await request(EVENTHUB_MONITOR_ENDPOINT, params);
if (response.status === DataPlaneStatusCode.SuccessLowerBound) {
return await response.json() as Message[];
}
else {
const error = await response.json();
throw new Error(error && error.name);
}
};
export const stopEventHubMonitoring = async (): Promise<void> => {
await request(EVENTHUB_STOP_ENDPOINT, {});
};

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

@ -1,17 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { DeviceInterface, SendMessageToDeviceParameters } from '../../../../public/interfaces/deviceInterface';
import { CONTROLLER_API_ENDPOINT, CLOUD_TO_DEVICE, EVENTHUB, MONITOR, STOP } from '../../constants/apiConstants';
import { request } from '../services/dataplaneServiceHelper';
const EVENTHUB_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${EVENTHUB}`;
export const EVENTHUB_MONITOR_ENDPOINT = `${EVENTHUB_CONTROLLER_ENDPOINT}${MONITOR}`;
export const EVENTHUB_STOP_ENDPOINT = `${EVENTHUB_CONTROLLER_ENDPOINT}${STOP}`;
export class DevicesServiceHandler implements DeviceInterface {
public sendMessageToDevice = async (params: SendMessageToDeviceParameters): Promise<void> => {
await request(`${CONTROLLER_API_ENDPOINT}${CLOUD_TO_DEVICE}`, params);
}
}

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

@ -1,28 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { EventHubInterface, Message, StartEventHubMonitoringParameters } from '../../../../public/interfaces/eventHubInterface';
import { CONTROLLER_API_ENDPOINT, DataPlaneStatusCode, EVENTHUB, MONITOR, STOP } from '../../constants/apiConstants';
import { request } from '../services/dataplaneServiceHelper';
const EVENTHUB_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${EVENTHUB}`;
export const EVENTHUB_MONITOR_ENDPOINT = `${EVENTHUB_CONTROLLER_ENDPOINT}${MONITOR}`;
export const EVENTHUB_STOP_ENDPOINT = `${EVENTHUB_CONTROLLER_ENDPOINT}${STOP}`;
export class EventHubServiceHandler implements EventHubInterface {
public startEventHubMonitoring = async (params: StartEventHubMonitoringParameters): Promise<Message[]> => {
const response = await request(EVENTHUB_MONITOR_ENDPOINT, params);
if (response.status === DataPlaneStatusCode.SuccessLowerBound) {
return await response.json() as Message[];
}
else {
const error = await response.json();
throw new Error(error && error.name);
}
}
public stopEventHubMonitoring = async (): Promise<void> => {
await request(EVENTHUB_STOP_ENDPOINT, {});
}
}

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

@ -29,9 +29,6 @@ export interface MonitorEventsParameters {
deviceId: string;
moduleId: string;
consumerGroup: string;
startListeners: boolean;
customEventHubName?: string;
customEventHubConnectionString?: string;
hubConnectionString?: string;
startTime?: Date;

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

@ -10,7 +10,7 @@ import { Twin } from '../models/device';
import { DeviceIdentity } from './../models/deviceIdentity';
import { buildQueryString, getConnectionInfoFromConnectionString } from '../shared/utils';
import { MonitorEventsParameters } from '../parameters/deviceParameters';
import * as interfaceUtils from '../shared/interfaceUtils';
import { EVENTHUB_MONITOR_ENDPOINT, EVENTHUB_STOP_ENDPOINT } from '../handlers/eventHubServiceHandler';
const deviceId = 'deviceId';
const connectionString = 'HostName=test-string.azure-devices.net;SharedAccessKeyName=owner;SharedAccessKey=fakeKey=';
@ -59,6 +59,9 @@ const mockDataPlaneConnectionHelper = () => {
};
describe('deviceTwinService', () => {
afterEach(() => {
jest.clearAllMocks();
});
context('fetchDeviceTwin', () => {
it ('returns if deviceId is not specified', () => {
@ -391,6 +394,7 @@ describe('deviceTwinService', () => {
const dataPlaneRequest: DataplaneService.DataPlaneRequest = {
apiVersion: HUB_DATA_PLANE_API_VERSION,
body: JSON.stringify(deviceIdentity),
headers: {'If-Match': `"null"`},
hostName: connectionInformation.connectionInfo.hostName,
httpMethod: HTTP_OPERATION_TYPES.Put,
path: `devices/${deviceId}`,
@ -631,45 +635,76 @@ describe('deviceTwinService', () => {
context('monitorEvents', () => {
const parameters: MonitorEventsParameters = {
consumerGroup: '$Default',
customEventHubConnectionString: undefined,
customEventHubConnectionString: 'customConnectionString',
deviceId,
hubConnectionString: undefined,
startListeners: true,
moduleId: undefined,
startTime: undefined
moduleId: ''
};
it('calls startEventHubMonitoring with expected parameters', async () => {
jest.spyOn(DataplaneService, 'dataPlaneConnectionHelper').mockResolvedValue({
connectionInfo: getConnectionInfoFromConnectionString(connectionString), connectionString, sasToken});
const startEventHubMonitoring = jest.fn();
jest.spyOn(interfaceUtils, 'getEventHubInterface').mockReturnValue({
startEventHubMonitoring,
stopEventHubMonitoring: jest.fn()
});
it('calls fetch with specified parameters', async () => {
// tslint:disable
const response = {
json: () => {
return {
headers:{}
}
},
status: 200
} as any;
// tslint:enable
jest.spyOn(window, 'fetch').mockResolvedValue(response);
await DevicesService.monitorEvents(parameters);
expect(startEventHubMonitoring).toBeCalledWith({
...parameters,
hubConnectionString: connectionString,
startTime: parameters.startTime && parameters.startTime.toISOString()
expect(fetch).toBeCalledWith(EVENTHUB_MONITOR_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',
});
});
it('throws Error when promise rejects', async () => {
window.fetch = jest.fn().mockRejectedValueOnce(new Error());
await expect(DevicesService.monitorEvents(parameters)).rejects.toThrow(new Error());
});
});
context('stopMonitoringEvents', () => {
it('calls stopEventHubMonitoring', async () => {
const stopEventHubMonitoring = jest.fn();
jest.spyOn(interfaceUtils, 'getEventHubInterface').mockReturnValue({
startEventHubMonitoring: jest.fn(),
stopEventHubMonitoring
});
it('calls fetch with specified parameters', async () => {
// tslint:disable
const response = {
json: () => {
return {
headers:{}
}
},
status: 200
} as any;
// tslint:enable
jest.spyOn(window, 'fetch').mockResolvedValue(response);
await DevicesService.stopMonitoringEvents();
expect(stopEventHubMonitoring).toBeCalled();
expect(fetch).toBeCalledWith(EVENTHUB_STOP_ENDPOINT, {
body: JSON.stringify({}),
cache: 'no-cache',
credentials: 'include',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
method: HTTP_OPERATION_TYPES.Post,
mode: 'cors',
});
});
it('throws Error when promise rejects', async () => {
window.fetch = jest.fn().mockRejectedValueOnce(new Error());
await expect(DevicesService.stopMonitoringEvents()).rejects.toThrow(new Error());
});
});
});

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

@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { Type } from 'protobufjs';
import {
CloudToDeviceMessageParameters,
FetchDeviceTwinParameters,
@ -23,11 +24,10 @@ import { Message } from '../models/messages';
import { Twin, Device, DataPlaneResponse } from '../models/device';
import { DeviceIdentity } from '../models/deviceIdentity';
import { dataPlaneConnectionHelper, dataPlaneResponseHelper, request, DATAPLANE_CONTROLLER_ENDPOINT, DataPlaneRequest } from './dataplaneServiceHelper';
import { getEventHubInterface } from '../shared/interfaceUtils';
import { parseEventHubMessage } from './eventHubMessageHelper';
import { HttpError } from '../models/httpError';
import { AppInsightsClient } from '../../shared/appTelemetry/appInsightsClient';
import { TELEMETRY_EVENTS } from '../../constants/telemetry';
import { startEventHubMonitoring, stopEventHubMonitoring } from '../handlers/eventHubServiceHandler';
const PAGE_SIZE = 100;
@ -251,15 +251,14 @@ export const deleteDevices = async (parameters: DeleteDevicesParameters) => {
return result && result.body;
};
// tslint:disable-next-line:cyclomatic-complexity
export const monitorEvents = async (parameters: MonitorEventsParameters): Promise<Message[]> => {
export const monitorEvents = async (parameters: MonitorEventsParameters): Promise<void> => {
let requestParameters = {
...parameters,
startTime: parameters.startTime && parameters.startTime.toISOString()
};
// if no custom event hub info is provided, use default hub connection string to connect to event hub
if (!parameters.customEventHubConnectionString || !parameters.customEventHubName) {
if (!parameters.customEventHubConnectionString) {
const connectionInfo = await dataPlaneConnectionHelper();
requestParameters = {
...requestParameters,
@ -267,12 +266,14 @@ export const monitorEvents = async (parameters: MonitorEventsParameters): Promis
};
}
const api = getEventHubInterface();
const result = await api.startEventHubMonitoring(requestParameters);
return result && result.length && result.length !== 0 && Promise.all(result.map(message => parseEventHubMessage(message, parameters.decoderPrototype)) || []);
await startEventHubMonitoring(requestParameters);
};
export const parseEvents = async (params: {messages: Message[], decoderPrototype?: Type}): Promise<Message[]> => {
const { messages, decoderPrototype } = params;
return Promise.all(messages?.map(message => parseEventHubMessage(message, decoderPrototype)) || []);
};
export const stopMonitoringEvents = async (): Promise<void> => {
const api = getEventHubInterface();
await api.stopEventHubMonitoring();
await stopEventHubMonitoring();
};

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

@ -49,16 +49,6 @@ describe('browserSettingsApi', () => {
});
});
describe('getDeviceInterface', () => {
it('calls expected factory when mode is electron', () => {
appConfig.hostMode = HostMode.Electron;
const factory = jest.spyOn(interfaceUtils, 'getElectronInterface');
interfaceUtils.getDeviceInterface();
expect(factory).toHaveBeenLastCalledWith(API_INTERFACES.DEVICE);
});
});
describe('getDirectoryInterface', () => {
it('calls expected factory when mode is electron', () => {
appConfig.hostMode = HostMode.Electron;
@ -79,12 +69,3 @@ describe('getLocalModelRepositoryInterface', () => {
});
});
describe('getEventHubInterface', () => {
it('calls expected factory when mode is electron', () => {
appConfig.hostMode = HostMode.Electron;
const factory = jest.spyOn(interfaceUtils, 'getElectronInterface');
interfaceUtils.getEventHubInterface()
expect(factory).toHaveBeenLastCalledWith(API_INTERFACES.EVENTHUB);
});
});

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

@ -3,17 +3,13 @@
* Licensed under the MIT License
**********************************************************/
import { SettingsInterface } from '../../../../public/interfaces/settingsInterface';
import { DeviceInterface } from '../../../../public/interfaces/deviceInterface';
import { DirectoryInterface } from '../../../../public/interfaces/directoryInterface';
import { ModelRepositoryInterface } from '../../../../public/interfaces/modelRepositoryInterface';
import { EventHubInterface } from './../../../../public/interfaces/eventHubInterface';
import { AuthenticationInterface } from './../../../../public/interfaces/authenticationInterface';
import { API_INTERFACES } from '../../../../public/constants';
import { appConfig, HostMode } from '../../../appConfig/appConfig';
import { HIGH_CONTRAST } from '../../constants/browserStorage';
import { LocalRepoServiceHandler } from '../legacy/localRepoServiceHandler';
import { DevicesServiceHandler } from '../legacy/devicesServiceHandler';
import { EventHubServiceHandler } from '../legacy/eventHubServiceHandler';
import { LocalRepoServiceHandler } from '../handlers/localRepoServiceHandler';
import { PublicDigitalTwinsModelRepoHelper, PublicDigitalTwinsModelInterface } from '../services/publicDigitalTwinsModelRepoHelper';
export const NOT_AVAILABLE = 'Feature is not available in this configuration';
@ -33,13 +29,6 @@ export const getSettingsInterfaceForBrowser = (): SettingsInterface => {
});
};
export const getDeviceInterface = (): DeviceInterface => {
if (appConfig.hostMode !== HostMode.Electron) {
return new DevicesServiceHandler();
}
return getElectronInterface(API_INTERFACES.DEVICE);
};
export const getLocalModelRepositoryInterface = (): ModelRepositoryInterface => {
if (appConfig.hostMode !== HostMode.Electron) {
return new LocalRepoServiceHandler();
@ -56,14 +45,6 @@ export const getDirectoryInterface = (): DirectoryInterface => {
return getElectronInterface(API_INTERFACES.DIRECTORY);
};
export const getEventHubInterface = (): EventHubInterface => {
if (appConfig.hostMode !== HostMode.Electron) {
return new EventHubServiceHandler();
}
return getElectronInterface(API_INTERFACES.EVENTHUB);
};
export const getPublicDigitalTwinsModelInterface = (): PublicDigitalTwinsModelInterface => {
return new PublicDigitalTwinsModelRepoHelper();
};

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

@ -29,6 +29,7 @@ export const SET_DECODE_INFO = 'SET_DECODE_INFO';
export const UPDATE_TWIN = 'UPDATE_TWIN';
export const UPDATE_DEVICE_IDENTITY = 'UPDATE_DEVICE_IDENTITY';
export const SET_DEFAULT_DECODE_INFO = 'SET_DEFAULT_DECODE_INFO';
export const SET_EVENTS_MESSAGES = 'SET_EVENTS_MESSAGES';
// module identity
export const ADD_MODULE_IDENTITY = 'ADD_MODULE_IDENTITY';

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

@ -9,7 +9,7 @@ import { CUSTOM_CONTROLLER_PORT } from './browserStorage';
export const DATAPLANE = '/DataPlane';
export const EVENTHUB = '/EventHub';
export const MODELREPO = '/ModelRepo';
export const CLOUD_TO_DEVICE = '/CloudToDevice';
export const READ_FILE = '/ReadFile';
export const GET_DIRECTORIES = '/Directories';
export const DEFAULT_DIRECTORY = '$DEFAULT';
@ -55,6 +55,7 @@ export enum DataPlaneStatusCode {
export const DEFAULT_CONSUMER_GROUP = '$Default';
const wsIp = 'ws://127.0.0.1';
const localIp = 'http://127.0.0.1';
const apiPath = '/api';
@ -71,6 +72,8 @@ export const CONTROLLER_API_ENDPOINT =
`${localIp}:${appConfig.controllerPort}${apiPath}` :
`${localIp}:${getPort()}${apiPath}`;
export const WEBSOCKET_ENDPOINT = `${wsIp}:${getPort()}`;
export enum HTTP_OPERATION_TYPES {
Delete = 'DELETE',
Get = 'GET',

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

@ -4,14 +4,15 @@
**********************************************************/
import actionCreatorFactory from 'typescript-fsa';
import { DEVICECONTENT } from '../../constants/actionPrefixes';
import { CLEAR_MONITORING_EVENTS, SET_DEFAULT_DECODE_INFO, SET_DECODE_INFO, START_EVENTS_MONITORING, STOP_EVENTS_MONITORING } from '../../constants/actionTypes';
import { CLEAR_MONITORING_EVENTS, SET_DEFAULT_DECODE_INFO, SET_DECODE_INFO, START_EVENTS_MONITORING, STOP_EVENTS_MONITORING, SET_EVENTS_MESSAGES } from '../../constants/actionTypes';
import { Message } from '../../api/models/messages';
import { MonitorEventsParameters, SetDecoderInfoParameters } from '../../api/parameters/deviceParameters';
import { ContentTypeState } from './state';
const deviceContentCreator = actionCreatorFactory(DEVICECONTENT);
export const startEventsMonitoringAction = deviceContentCreator.async<MonitorEventsParameters, Message[]>(START_EVENTS_MONITORING);
export const startEventsMonitoringAction = deviceContentCreator.async<MonitorEventsParameters, void>(START_EVENTS_MONITORING);
export const stopEventsMonitoringAction = deviceContentCreator.async<void, void>(STOP_EVENTS_MONITORING);
export const clearMonitoringEventsAction = deviceContentCreator(CLEAR_MONITORING_EVENTS);
export const setDecoderInfoAction = deviceContentCreator.async<SetDecoderInfoParameters, ContentTypeState>(SET_DECODE_INFO);
export const setDefaultDecodeInfoAction = deviceContentCreator(SET_DEFAULT_DECODE_INFO);
export const setEventsMessagesAction = deviceContentCreator.async<Message[], Message[]>(SET_EVENTS_MESSAGES);

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

@ -88,13 +88,13 @@ exports[`commands matches snapshot in hosted environment 1`] = `
items={
Array [
Object {
"ariaLabel": "deviceEvents.command.fetch",
"ariaLabel": "deviceEvents.command.start",
"disabled": false,
"iconProps": Object {
"iconName": "Play",
},
"key": "Play",
"name": "deviceEvents.command.fetch",
"name": "deviceEvents.command.start",
"onClick": [Function],
},
Object {

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

@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConsumerGroup matches snapshot 1`] = `
<StyledTextFieldBase
ariaLabel="deviceEvents.consumerGroups.label"
className="consumer-group-text-field"
disabled={true}
label="deviceEvents.consumerGroups.label"
onChange={[Function]}
onRenderLabel={[Function]}
underlined={true}
value="$Default"
/>
`;

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

@ -28,15 +28,7 @@ exports[`customEventHub matches snapshot 1`] = `
placeholder="deviceEvents.customEventHub.connectionString.placeHolder"
required={true}
underlined={true}
/>
<StyledTextFieldBase
ariaLabel="deviceEvents.customEventHub.name.label"
className="custom-text-field"
disabled={true}
label="deviceEvents.customEventHub.name.label"
onChange={[Function]}
required={true}
underlined={true}
value=""
/>
</Stack>
</Stack>

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

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`deviceEvents deviceEvents in non-pnp context matches snapshot 1`] = `
exports[`deviceEvents matches snapshot after loaded 1`] = `
<Stack
className="device-events"
key="device-events"
@ -18,6 +18,7 @@ exports[`deviceEvents deviceEvents in non-pnp context matches snapshot 1`] = `
showSimulationPanel={false}
showSystemProperties={false}
startDisabled={false}
stopFetching={[Function]}
/>
<HeaderView
headerText="deviceEvents.headerText"
@ -45,10 +46,9 @@ exports[`deviceEvents deviceEvents in non-pnp context matches snapshot 1`] = `
<CustomEventHub
monitoringData={false}
setCustomEventHubConnectionString={[Function]}
setCustomEventHubName={[Function]}
setHasError={[Function]}
setUseBuiltInEventHub={[Function]}
useBuiltInEventHub={false}
useBuiltInEventHub={true}
/>
</div>
<DeviceSimulationPanel
@ -62,168 +62,15 @@ exports[`deviceEvents deviceEvents in non-pnp context matches snapshot 1`] = `
<div
className="device-events-container"
>
<div
className=""
>
<article
className="device-events-content"
key="0"
>
<h5>
Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)
:
</h5>
<pre>
{
"body": {
"humid": 123
},
"enqueuedTime": "Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)",
"properties": {
"iothub-message-schema": "humid"
}
}
</pre>
</article>
</div>
</div>
</Stack>
`;
exports[`deviceEvents deviceEvents in pnp context matches snapshot while interface cannot be found 1`] = `
<Stack
className="device-events"
key="device-events"
>
<Commands
fetchData={[Function]}
monitoringData={false}
setMonitoringData={[Function]}
setShowContentTypePanel={[Function]}
setShowPnpModeledEvents={[Function]}
setShowSimulationPanel={[Function]}
setShowSystemProperties={[Function]}
showContentTypePanel={false}
showPnpModeledEvents={true}
showSimulationPanel={false}
showSystemProperties={false}
startDisabled={false}
/>
<HeaderView
headerText="deviceEvents.headerText"
tooltip="deviceEvents.tooltip"
/>
<div
className="horizontal-item"
>
<ConsumerGroup
consumerGroup="$Default"
<Loader
monitoringData={false}
setConsumerGroup={[Function]}
/>
</div>
<StartTime
monitoringData={false}
setHasError={[Function]}
setSpecifyStartTime={[Function]}
setStartTime={[Function]}
specifyStartTime={false}
/>
<div
className="horizontal-item"
>
<CustomEventHub
monitoringData={false}
setCustomEventHubConnectionString={[Function]}
setCustomEventHubName={[Function]}
setHasError={[Function]}
setUseBuiltInEventHub={[Function]}
useBuiltInEventHub={false}
/>
</div>
<DeviceSimulationPanel
onToggleSimulationPanel={[Function]}
showSimulationPanel={false}
/>
<DeviceContentTypePanel
onToggleContentTypePanel={[Function]}
showContentTypePanel={false}
/>
<div
className="device-events-container"
>
<div
className=""
<EventsContent
showPnpModeledEvents={false}
showSystemProperties={false}
/>
</div>
</Stack>
`;
exports[`deviceEvents deviceEvents in pnp context matches snapshot while interface definition is retrieved 1`] = `
<Stack
className="device-events"
key="device-events"
>
<Commands
fetchData={[Function]}
monitoringData={false}
setMonitoringData={[Function]}
setShowContentTypePanel={[Function]}
setShowPnpModeledEvents={[Function]}
setShowSimulationPanel={[Function]}
setShowSystemProperties={[Function]}
showContentTypePanel={false}
showPnpModeledEvents={true}
showSimulationPanel={false}
showSystemProperties={false}
startDisabled={false}
/>
<HeaderView
headerText="deviceEvents.headerText"
tooltip="deviceEvents.tooltip"
/>
<div
className="horizontal-item"
>
<ConsumerGroup
consumerGroup="$Default"
monitoringData={false}
setConsumerGroup={[Function]}
/>
</div>
<StartTime
monitoringData={false}
setHasError={[Function]}
setSpecifyStartTime={[Function]}
setStartTime={[Function]}
specifyStartTime={false}
/>
<div
className="horizontal-item"
>
<CustomEventHub
monitoringData={false}
setCustomEventHubConnectionString={[Function]}
setCustomEventHubName={[Function]}
setHasError={[Function]}
setUseBuiltInEventHub={[Function]}
useBuiltInEventHub={false}
/>
</div>
<DeviceSimulationPanel
onToggleSimulationPanel={[Function]}
showSimulationPanel={false}
/>
<DeviceContentTypePanel
onToggleContentTypePanel={[Function]}
showContentTypePanel={false}
/>
<div
className="device-events-container"
>
<div
className=""
/>
</div>
</Stack>
`;
exports[`deviceEvents matches snapshot when loading 1`] = `<MultiLineShimmer />`;

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

@ -0,0 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EventsContent EventsContent in non-pnp context matches snapshot 1`] = `
<div
className=""
>
<article
className="device-events-content"
key="0"
>
<h5>
Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)
:
</h5>
<pre>
{
"body": {
"humid": 123
},
"enqueuedTime": "Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)",
"properties": {
"iothub-message-schema": "humid"
}
}
</pre>
</article>
</div>
`;
exports[`EventsContent EventsContent in pnp context matches snapshot while interface definition is retrieved 1`] = `
<div
className=""
>
<div
className="pnp-modeled-list"
>
<div
className="list-header list-header-uncollapsible flex-grid-row"
>
<span
className="col-sm2"
>
deviceEvents.columns.timestamp
</span>
<span
className="col-sm2"
>
deviceEvents.columns.displayName
</span>
<span
className="col-sm2"
>
deviceEvents.columns.schema
</span>
<span
className="col-sm2"
>
deviceEvents.columns.unit
</span>
<span
className="col-sm4"
>
deviceEvents.columns.value
</span>
</div>
</div>
<section
role="feed"
>
<article
className="list-item event-list-item"
key="0"
role="article"
>
<section
className="flex-grid-row item-summary"
>
<ErrorBoundary
error="errorBoundary.text"
>
<div
className="col-sm2"
>
<StyledLabelBase
aria-label="deviceEvents.columns.timestamp"
>
Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)
</StyledLabelBase>
</div>
<div
className="col-sm2"
>
<StyledLabelBase
aria-label="deviceEvents.columns.displayName"
>
--
</StyledLabelBase>
</div>
<div
className="col-sm2"
>
<StyledLabelBase
aria-label="deviceEvents.columns.schema"
>
--
</StyledLabelBase>
</div>
<div
className="col-sm2"
>
<StyledLabelBase
aria-label="deviceEvents.columns.unit"
>
<SemanticUnit />
</StyledLabelBase>
</div>
<div
className="column-value-text col-sm4"
>
<StyledLabelBase
aria-label="deviceEvents.columns.value"
>
{
"humid": 123
}
</StyledLabelBase>
</div>
</ErrorBoundary>
</section>
</article>
</section>
</div>
`;

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

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Loader matches snapshot when loading 1`] = `
<Fragment>
<StyledMessageBar
messageBarType={0}
>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 10,
}
}
>
<div>
deviceEvents.infiniteScroll.loading
</div>
<StyledSpinnerBase
size={1}
/>
</Stack>
</StyledMessageBar>
</Fragment>
`;
exports[`Loader matches snapshot when not loading 1`] = `<Fragment />`;

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

@ -38,6 +38,7 @@ describe('commands', () => {
setShowPnpModeledEvents={jest.fn()}
setShowSimulationPanel={jest.fn()}
fetchData={jest.fn()}
stopFetching={jest.fn()}
/>)).toMatchSnapshot();
});
@ -59,6 +60,7 @@ describe('commands', () => {
setShowPnpModeledEvents={jest.fn()}
setShowSimulationPanel={jest.fn()}
fetchData={jest.fn()}
stopFetching={jest.fn()}
/>)).toMatchSnapshot();
});
@ -82,6 +84,7 @@ describe('commands', () => {
setShowPnpModeledEvents={mockSetShowPnpModeledEvents}
setShowSimulationPanel={jest.fn()}
fetchData={jest.fn()}
stopFetching={jest.fn()}
/>);
expect(wrapper.find(CommandBar).props().items.length).toEqual(5);

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

@ -8,14 +8,13 @@ import { useLocation, useHistory } from 'react-router-dom';
import { CommandBar, ICommandBarItemProps } from '@fluentui/react';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import { CLEAR, CHECKED_CHECKBOX, EMPTY_CHECKBOX, START, STOP, NAVIGATE_BACK, REFRESH, REMOVE, CODE, UPLOAD } from '../../../constants/iconNames';
import { appConfig, HostMode } from '../../../../appConfig/appConfig';
import { getComponentNameFromQueryString } from '../../../shared/utils/queryStringHelper';
import { usePnpStateContext } from '../../../shared/contexts/pnpStateContext';
import './deviceEvents.scss';
import { getBackUrl } from '../../pnp/utils';
import { AppInsightsClient } from '../../../shared/appTelemetry/appInsightsClient';
import { TELEMETRY_USER_ACTIONS } from '../../../../app/constants/telemetry';
import { useDeviceEventsStateContext } from '../context/deviceEventsStateContext';
import './deviceEvents.scss';
export interface CommandsProps {
startDisabled: boolean;
@ -30,6 +29,7 @@ export interface CommandsProps {
setShowSimulationPanel: (showSimulationPanel: boolean) => void;
setShowContentTypePanel: (showDecoderPanel: boolean) => void;
fetchData(): void;
stopFetching(): void;
}
export const Commands: React.FC<CommandsProps> = ({
@ -44,12 +44,13 @@ export const Commands: React.FC<CommandsProps> = ({
setShowPnpModeledEvents,
setShowSimulationPanel,
setShowContentTypePanel,
fetchData}) => {
fetchData,
stopFetching}) => {
const {t} = useTranslation();
const { search, pathname } = useLocation();
const history = useHistory();
const { pnpState, getModelDefinition } = usePnpStateContext();
const { getModelDefinition } = usePnpStateContext();
const componentName = getComponentNameFromQueryString(search); // if component name exist, we are in pnp context
const [ state, api ] = useDeviceEventsStateContext();
@ -111,32 +112,18 @@ export const Commands: React.FC<CommandsProps> = ({
// tslint:disable-next-line: cyclomatic-complexity
const createStartMonitoringCommandItem = (): ICommandBarItemProps => {
if (appConfig.hostMode !== HostMode.Browser) {
const label = monitoringData ? t(ResourceKeys.deviceEvents.command.stop) : t(ResourceKeys.deviceEvents.command.start);
const icon = monitoringData ? STOP : START;
return {
ariaLabel: label,
disabled: startDisabled,
iconProps: {
iconName: icon
},
key: icon,
name: label,
onClick: onToggleStart
};
}
else {
return {
ariaLabel: t(ResourceKeys.deviceEvents.command.fetch),
disabled: state.formMode === 'updating' || monitoringData,
iconProps: {
iconName: START
},
key: START,
name: t(ResourceKeys.deviceEvents.command.fetch),
onClick: onToggleStart
};
}
const label = monitoringData ? t(ResourceKeys.deviceEvents.command.stop) : t(ResourceKeys.deviceEvents.command.start);
const icon = monitoringData ? STOP : START;
return {
ariaLabel: label,
disabled: startDisabled,
iconProps: {
iconName: icon
},
key: icon,
name: label,
onClick: onToggleStart
};
};
const createSimulationCommandItem = (): ICommandBarItemProps => {
@ -194,6 +181,7 @@ export const Commands: React.FC<CommandsProps> = ({
const onToggleStart = () => {
if (monitoringData) {
setMonitoringData(false);
stopFetching();
} else {
fetchData();
setMonitoringData(true);

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

@ -0,0 +1,24 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as React from 'react';
import { act } from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';
import { TextField } from '@fluentui/react';
import { ConsumerGroup } from './consumerGroup';
describe('ConsumerGroup', () => {
it('matches snapshot', () => {
expect(shallow(<ConsumerGroup monitoringData={true} consumerGroup={'$Default'} setConsumerGroup={jest.fn()}/>)).toMatchSnapshot();
});
it('changes state accordingly when consumer group value is changed', () => {
const setConsumerGroup = jest.fn();
const wrapper = mount(<ConsumerGroup monitoringData={false} consumerGroup={''} setConsumerGroup={setConsumerGroup}/>);
const textField = wrapper.find(TextField).first();
act(() => textField.props().onChange(undefined, 'testGroup'));
expect(setConsumerGroup).toBeCalledWith('testGroup');
});
});

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

@ -22,10 +22,8 @@ describe('customEventHub', () => {
<CustomEventHub
monitoringData={true}
useBuiltInEventHub={false}
customEventHubName={undefined}
customEventHubConnectionString={undefined}
customEventHubConnectionString={''}
setUseBuiltInEventHub={jest.fn()}
setCustomEventHubName={jest.fn()}
setCustomEventHubConnectionString={jest.fn()}
setHasError={jest.fn()}
/>)).toMatchSnapshot();
@ -39,10 +37,8 @@ describe('customEventHub', () => {
<CustomEventHub
monitoringData={true}
useBuiltInEventHub={false}
customEventHubName={undefined}
customEventHubConnectionString={undefined}
customEventHubConnectionString={''}
setUseBuiltInEventHub={mockSetUseBuiltInEventHub}
setCustomEventHubName={mockSetCustomEventHubName}
setCustomEventHubConnectionString={mockSetCustomEventHubConnectionString}
setHasError={jest.fn()}
/>);
@ -52,9 +48,7 @@ describe('customEventHub', () => {
expect(mockSetUseBuiltInEventHub).toBeCalledWith(true);
act(() => wrapper.find(TextField).first().props().onChange(undefined, 'connectionString'));
act(() => wrapper.find(TextField).at(1).props().onChange(undefined, 'hubName'));
wrapper.update();
expect(mockSetCustomEventHubConnectionString).toBeCalledWith('connectionString');
expect(mockSetCustomEventHubName).toBeCalledWith('hubName');
});
});

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

@ -12,10 +12,8 @@ import './deviceEvents.scss';
export interface CustomEventHubProps {
monitoringData: boolean;
useBuiltInEventHub: boolean;
customEventHubName: string;
customEventHubConnectionString: string;
setUseBuiltInEventHub: (monitoringData: boolean) => void;
setCustomEventHubName: (customEventHubName: string) => void;
setCustomEventHubConnectionString: (customEventHubConnectionString: string) => void;
setHasError: (hasError: boolean) => void;
}
@ -23,10 +21,8 @@ export interface CustomEventHubProps {
export const CustomEventHub: React.FC<CustomEventHubProps> = ({
monitoringData,
useBuiltInEventHub,
customEventHubName,
customEventHubConnectionString,
setUseBuiltInEventHub,
setCustomEventHubName,
setCustomEventHubConnectionString,
setHasError}) => {
@ -52,10 +48,6 @@ export const CustomEventHub: React.FC<CustomEventHubProps> = ({
setCustomEventHubConnectionString(newValue);
};
const customEventHubNameChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
setCustomEventHubName(newValue);
};
return (
<Stack horizontal={true} horizontalAlign="space-between">
<Toggle
@ -82,16 +74,6 @@ export const CustomEventHub: React.FC<CustomEventHubProps> = ({
errorMessage={error}
required={true}
/>
<TextField
className="custom-text-field"
label={t(ResourceKeys.deviceEvents.customEventHub.name.label)}
ariaLabel={t(ResourceKeys.deviceEvents.customEventHub.name.label)}
underlined={true}
value={customEventHubName}
disabled={monitoringData}
onChange={customEventHubNameChange}
required={true}
/>
</Stack>
}
</Stack>

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

@ -4,22 +4,11 @@
**********************************************************/
import 'jest';
import * as React from 'react';
import { act } from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';
import { TextField, CommandBar, Shimmer } from '@fluentui/react';
import { DeviceEvents } from './deviceEvents';
import { DEFAULT_CONSUMER_GROUP } from '../../../constants/apiConstants';
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
import * as pnpStateContext from '../../../shared/contexts/pnpStateContext';
import { pnpStateInitial, PnpStateInterface } from '../../pnp/state';
import { testModelDefinition } from '../../pnp/components/deviceEvents/testData';
import { REPOSITORY_LOCATION_TYPE } from '../../../constants/repositoryLocationTypes';
import { ErrorBoundary } from '../../shared/components/errorBoundary';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import * as TransformHelper from '../../../api/dataTransforms/transformHelper';
import * as deviceEventsStateContext from '../context/deviceEventsStateContext';
import { getInitialDeviceEventsState } from '../state';
const pathname = `#/devices/detail/events/?id=device1`;
jest.mock('react-router-dom', () => ({
useHistory: () => ({ push: jest.fn() }),
@ -27,250 +16,27 @@ jest.mock('react-router-dom', () => ({
}));
describe('deviceEvents', () => {
describe('deviceEvents in non-pnp context', () => {
const events = [{
body: {
humid: 123
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
properties: {
'iothub-message-schema': 'humid'
it('matches snapshot when loading', () => {
const pnpState: PnpStateInterface = {
...pnpStateInitial(),
modelDefinitionWithSource: {
payload: undefined,
synchronizationStatus: SynchronizationStatus.working
}
}];
beforeEach(() => {
const realUseState = React.useState;
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(DEFAULT_CONSUMER_GROUP));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(undefined));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(undefined));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(undefined));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(TransformHelper, 'parseDateTimeString').mockImplementationOnce(parameters => {
return '9:44:58 PM, 10/14/2019';
});
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('matches snapshot', () => {
const startEventsMonitoring = jest.fn();
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events},
{...deviceEventsStateContext.getInitialDeviceEventsOps(), startEventsMonitoring}]);
expect(shallow(<DeviceEvents/>)).toMatchSnapshot();
});
it('changes state accordingly when command bar buttons are clicked', () => {
const startEventsMonitoring = jest.fn();
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[getInitialDeviceEventsState(),
{...deviceEventsStateContext.getInitialDeviceEventsOps(), startEventsMonitoring}]);
const wrapper = mount(<DeviceEvents/>);
const commandBar = wrapper.find(CommandBar).first();
// tslint:disable-next-line: no-magic-numbers
expect(commandBar.props().items.length).toEqual(5);
// click the start button
act(() => commandBar.props().items[0].onClick(null));
wrapper.update();
expect(startEventsMonitoring).toBeCalledWith({
consumerGroup: DEFAULT_CONSUMER_GROUP,
deviceId: 'device1',
moduleId: null,
startListeners: true,
startTime: undefined
});
});
it('changes state accordingly when consumer group value is changed', () => {
const wrapper = mount(<DeviceEvents/>);
const textField = wrapper.find(TextField).first();
act(() => textField.props().onChange(undefined, 'testGroup'));
wrapper.update();
expect(wrapper.find(TextField).first().props().value).toEqual('testGroup');
});
it('renders events', () => {
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: pnpStateInitial(), dispatch: jest.fn()});
const wrapper = mount(<DeviceEvents/>);
const rawTelemetry = wrapper.find('article');
expect(rawTelemetry).toHaveLength(1);
});
};
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({pnpState, dispatch: jest.fn(), getModelDefinition: jest.fn()});
expect(shallow(<DeviceEvents/>)).toMatchSnapshot();
});
describe('deviceEvents in pnp context', () => {
const getModelDefinitionMock = jest.fn();
const mockFetchedState = () => {
const pnpState: PnpStateInterface = {
...pnpStateInitial(),
modelDefinitionWithSource: {
payload: {
isModelValid: true,
modelDefinition: testModelDefinition,
source: REPOSITORY_LOCATION_TYPE.Public,
},
synchronizationStatus: SynchronizationStatus.fetched
}
};
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({pnpState, dispatch: jest.fn(), getModelDefinition: getModelDefinitionMock});
it('matches snapshot after loaded', () => {
const pnpState: PnpStateInterface = {
...pnpStateInitial(),
modelDefinitionWithSource: {
payload: undefined,
synchronizationStatus: SynchronizationStatus.fetched
}
};
beforeEach(() => {
const realUseState = React.useState;
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(DEFAULT_CONSUMER_GROUP));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(undefined));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(undefined));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(undefined));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(false));
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(true));
jest.spyOn(TransformHelper, 'parseDateTimeString').mockImplementationOnce(parameters => {
return '9:44:58 PM, 10/14/2019';
});
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('renders Shimmer while loading', () => {
const pnpState: PnpStateInterface = {
...pnpStateInitial(),
modelDefinitionWithSource: {
synchronizationStatus: SynchronizationStatus.working
}
};
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValueOnce({pnpState, dispatch: jest.fn(), getModelDefinition: jest.fn()});
const wrapper = mount(<DeviceEvents/>);
expect(wrapper.find(Shimmer)).toBeDefined();
});
it('matches snapshot while interface cannot be found', () => {
const pnpState: PnpStateInterface = {
...pnpStateInitial(),
modelDefinitionWithSource: {
payload: null,
synchronizationStatus: SynchronizationStatus.fetched
}
};
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValueOnce({pnpState, dispatch: jest.fn(), getModelDefinition: jest.fn()});
expect(shallow(<DeviceEvents/>)).toMatchSnapshot();
});
it('matches snapshot while interface definition is retrieved', () => {
mockFetchedState();
expect(shallow(<DeviceEvents/>)).toMatchSnapshot();
});
it('renders events which body\'s value type is wrong with expected columns', () => {
const events = [{
body: {
humid: '123' // intentionally set a value which type is double
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
systemProperties: {
'iothub-message-schema': 'humid'
}
}];
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = mount(<DeviceEvents/>);
const errorBoundary = wrapper.find(ErrorBoundary);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('humid (Temperature)');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('double'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify(events[0].body, undefined, 2)); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[1].props['aria-label']).toEqual(ResourceKeys.deviceEvents.columns.validation.value.label); // tslint:disable-line:no-magic-numbers
});
it('renders events which body\'s key name is wrong with expected columns', () => {
const events = [{
body: {
'non-matching-key': 0
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
systemProperties: {
'iothub-message-schema': 'humid'
}
}];
mockFetchedState();
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = mount(<DeviceEvents/>);
const errorBoundary = wrapper.find(ErrorBoundary);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('humid (Temperature)');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('double'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify(events[0].body, undefined, 2)); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[1].props.className).toEqual('value-validation-error'); // tslint:disable-line:no-magic-numbers
});
it('renders events when body is exploded and schema is not provided in system properties', () => {
const events = [{
body: {
'humid': 0,
'humid-foo': 'test'
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
systemProperties: {}
}];
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = shallow(<DeviceEvents/>);
let errorBoundary = wrapper.find(ErrorBoundary).first();
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('humid (Temperature)');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('double'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify({humid: 0}, undefined, 2)); // tslint:disable-line:no-magic-numbers
errorBoundary = wrapper.find(ErrorBoundary).at(1);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('--');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('--'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify({'humid-foo': 'test'}, undefined, 2)); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[1].props.className).toEqual('value-validation-error'); // tslint:disable-line:no-magic-numbers
});
it('renders events which body\'s key name is not populated', () => {
const events = [{
body: {
'non-matching-key': 0
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)'
}];
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = shallow(<DeviceEvents/>);
const errorBoundary = wrapper.find(ErrorBoundary);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('--');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('--'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children).toEqual(JSON.stringify(events[0].body, undefined, 2)); // tslint:disable-line:no-magic-numbers
});
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({pnpState, dispatch: jest.fn(), getModelDefinition: jest.fn()});
expect(shallow(<DeviceEvents/>)).toMatchSnapshot();
});
});

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

@ -3,49 +3,33 @@
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Spinner, SpinnerSize, Label, Stack, MessageBarType, MessageBar } from '@fluentui/react';
import { Stack } from '@fluentui/react';
import { useLocation } from 'react-router-dom';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import { Message, MESSAGE_SYSTEM_PROPERTIES, MESSAGE_PROPERTIES, IOTHUB_MESSAGE_SOURCE_TELEMETRY } from '../../../api/models/messages';
import { getDeviceIdFromQueryString, getComponentNameFromQueryString, getInterfaceIdFromQueryString, getModuleIdentityIdFromQueryString } from '../../../shared/utils/queryStringHelper';
import { getDeviceIdFromQueryString, getModuleIdentityIdFromQueryString } from '../../../shared/utils/queryStringHelper';
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
import { MonitorEventsParameters } from '../../../api/parameters/deviceParameters';
import { DEFAULT_CONSUMER_GROUP } from '../../../constants/apiConstants';
import { appConfig, HostMode } from '../../../../appConfig/appConfig';
import { DEFAULT_CONSUMER_GROUP, WEBSOCKET_ENDPOINT } from '../../../constants/apiConstants';
import { HeaderView } from '../../../shared/components/headerView';
import { useDeviceEventsStateContext } from '../context/deviceEventsStateContext';
import { DEFAULT_COMPONENT_FOR_DIGITAL_TWIN } from '../../../constants/devices';
import { usePnpStateContext } from '../../../shared/contexts/pnpStateContext';
import { getDeviceTelemetry, TelemetrySchema } from '../../pnp/components/deviceEvents/dataHelper';
import { MultiLineShimmer } from '../../../shared/components/multiLineShimmer';
import { ErrorBoundary } from '../../shared/components/errorBoundary';
import { SemanticUnit } from '../../../shared/units/components/semanticUnit';
import { getSchemaType, getSchemaValidationErrors } from '../../../shared/utils/jsonSchemaAdaptor';
import { ParsedJsonSchema } from '../../../api/models/interfaceJsonParserOutput';
import { TelemetryContent } from '../../../api/models/modelDefinition';
import { getLocalizedData } from '../../../api/dataTransforms/modelDefinitionTransform';
import { DeviceSimulationPanel } from './deviceSimulationPanel';
import { Commands } from './commands';
import { CustomEventHub } from './customEventHub';
import { ConsumerGroup } from './consumerGroup';
import { StartTime } from './startTime';
import { AppInsightsClient } from '../../../shared/appTelemetry/appInsightsClient';
import { TELEMETRY_PAGE_NAMES } from '../../../../app/constants/telemetry';
import './deviceEvents.scss';
import { DeviceContentTypePanel } from './deviceContentTypePanel';
import { Loader } from './loader';
import { EventsContent } from './eventsContent';
import './deviceEvents.scss';
const JSON_SPACES = 2;
const LOADING_LOCK = 8000;
let client: WebSocket;
export const DeviceEvents: React.FC = () => {
const { t } = useTranslation();
const { search } = useLocation();
const deviceId = getDeviceIdFromQueryString(search);
const moduleId = getModuleIdentityIdFromQueryString(search);
const [ state, api ] = useDeviceEventsStateContext();
const events = state.message;
const decoderPrototype = state.contentType.decoderPrototype;
// event hub settings
@ -54,7 +38,6 @@ export const DeviceEvents: React.FC = () => {
const [startTime, setStartTime] = React.useState<Date>();
const [useBuiltInEventHub, setUseBuiltInEventHub] = React.useState<boolean>(true);
const [customEventHubConnectionString, setCustomEventHubConnectionString] = React.useState<string>(undefined);
const [customEventHubName, setCustomEventHubName] = React.useState<string>(undefined);
const [showSystemProperties, setShowSystemProperties] = React.useState<boolean>(false);
// event message state
@ -63,14 +46,8 @@ export const DeviceEvents: React.FC = () => {
const [hasError, setHasError] = React.useState<boolean>(false);
// pnp events specific
const TELEMETRY_SCHEMA_PROP = MESSAGE_PROPERTIES.IOTHUB_MESSAGE_SCHEMA;
const componentName = getComponentNameFromQueryString(search); // if component name exist, we are in pnp context
const interfaceId = getInterfaceIdFromQueryString(search);
const { pnpState, } = usePnpStateContext();
const modelDefinitionWithSource = pnpState.modelDefinitionWithSource.payload;
const modelDefinition = modelDefinitionWithSource && modelDefinitionWithSource.modelDefinition;
const isLoading = pnpState.modelDefinitionWithSource.synchronizationStatus === SynchronizationStatus.working;
const telemetrySchema = React.useMemo(() => getDeviceTelemetry(modelDefinition), [modelDefinition]);
const [showPnpModeledEvents, setShowPnpModeledEvents] = React.useState(false);
// simulation specific
@ -83,17 +60,19 @@ export const DeviceEvents: React.FC = () => {
() => {
return () => {
stopMonitoring();
client?.close();
};
},
[]);
React.useEffect(() => {
if (componentName) {
AppInsightsClient.getInstance()?.trackPageView({ name: TELEMETRY_PAGE_NAMES.PNP_TELEMETRY });
} else {
AppInsightsClient.getInstance()?.trackPageView({ name: TELEMETRY_PAGE_NAMES.DEVICE_TELEMETRY });
}
}, []); // tslint:disable-line: align
React.useEffect(
() => {
client = new WebSocket(WEBSOCKET_ENDPOINT);
client.onmessage = message => {
api.setEvents(JSON.parse(message.data));
};
},
[]);
React.useEffect( // tslint:disable-next-line: cyclomatic-complexity
() => {
@ -101,43 +80,14 @@ export const DeviceEvents: React.FC = () => {
// when specifying start time, valid time need to be provided
(specifyStartTime && (!startTime || hasError)) ||
// when using custom event hub, both valid connection string and name need to be provided
(!useBuiltInEventHub && (!customEventHubConnectionString || !customEventHubName || hasError))) {
(!useBuiltInEventHub && (!customEventHubConnectionString || hasError))) {
setStartDisabled(true);
}
else {
setStartDisabled(false);
}
},
[hasError, state.formMode, useBuiltInEventHub, customEventHubConnectionString, customEventHubName, specifyStartTime, startTime]);
React.useEffect(// tslint:disable-next-line: cyclomatic-complexity
() => {
if (state.formMode === 'fetched') {
if (appConfig.hostMode !== HostMode.Browser) {
if (monitoringData) {
setStartTime(new Date());
setTimeout(
() => {
fetchData(false)();
},
LOADING_LOCK);
}
else {
stopMonitoring();
}
}
else {
stopMonitoring();
}
}
if (state.formMode === 'upserted') {
setMonitoringData(false);
}
if (monitoringData && state.formMode === 'failed') {
stopMonitoring();
}
},
[state.formMode]);
[hasError, state.formMode, useBuiltInEventHub, customEventHubConnectionString, specifyStartTime, startTime]);
const renderCommands = () => {
return (
@ -153,7 +103,8 @@ export const DeviceEvents: React.FC = () => {
setShowPnpModeledEvents={setShowPnpModeledEvents}
setShowSimulationPanel={setShowSimulationPanel}
setShowContentTypePanel={setShowContentTypePanel}
fetchData={fetchData(true)}
fetchData={fetchData}
stopFetching={stopMonitoring}
/>
);
};
@ -189,10 +140,8 @@ export const DeviceEvents: React.FC = () => {
<CustomEventHub
monitoringData={monitoringData}
useBuiltInEventHub={useBuiltInEventHub}
customEventHubName={customEventHubName}
customEventHubConnectionString={customEventHubConnectionString}
setUseBuiltInEventHub={setUseBuiltInEventHub}
setCustomEventHubName={setCustomEventHubName}
setCustomEventHubConnectionString={setCustomEventHubConnectionString}
setHasError={setHasError}
/>
@ -204,308 +153,23 @@ export const DeviceEvents: React.FC = () => {
api.stopEventsMonitoring();
};
// tslint:disable-next-line: cyclomatic-complexity
const filterMessage = (message: Message) => {
if (!message || !message.systemProperties) {
return false;
}
if (message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_MESSAGE_SOURCE] &&
message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_MESSAGE_SOURCE].toLowerCase() !== IOTHUB_MESSAGE_SOURCE_TELEMETRY) {
// filter out telemetry sent from other sources
return false;
}
if (componentName === DEFAULT_COMPONENT_FOR_DIGITAL_TWIN) {
// for default component, we only expect ${IOTHUB_INTERFACE_ID} to be in the system property not ${IOTHUB_COMPONENT_NAME}
return message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_INTERFACE_ID] === interfaceId &&
!message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_COMPONENT_NAME];
}
return message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_COMPONENT_NAME] === componentName;
};
const renderRawEvents = () => {
const filteredEvents = componentName ? events?.filter(result => filterMessage(result)) : events;
return (
<>
{
filteredEvents && filteredEvents.map((event: Message, index) => {
const modifiedEvents = showSystemProperties ? event : {
body: event.body,
enqueuedTime: event.enqueuedTime,
properties: event.properties
};
return (
<article key={index} className="device-events-content">
{<h5>{modifiedEvents.enqueuedTime}:</h5>}
<pre>{JSON.stringify(modifiedEvents, undefined, JSON_SPACES)}</pre>
</article>
);
})
}
</>
);
};
//#region pnp specific render
const renderPnpModeledEvents = () => {
const filteredEvents = componentName ? events.filter(result => filterMessage(result)) : events;
return (
<>
{
filteredEvents && filteredEvents.length > 0 &&
<>
<div className="pnp-modeled-list">
<div className="list-header list-header-uncollapsible flex-grid-row">
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.timestamp)}</span>
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.displayName)}</span>
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.schema)}</span>
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.unit)}</span>
<span className="col-sm4">{t(ResourceKeys.deviceEvents.columns.value)}</span>
</div>
</div>
<section role="feed">
{
filteredEvents.map((event: Message, index) => {
return !event.systemProperties ? renderEventsWithNoSystemProperties(event, index) :
event.systemProperties[TELEMETRY_SCHEMA_PROP] ?
renderEventsWithSchemaProperty(event, index) :
renderEventsWithNoSchemaProperty(event, index);
})
}
</section>
</>
}
</>
);
};
const renderEventsWithNoSystemProperties = (event: Message, index: number) => {
return (
<article className="list-item event-list-item" role="article" key={index}>
<section className="flex-grid-row item-summary">
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(event.enqueuedTime)}
{renderEventName()}
{renderEventSchema()}
{renderEventUnit()}
{renderMessageBodyWithNoSchema(event.body)}
</ErrorBoundary>
</section>
</article>
);
};
const renderEventsWithSchemaProperty = (event: Message, index: number) => {
const { telemetryModelDefinition, parsedSchema } = getModelDefinitionAndSchema(event.systemProperties[TELEMETRY_SCHEMA_PROP]);
return (
<article className="list-item event-list-item" role="article" key={index}>
<section className="flex-grid-row item-summary">
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(event.enqueuedTime)}
{renderEventName(telemetryModelDefinition)}
{renderEventSchema(telemetryModelDefinition)}
{renderEventUnit(telemetryModelDefinition)}
{renderMessageBodyWithSchema(event.body, parsedSchema, event.systemProperties[TELEMETRY_SCHEMA_PROP])}
</ErrorBoundary>
</section>
</article>
);
};
const renderEventsWithNoSchemaProperty = (event: Message, index: number) => {
const telemetryKeys = Object.keys(event.body);
if (telemetryKeys && telemetryKeys.length !== 0) {
return telemetryKeys.map((key, keyIndex) => {
const { telemetryModelDefinition, parsedSchema } = getModelDefinitionAndSchema(key);
const partialEventBody: any = {}; // tslint:disable-line:no-any
partialEventBody[key] = event.body[key];
const isNotItemLast = keyIndex !== telemetryKeys.length - 1;
return (
<article className="list-item event-list-item" role="article" key={key + index}>
<section className={`flex-grid-row item-summary ${isNotItemLast && 'item-summary-partial'}`}>
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(keyIndex === 0 ? event.enqueuedTime : null)}
{renderEventName(telemetryModelDefinition)}
{renderEventSchema(telemetryModelDefinition)}
{renderEventUnit(telemetryModelDefinition)}
{renderMessageBodyWithSchema(partialEventBody, parsedSchema, key)}
</ErrorBoundary>
</section>
</article>
);
});
}
return (
<article className="list-item event-list-item" role="article" key={index}>
<section className="flex-grid-row item-summary">
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(event.enqueuedTime)}
{renderEventName()}
{renderEventSchema()}
{renderEventUnit()}
{renderMessageBodyWithSchema(event.body, null, null)}
</ErrorBoundary>
</section>
</article>
);
};
const renderTimestamp = (enqueuedTime: string) => {
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.timestamp)}>
{enqueuedTime}
</Label>
</div>
);
};
const renderEventName = (telemetryModelDefinition?: TelemetryContent) => {
const displayName = telemetryModelDefinition ? getLocalizedData(telemetryModelDefinition.displayName) : '';
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.displayName)}>
{telemetryModelDefinition ?
`${telemetryModelDefinition.name} (${displayName ? displayName : '--'})` : '--'}
</Label>
</div>
);
};
const renderEventSchema = (telemetryModelDefinition?: TelemetryContent) => {
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.schema)}>
{telemetryModelDefinition ? getSchemaType(telemetryModelDefinition.schema) : '--'}
</Label>
</div>
);
};
const renderEventUnit = (telemetryModelDefinition?: TelemetryContent) => {
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.unit)}>
<SemanticUnit unitHost={telemetryModelDefinition} />
</Label>
</div>
);
};
// tslint:disable-next-line: cyclomatic-complexity
const renderMessageBodyWithSchema = (eventBody: any, schema: ParsedJsonSchema, key: string) => { // tslint:disable-line:no-any
if (key && !schema) { // DTDL doesn't contain corresponding key
const labelContent = t(ResourceKeys.deviceEvents.columns.validation.key.isNotSpecified, { key });
return (
<div className="column-value-text col-sm4">
<span aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
<Label className="value-validation-error">
{labelContent}
</Label>
</span>
</div>
);
}
if (eventBody && Object.keys(eventBody) && Object.keys(eventBody)[0] !== key) { // key in event body doesn't match property name
const labelContent = Object.keys(eventBody)[0] ? t(ResourceKeys.deviceEvents.columns.validation.key.doesNotMatch, {
expectedKey: key,
receivedKey: Object.keys(eventBody)[0]
}) : t(ResourceKeys.deviceEvents.columns.validation.value.bodyIsEmpty);
return (
<div className="column-value-text col-sm4">
<span aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
<Label className="value-validation-error">
{labelContent}
</Label>
</span>
</div>
);
}
return renderMessageBodyWithValueValidation(eventBody, schema, key);
};
const renderMessageBodyWithValueValidation = (eventBody: any, schema: ParsedJsonSchema, key: string) => { // tslint:disable-line:no-any
const errors = getSchemaValidationErrors(eventBody[key], schema, true);
return (
<div className="column-value-text col-sm4">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
{errors.length !== 0 &&
<section className="value-validation-error" aria-label={t(ResourceKeys.deviceEvents.columns.validation.value.label)}>
<span>{t(ResourceKeys.deviceEvents.columns.validation.value.label)}</span>
<ul>
{errors.map((element, index) =>
<li key={index}>{element.message}</li>
)}
</ul>
</section>
}
</Label>
</div>
);
};
const renderMessageBodyWithNoSchema = (eventBody: any) => { // tslint:disable-line:no-any
return (
<div className="column-value-text col-sm4">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
</Label>
</div>
);
};
const getModelDefinitionAndSchema = (key: string): TelemetrySchema => {
const matchingSchema = telemetrySchema.filter(schema => schema.telemetryModelDefinition.name === key);
const telemetryModelDefinition = matchingSchema && matchingSchema.length !== 0 && matchingSchema[0].telemetryModelDefinition;
const parsedSchema = matchingSchema && matchingSchema.length !== 0 && matchingSchema[0].parsedSchema;
return {
parsedSchema,
telemetryModelDefinition
const fetchData = () => {
client.onopen = () => { // intentionally blank
};
};
//#endregion
const renderLoader = (): JSX.Element => {
return (
<>
{monitoringData &&
<MessageBar
messageBarType={MessageBarType.info}
>
<Stack horizontal={true} tokens={{ childrenGap: 10 }}>
<div>{t(ResourceKeys.deviceEvents.infiniteScroll.loading)}</div>
{<Spinner size={SpinnerSize.small} />}
</Stack>
</MessageBar>}
</>
);
};
const fetchData = (startListeners: boolean) => () => {
let parameters: MonitorEventsParameters = {
consumerGroup,
decoderPrototype,
deviceId,
moduleId,
startListeners,
startTime
};
if (!useBuiltInEventHub) {
parameters = {
...parameters,
customEventHubConnectionString,
customEventHubName
customEventHubConnectionString
};
}
api.startEventsMonitoring(parameters);
};
@ -540,10 +204,8 @@ export const DeviceEvents: React.FC = () => {
onToggleContentTypePanel={onToggleContentTypePanel}
/>
<div className="device-events-container">
{renderLoader()}
<div className={componentName ? 'pnp-telemetry' : ''}>
{showPnpModeledEvents ? renderPnpModeledEvents() : renderRawEvents()}
</div>
<Loader monitoringData={monitoringData}/>
<EventsContent showPnpModeledEvents={showPnpModeledEvents} showSystemProperties={showSystemProperties}/>
</div>
</Stack>
);

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

@ -0,0 +1,172 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as React from 'react';
import { shallow, mount } from 'enzyme';
import { REPOSITORY_LOCATION_TYPE } from '../../../constants/repositoryLocationTypes';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
import * as pnpStateContext from '../../../shared/contexts/pnpStateContext';
import { ErrorBoundary } from '../../shared/components/errorBoundary';
import { pnpStateInitial, PnpStateInterface } from '../../pnp/state';
import { testModelDefinition } from '../../pnp/components/deviceEvents/testData';
import * as deviceEventsStateContext from '../context/deviceEventsStateContext';
import { getInitialDeviceEventsState } from '../state';
import { EventsContent } from './eventsContent';
const pathname = `#/devices/detail/events/?id=device1`;
jest.mock('react-router-dom', () => ({
useHistory: () => ({ push: jest.fn() }),
useLocation: () => ({ search: `?deviceId=device1`, pathname, push: jest.fn() })
}));
describe('EventsContent', () => {
describe('EventsContent in non-pnp context', () => {
const events = [{
body: {
humid: 123
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
properties: {
'iothub-message-schema': 'humid'
}
}];
it('matches snapshot', () => {
const startEventsMonitoring = jest.fn();
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events},
{...deviceEventsStateContext.getInitialDeviceEventsOps(), startEventsMonitoring}]);
expect(shallow(<EventsContent showPnpModeledEvents={false} showSystemProperties={false}/>)).toMatchSnapshot();
});
it('renders events', () => {
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: pnpStateInitial(), dispatch: jest.fn()});
const wrapper = mount(<EventsContent showPnpModeledEvents={false} showSystemProperties={false}/>);
const rawTelemetry = wrapper.find('article');
expect(rawTelemetry).toHaveLength(1);
});
});
describe('EventsContent in pnp context', () => {
const getModelDefinitionMock = jest.fn();
const mockFetchedState = () => {
const pnpState: PnpStateInterface = {
...pnpStateInitial(),
modelDefinitionWithSource: {
payload: {
isModelValid: true,
modelDefinition: testModelDefinition,
source: REPOSITORY_LOCATION_TYPE.Public,
},
synchronizationStatus: SynchronizationStatus.fetched
}
};
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({pnpState, dispatch: jest.fn(), getModelDefinition: getModelDefinitionMock});
};
it('matches snapshot while interface definition is retrieved', () => {
mockFetchedState();
expect(shallow(<EventsContent showPnpModeledEvents={true} showSystemProperties={false}/>)).toMatchSnapshot();
});
it('renders events which body\'s value type is wrong with expected columns', () => {
const events = [{
body: {
humid: '123' // intentionally set a value which type is double
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
systemProperties: {
'iothub-message-schema': 'humid'
}
}];
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = mount(<EventsContent showPnpModeledEvents={true} showSystemProperties={false}/>);
const errorBoundary = wrapper.find(ErrorBoundary);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('humid (Temperature)');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('double'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify(events[0].body, undefined, 2)); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[1].props['aria-label']).toEqual(ResourceKeys.deviceEvents.columns.validation.value.label); // tslint:disable-line:no-magic-numbers
});
it('renders events which body\'s key name is wrong with expected columns', () => {
const events = [{
body: {
'non-matching-key': 0
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
systemProperties: {
'iothub-message-schema': 'humid'
}
}];
mockFetchedState();
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = mount(<EventsContent showPnpModeledEvents={true} showSystemProperties={false}/>);
const errorBoundary = wrapper.find(ErrorBoundary);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('humid (Temperature)');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('double'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify(events[0].body, undefined, 2)); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[1].props.className).toEqual('value-validation-error'); // tslint:disable-line:no-magic-numbers
});
it('renders events when body is exploded and schema is not provided in system properties', () => {
const events = [{
body: {
'humid': 0,
'humid-foo': 'test'
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)',
systemProperties: {}
}];
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = shallow(<EventsContent showPnpModeledEvents={true} showSystemProperties={false}/>);
let errorBoundary = wrapper.find(ErrorBoundary).first();
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('humid (Temperature)');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('double'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify({humid: 0}, undefined, 2)); // tslint:disable-line:no-magic-numbers
errorBoundary = wrapper.find(ErrorBoundary).at(1);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('--');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('--'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[0]).toEqual(JSON.stringify({'humid-foo': 'test'}, undefined, 2)); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children[1].props.className).toEqual('value-validation-error'); // tslint:disable-line:no-magic-numbers
});
it('renders events which body\'s key name is not populated', () => {
const events = [{
body: {
'non-matching-key': 0
},
enqueuedTime: 'Wed Feb 17 2021 16:06:00 GMT-0800 (Pacific Standard Time)'
}];
jest.spyOn(deviceEventsStateContext, 'useDeviceEventsStateContext').mockReturnValue(
[{...getInitialDeviceEventsState(), message: events, formMode: 'fetched'},
{...deviceEventsStateContext.getInitialDeviceEventsOps()}]);
mockFetchedState();
const wrapper = shallow(<EventsContent showPnpModeledEvents={true} showSystemProperties={false}/>);
const errorBoundary = wrapper.find(ErrorBoundary);
expect(errorBoundary.children().at(1).props().children.props.children).toEqual('--');
expect(errorBoundary.children().at(2).props().children.props.children).toEqual('--'); // tslint:disable-line:no-magic-numbers
expect(errorBoundary.children().at(4).props().children.props.children).toEqual(JSON.stringify(events[0].body, undefined, 2)); // tslint:disable-line:no-magic-numbers
});
});
});

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

@ -0,0 +1,326 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Label } from '@fluentui/react';
import { useLocation } from 'react-router-dom';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import { Message, MESSAGE_SYSTEM_PROPERTIES, MESSAGE_PROPERTIES, IOTHUB_MESSAGE_SOURCE_TELEMETRY } from '../../../api/models/messages';
import { getComponentNameFromQueryString, getInterfaceIdFromQueryString } from '../../../shared/utils/queryStringHelper';
import { useDeviceEventsStateContext } from '../context/deviceEventsStateContext';
import { DEFAULT_COMPONENT_FOR_DIGITAL_TWIN } from '../../../constants/devices';
import { usePnpStateContext } from '../../../shared/contexts/pnpStateContext';
import { getDeviceTelemetry, TelemetrySchema } from '../../pnp/components/deviceEvents/dataHelper';
import { ErrorBoundary } from '../../shared/components/errorBoundary';
import { SemanticUnit } from '../../../shared/units/components/semanticUnit';
import { getSchemaType, getSchemaValidationErrors } from '../../../shared/utils/jsonSchemaAdaptor';
import { ParsedJsonSchema } from '../../../api/models/interfaceJsonParserOutput';
import { TelemetryContent } from '../../../api/models/modelDefinition';
import { getLocalizedData } from '../../../api/dataTransforms/modelDefinitionTransform';
import { AppInsightsClient } from '../../../shared/appTelemetry/appInsightsClient';
import { TELEMETRY_PAGE_NAMES } from '../../../../app/constants/telemetry';
const JSON_SPACES = 2;
interface EventsContentProps {
showSystemProperties: boolean;
showPnpModeledEvents: boolean;
}
export const EventsContent: React.FC<EventsContentProps> = ({showSystemProperties, showPnpModeledEvents}) => {
React.useEffect(
() => {
if (componentName) {
AppInsightsClient.getInstance()?.trackPageView({ name: TELEMETRY_PAGE_NAMES.PNP_TELEMETRY });
} else {
AppInsightsClient.getInstance()?.trackPageView({ name: TELEMETRY_PAGE_NAMES.DEVICE_TELEMETRY });
}
},
[]);
const { t } = useTranslation();
const { search } = useLocation();
const [ {message: events} ] = useDeviceEventsStateContext();
const { pnpState, } = usePnpStateContext();
const TELEMETRY_SCHEMA_PROP = MESSAGE_PROPERTIES.IOTHUB_MESSAGE_SCHEMA;
const componentName = getComponentNameFromQueryString(search); // if component name exist, we are in pnp context
const interfaceId = getInterfaceIdFromQueryString(search);
const modelDefinitionWithSource = pnpState.modelDefinitionWithSource.payload;
const modelDefinition = modelDefinitionWithSource && modelDefinitionWithSource.modelDefinition;
const telemetrySchema = React.useMemo(() => getDeviceTelemetry(modelDefinition), [modelDefinition]);
// tslint:disable-next-line: cyclomatic-complexity
const filterMessage = (message: Message) => {
if (!message || !message.systemProperties) {
return false;
}
if (message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_MESSAGE_SOURCE] &&
message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_MESSAGE_SOURCE].toLowerCase() !== IOTHUB_MESSAGE_SOURCE_TELEMETRY) {
// filter out telemetry sent from other sources
return false;
}
if (componentName === DEFAULT_COMPONENT_FOR_DIGITAL_TWIN) {
// for default component, we only expect ${IOTHUB_INTERFACE_ID} to be in the system property not ${IOTHUB_COMPONENT_NAME}
return message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_INTERFACE_ID] === interfaceId &&
!message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_COMPONENT_NAME];
}
return message.systemProperties[MESSAGE_SYSTEM_PROPERTIES.IOTHUB_COMPONENT_NAME] === componentName;
};
const renderRawEvents = () => {
const filteredEvents = componentName ? events?.filter(result => filterMessage(result)) : events;
return (
<>
{
filteredEvents && filteredEvents.map((event: Message, index) => {
const modifiedEvents = showSystemProperties ? event : {
body: event.body,
enqueuedTime: event.enqueuedTime,
properties: event.properties
};
return (
<article key={index} className="device-events-content">
{<h5>{modifiedEvents.enqueuedTime}:</h5>}
<pre>{JSON.stringify(modifiedEvents, undefined, JSON_SPACES)}</pre>
</article>
);
})
}
</>
);
};
//#region pnp specific render
const renderPnpModeledEvents = () => {
const filteredEvents = componentName ? events.filter(result => filterMessage(result)) : events;
return (
<>
{
filteredEvents && filteredEvents.length > 0 &&
<>
<div className="pnp-modeled-list">
<div className="list-header list-header-uncollapsible flex-grid-row">
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.timestamp)}</span>
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.displayName)}</span>
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.schema)}</span>
<span className="col-sm2">{t(ResourceKeys.deviceEvents.columns.unit)}</span>
<span className="col-sm4">{t(ResourceKeys.deviceEvents.columns.value)}</span>
</div>
</div>
<section role="feed">
{
filteredEvents.map((event: Message, index) => {
return !event.systemProperties ? renderEventsWithNoSystemProperties(event, index) :
event.systemProperties[TELEMETRY_SCHEMA_PROP] ?
renderEventsWithSchemaProperty(event, index) :
renderEventsWithNoSchemaProperty(event, index);
})
}
</section>
</>
}
</>
);
};
const renderEventsWithNoSystemProperties = (event: Message, index: number) => {
return (
<article className="list-item event-list-item" role="article" key={index}>
<section className="flex-grid-row item-summary">
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(event.enqueuedTime)}
{renderEventName()}
{renderEventSchema()}
{renderEventUnit()}
{renderMessageBodyWithNoSchema(event.body)}
</ErrorBoundary>
</section>
</article>
);
};
const renderEventsWithSchemaProperty = (event: Message, index: number) => {
const { telemetryModelDefinition, parsedSchema } = getModelDefinitionAndSchema(event.systemProperties[TELEMETRY_SCHEMA_PROP]);
return (
<article className="list-item event-list-item" role="article" key={index}>
<section className="flex-grid-row item-summary">
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(event.enqueuedTime)}
{renderEventName(telemetryModelDefinition)}
{renderEventSchema(telemetryModelDefinition)}
{renderEventUnit(telemetryModelDefinition)}
{renderMessageBodyWithSchema(event.body, parsedSchema, event.systemProperties[TELEMETRY_SCHEMA_PROP])}
</ErrorBoundary>
</section>
</article>
);
};
const renderEventsWithNoSchemaProperty = (event: Message, index: number) => {
const telemetryKeys = Object.keys(event.body);
if (telemetryKeys && telemetryKeys.length !== 0) {
return telemetryKeys.map((key, keyIndex) => {
const { telemetryModelDefinition, parsedSchema } = getModelDefinitionAndSchema(key);
const partialEventBody: any = {}; // tslint:disable-line:no-any
partialEventBody[key] = event.body[key];
const isNotItemLast = keyIndex !== telemetryKeys.length - 1;
return (
<article className="list-item event-list-item" role="article" key={key + index}>
<section className={`flex-grid-row item-summary ${isNotItemLast && 'item-summary-partial'}`}>
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(keyIndex === 0 ? event.enqueuedTime : null)}
{renderEventName(telemetryModelDefinition)}
{renderEventSchema(telemetryModelDefinition)}
{renderEventUnit(telemetryModelDefinition)}
{renderMessageBodyWithSchema(partialEventBody, parsedSchema, key)}
</ErrorBoundary>
</section>
</article>
);
});
}
return (
<article className="list-item event-list-item" role="article" key={index}>
<section className="flex-grid-row item-summary">
<ErrorBoundary error={t(ResourceKeys.errorBoundary.text)}>
{renderTimestamp(event.enqueuedTime)}
{renderEventName()}
{renderEventSchema()}
{renderEventUnit()}
{renderMessageBodyWithSchema(event.body, null, null)}
</ErrorBoundary>
</section>
</article>
);
};
const renderTimestamp = (enqueuedTime: string) => {
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.timestamp)}>
{enqueuedTime}
</Label>
</div>
);
};
const renderEventName = (telemetryModelDefinition?: TelemetryContent) => {
const displayName = telemetryModelDefinition ? getLocalizedData(telemetryModelDefinition.displayName) : '';
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.displayName)}>
{telemetryModelDefinition ?
`${telemetryModelDefinition.name} (${displayName ? displayName : '--'})` : '--'}
</Label>
</div>
);
};
const renderEventSchema = (telemetryModelDefinition?: TelemetryContent) => {
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.schema)}>
{telemetryModelDefinition ? getSchemaType(telemetryModelDefinition.schema) : '--'}
</Label>
</div>
);
};
const renderEventUnit = (telemetryModelDefinition?: TelemetryContent) => {
return (
<div className="col-sm2">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.unit)}>
<SemanticUnit unitHost={telemetryModelDefinition} />
</Label>
</div>
);
};
// tslint:disable-next-line: cyclomatic-complexity
const renderMessageBodyWithSchema = (eventBody: any, schema: ParsedJsonSchema, key: string) => { // tslint:disable-line:no-any
if (key && !schema) { // DTDL doesn't contain corresponding key
const labelContent = t(ResourceKeys.deviceEvents.columns.validation.key.isNotSpecified, { key });
return (
<div className="column-value-text col-sm4">
<span aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
<Label className="value-validation-error">
{labelContent}
</Label>
</span>
</div>
);
}
if (eventBody && Object.keys(eventBody) && Object.keys(eventBody)[0] !== key) { // key in event body doesn't match property name
const labelContent = Object.keys(eventBody)[0] ? t(ResourceKeys.deviceEvents.columns.validation.key.doesNotMatch, {
expectedKey: key,
receivedKey: Object.keys(eventBody)[0]
}) : t(ResourceKeys.deviceEvents.columns.validation.value.bodyIsEmpty);
return (
<div className="column-value-text col-sm4">
<span aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
<Label className="value-validation-error">
{labelContent}
</Label>
</span>
</div>
);
}
return renderMessageBodyWithValueValidation(eventBody, schema, key);
};
const renderMessageBodyWithValueValidation = (eventBody: any, schema: ParsedJsonSchema, key: string) => { // tslint:disable-line:no-any
const errors = getSchemaValidationErrors(eventBody[key], schema, true);
return (
<div className="column-value-text col-sm4">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
{errors.length !== 0 &&
<section className="value-validation-error" aria-label={t(ResourceKeys.deviceEvents.columns.validation.value.label)}>
<span>{t(ResourceKeys.deviceEvents.columns.validation.value.label)}</span>
<ul>
{errors.map((element, index) =>
<li key={index}>{element.message}</li>
)}
</ul>
</section>
}
</Label>
</div>
);
};
const renderMessageBodyWithNoSchema = (eventBody: any) => { // tslint:disable-line:no-any
return (
<div className="column-value-text col-sm4">
<Label aria-label={t(ResourceKeys.deviceEvents.columns.value)}>
{JSON.stringify(eventBody, undefined, JSON_SPACES)}
</Label>
</div>
);
};
const getModelDefinitionAndSchema = (key: string): TelemetrySchema => {
const matchingSchema = telemetrySchema.filter(schema => schema.telemetryModelDefinition.name === key);
const telemetryModelDefinition = matchingSchema && matchingSchema.length !== 0 && matchingSchema[0].telemetryModelDefinition;
const parsedSchema = matchingSchema && matchingSchema.length !== 0 && matchingSchema[0].parsedSchema;
return {
parsedSchema,
telemetryModelDefinition
};
};
//#endregion
return (
<div className={componentName ? 'pnp-telemetry' : ''}>
{showPnpModeledEvents ? renderPnpModeledEvents() : renderRawEvents()}
</div>
);
};

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

@ -0,0 +1,18 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as React from 'react';
import { shallow, mount } from 'enzyme';
import { Loader } from './loader';
describe('Loader', () => {
it('matches snapshot when loading', () => {
expect(shallow(<Loader monitoringData={true}/>)).toMatchSnapshot();
});
it('matches snapshot when not loading', () => {
expect(shallow(<Loader monitoringData={false}/>)).toMatchSnapshot();
});
});

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

@ -0,0 +1,27 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Spinner, SpinnerSize, Stack, MessageBarType, MessageBar } from '@fluentui/react';
import { ResourceKeys } from '../../../../localization/resourceKeys';
export const Loader: React.FC<{monitoringData: boolean}> = ({monitoringData}) => {
const { t } = useTranslation();
return (
<>
{monitoringData &&
<MessageBar
messageBarType={MessageBarType.info}
>
<Stack horizontal={true} tokens={{ childrenGap: 10 }}>
<div>{t(ResourceKeys.deviceEvents.infiniteScroll.loading)}</div>
{<Spinner size={SpinnerSize.small} />}
</Stack>
</MessageBar>
}
</>
);
};

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

@ -15,6 +15,7 @@ exports[`DeviceEventsStateContextProvider matches snapshot 1`] = `
"clearEventsMonitoring": [Function],
"setDecoderInfo": [Function],
"setDefaultDecodeInfo": [Function],
"setEvents": [Function],
"startEventsMonitoring": [Function],
"stopEventsMonitoring": [Function],
},

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

@ -10,6 +10,7 @@ export const getInitialDeviceEventsOps = (): DeviceEventsInterface => ({
clearEventsMonitoring: () => undefined,
setDecoderInfo: () => undefined,
setDefaultDecodeInfo: () => undefined,
setEvents: () => undefined,
startEventsMonitoring: () => undefined,
stopEventsMonitoring: () => undefined
});

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

@ -6,15 +6,17 @@ import * as React from 'react';
import { getInitialDeviceEventsState } from '../state';
import { deviceEventsReducer } from '../reducers';
import { DeviceEventsStateContext } from './deviceEventsStateContext';
import { startEventsMonitoringAction, stopEventsMonitoringAction, clearMonitoringEventsAction, setDecoderInfoAction, setDefaultDecodeInfoAction } from '../actions';
import { startEventsMonitoringAction, stopEventsMonitoringAction, clearMonitoringEventsAction, setDecoderInfoAction, setDefaultDecodeInfoAction, setEventsMessagesAction } from '../actions';
import { EventMonitoringSaga } from '../saga';
import { useAsyncSagaReducer } from '../../../shared/hooks/useAsyncSagaReducer';
import { MonitorEventsParameters, SetDecoderInfoParameters } from '../../../api/parameters/deviceParameters';
import { Message } from '../../../api/models/messages';
export interface DeviceEventsInterface {
clearEventsMonitoring(): void;
setDecoderInfo(params: SetDecoderInfoParameters): void;
setDefaultDecodeInfo(): void;
setEvents(messages: Message[]): void;
startEventsMonitoring(params: MonitorEventsParameters): void;
stopEventsMonitoring(): void;
}
@ -26,6 +28,7 @@ export const DeviceEventsStateContextProvider: React.FC = props => {
clearEventsMonitoring: () => dispatch(clearMonitoringEventsAction()),
setDecoderInfo: (params: SetDecoderInfoParameters) => dispatch(setDecoderInfoAction.started(params)),
setDefaultDecodeInfo: () => dispatch(setDefaultDecodeInfoAction()),
setEvents: (messages: Message[]) => dispatch(setEventsMessagesAction.started(messages)),
startEventsMonitoring: (params: MonitorEventsParameters) => dispatch(startEventsMonitoringAction.started(params)),
stopEventsMonitoring: () => dispatch(stopEventsMonitoringAction.started())
};

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

@ -3,8 +3,8 @@
* Licensed under the MIT License
**********************************************************/
import 'jest';
import { SET_DECODE_INFO, SET_DEFAULT_DECODE_INFO, START_EVENTS_MONITORING, STOP_EVENTS_MONITORING } from '../../constants/actionTypes';
import { setDecoderInfoAction, setDefaultDecodeInfoAction, startEventsMonitoringAction, stopEventsMonitoringAction } from './actions';
import { SET_DECODE_INFO, SET_DEFAULT_DECODE_INFO, SET_EVENTS_MESSAGES, START_EVENTS_MONITORING, STOP_EVENTS_MONITORING } from '../../constants/actionTypes';
import { setDecoderInfoAction, setDefaultDecodeInfoAction, setEventsMessagesAction, startEventsMonitoringAction, stopEventsMonitoringAction } from './actions';
import { deviceEventsReducer } from './reducers';
import { getInitialDeviceEventsState } from './state';
import { DEFAULT_CONSUMER_GROUP } from './../../constants/apiConstants';
@ -12,7 +12,7 @@ import { Type } from 'protobufjs';
describe('deviceEventsReducer', () => {
const deviceId = 'testDeviceId';
const params = {consumerGroup: DEFAULT_CONSUMER_GROUP, deviceId, moduleId:undefined, startTime: new Date()};
const params = {consumerGroup: DEFAULT_CONSUMER_GROUP, deviceId, moduleId:'', startTime: new Date()};
const events = [{
body: {
humid: '123' // intentionally set a value which type is double
@ -23,14 +23,14 @@ describe('deviceEventsReducer', () => {
}
}];
const decoderParams = {decoderFile: new File([], ''), decoderPrototype: 'decoderPrototype', decodeType: 'Protobuf'};
it (`handles ${START_EVENTS_MONITORING}/ACTION_START action`, () => {
const action = startEventsMonitoringAction.started(params);
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('working');
});
it (`handles ${START_EVENTS_MONITORING}/ACTION_DONE action`, () => {
const action = startEventsMonitoringAction.done({params, result: events});
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).message).toEqual(events);
const action = startEventsMonitoringAction.done({params});
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('fetched');
});
@ -42,19 +42,16 @@ describe('deviceEventsReducer', () => {
it (`handles ${STOP_EVENTS_MONITORING}/ACTION_START action`, () => {
const action = stopEventsMonitoringAction.started();
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('updating');
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).message).toEqual([]);
});
it (`handles ${STOP_EVENTS_MONITORING}/ACTION_DONE action`, () => {
const action = stopEventsMonitoringAction.done({});
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('upserted');
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).message).toEqual([]);
});
it (`handles ${STOP_EVENTS_MONITORING}/ACTION_FAILED action`, () => {
const action = stopEventsMonitoringAction.failed({error: -1});
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('failed');
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).message).toEqual([]);
});
it (`handles ${SET_DECODE_INFO}/ACTION_START action`, () => {
@ -76,4 +73,22 @@ describe('deviceEventsReducer', () => {
const action = setDefaultDecodeInfoAction();
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).contentType.decodeType).toEqual('JSON');
});
it (`handles ${SET_EVENTS_MESSAGES}/ACTION_START action`, () => {
const action = setEventsMessagesAction.started(events);
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('working');
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).message).toEqual([]);
});
it (`handles ${SET_EVENTS_MESSAGES}/ACTION_DONE action`, () => {
const action = setEventsMessagesAction.done({params: events, result: events});
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('fetched');
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).message).toEqual(events);
});
it (`handles ${SET_EVENTS_MESSAGES}/ACTION_FAILED action`, () => {
const action = setEventsMessagesAction.failed({error: -1, params: events});
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).formMode).toEqual('failed');
expect(deviceEventsReducer(getInitialDeviceEventsState(), action).message).toEqual([]);
});
});

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

@ -9,7 +9,8 @@ import {
stopEventsMonitoringAction,
clearMonitoringEventsAction,
setDecoderInfoAction,
setDefaultDecodeInfoAction
setDefaultDecodeInfoAction,
setEventsMessagesAction
} from './actions';
import { MonitorEventsParameters, SetDecoderInfoParameters } from '../../api/parameters/deviceParameters';
import { Message } from '../../api/models/messages';
@ -21,17 +22,10 @@ export const deviceEventsReducer = reducerWithInitialState<DeviceEventsStateInte
formMode: 'working'
};
})
.case(startEventsMonitoringAction.done, (state: DeviceEventsStateInterface, payload: {params: MonitorEventsParameters, result: Message[]}) => {
const messages = payload.result ? payload.result.reverse().map((message: Message) => message) : [];
let filteredMessages = messages;
if (state.message.length > 0 && messages.length > 0) {
// filter overlaped messages returned from event hub
filteredMessages = messages.filter(message => message.enqueuedTime > state.message[0].enqueuedTime);
}
.case(startEventsMonitoringAction.done, (state: DeviceEventsStateInterface, payload: {params: MonitorEventsParameters}) => {
return {
...state,
formMode: 'fetched',
message: [...filteredMessages, ...state.message]
};
})
.case(startEventsMonitoringAction.failed, (state: DeviceEventsStateInterface) => {
@ -94,4 +88,28 @@ export const deviceEventsReducer = reducerWithInitialState<DeviceEventsStateInte
decodeType: 'JSON'
}
};
})
.case(setEventsMessagesAction.started, (state: DeviceEventsStateInterface) => {
return {
...state,
formMode: 'working'
};
})
.case(setEventsMessagesAction.done, (state: DeviceEventsStateInterface, payload: { params: Message[], result: Message[]}) => {
let messages = payload.result ? payload.result.reverse().map((message: Message) => message) : [];
if (state.message.length > 0 && messages.length > 0) {
// filter overlapped messages returned from event hub
messages = messages.filter(message => message.enqueuedTime > state.message[0].enqueuedTime);
}
return {
...state,
formMode: 'fetched',
message: [...messages, ...state.message]
};
})
.case(setEventsMessagesAction.failed, (state: DeviceEventsStateInterface) => {
return {
...state,
formMode: 'failed'
};
});

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

@ -19,11 +19,11 @@ import { Type } from 'protobufjs';
describe('deviceMonitoringSaga', () => {
let startEventsMonitoringSagaGenerator;
const mockMonitorEventsFn = jest.spyOn(DevicesService, 'monitorEvents').mockImplementationOnce(parameters => {
const mockMonitorEventsFn = jest.spyOn(DevicesService, 'monitorEvents').mockImplementationOnce(() => {
return null;
});
const deviceId = 'test_id';
const params = {consumerGroup: DEFAULT_CONSUMER_GROUP, deviceId, startTime: new Date()};
const params = {consumerGroup: DEFAULT_CONSUMER_GROUP, deviceId, moduleId: ''};
beforeEach(() => {
startEventsMonitoringSagaGenerator = cloneableGenerator(startEventsMonitoringSagaWorker)(startEventsMonitoringAction.started(params));
@ -40,8 +40,7 @@ describe('deviceMonitoringSaga', () => {
expect(startEventsMonitoringSagaGenerator.next([])).toEqual({
done: false,
value: put(startEventsMonitoringAction.done({
params,
result: []
params
}))
});
@ -83,7 +82,7 @@ describe('deviceMonitoringSaga', () => {
describe('setDecoderInfoSaga', () => {
let setDecoderInfoSagaGenerator;
const params = {decoderFile: new File([], ''), decoderPrototype: 'decoderPrototype', decodeType: 'Protobuf'};
beforeEach(() => {
setDecoderInfoSagaGenerator = cloneableGenerator(setDecoderInfoSagaWorker)(setDecoderInfoAction.started(params));
});

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

@ -2,22 +2,33 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { call, put, all, takeEvery, takeLatest } from 'redux-saga/effects';
import { call, put, all, takeEvery, takeLatest, select } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
import { Action } from 'typescript-fsa';
import { Type } from 'protobufjs';
import { monitorEvents, stopMonitoringEvents } from '../../api/services/devicesService';
import { monitorEvents, parseEvents, stopMonitoringEvents } from '../../api/services/devicesService';
import { NotificationType } from '../../api/models/notification';
import { ResourceKeys } from '../../../localization/resourceKeys';
import { setDecoderInfoAction, startEventsMonitoringAction, stopEventsMonitoringAction } from './actions';
import { setDecoderInfoAction, setEventsMessagesAction, startEventsMonitoringAction, stopEventsMonitoringAction } from './actions';
import { raiseNotificationToast } from '../../notifications/components/notificationToast';
import { MonitorEventsParameters, SetDecoderInfoParameters } from '../../api/parameters/deviceParameters';
import { setDecoderInfo } from './utils';
import { Message } from '../../api/models/messages';
export function* setEventsSagaWorker(action: Action<Message[]>): SagaIterator {
try {
const state = yield select();
const result = yield call(parseEvents, {messages: action.payload, decoderPrototype: state.contentType.decoderPrototype});
yield put(setEventsMessagesAction.done({params: action.payload, result}));
} catch (error) {
yield put(setEventsMessagesAction.failed({params: action.payload, error}));
}
}
export function* startEventsMonitoringSagaWorker(action: Action<MonitorEventsParameters>): SagaIterator {
try {
const messages = yield call(monitorEvents, action.payload);
yield put(startEventsMonitoringAction.done({params: action.payload, result: messages}));
yield call(monitorEvents, action.payload);
yield put(startEventsMonitoringAction.done({params: action.payload}));
} catch (error) {
yield call(raiseNotificationToast, {
text: {
@ -72,6 +83,7 @@ export function* EventMonitoringSaga() {
yield all([
takeEvery(startEventsMonitoringAction.started, startEventsMonitoringSagaWorker),
takeLatest(stopEventsMonitoringAction.started, stopEventsMonitoringSagaWorker),
takeLatest(setDecoderInfoAction.started, setDecoderInfoSagaWorker)
takeLatest(setDecoderInfoAction.started, setDecoderInfoSagaWorker),
takeLatest(setEventsMessagesAction.started, setEventsSagaWorker)
]);
}

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

@ -578,9 +578,6 @@
"label": "Custom event hub connection string",
"placeHolder": "Endpoint=sb://<FQDN>/;SharedAccessKeyName=<KeyName>;SharedAccessKey=<KeyValue>",
"error": "Event hub connection string format should look like 'Endpoint=sb://<FQDN>/;SharedAccessKeyName=<KeyName>;SharedAccessKey=<KeyValue>'"
},
"name": {
"label": "Custom event hub name"
}
},
"headerText": "Telemetry",

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

@ -336,9 +336,6 @@ export class ResourceKeys {
label : "deviceEvents.customEventHub.connectionString.label",
placeHolder : "deviceEvents.customEventHub.connectionString.placeHolder",
},
name : {
label : "deviceEvents.customEventHub.name.label",
},
},
customizeContentType : {
contentTypeOption : {

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

@ -28,16 +28,6 @@ describe('serverBase', () => {
});
});
context('handlhandleCloudToDevicePostRequesteDataPlanePostRequest', () => {
it('returns 400 if body is not provided', async () => {
const req = mockRequest();
const res = mockResponse();
await ServerBase.handleCloudToDevicePostRequest(req, res);
expect(res.status).toHaveBeenCalledWith(400); // tslint:disable-line:no-magic-numbers
});
});
describe('handleEventHubMonitorPostRequest', () => {
it('returns 400 if body is not provided', async () => {
const req = mockRequest();
@ -46,16 +36,6 @@ describe('serverBase', () => {
await ServerBase.handleEventHubMonitorPostRequest(req, res);
expect(res.status).toHaveBeenCalledWith(400); // tslint:disable-line:no-magic-numbers
});
it('calls eventHubProvider when body is provided', async () => {
const req = mockRequest({params: {}});
const res = mockResponse();
const promise = {then: jest.fn()} as any; // tslint:disable-line:no-any
jest.spyOn(ServerBase, 'eventHubProvider').mockReturnValue(promise);
await ServerBase.handleEventHubMonitorPostRequest(req, res);
expect(ServerBase.eventHubProvider).toBeCalled();
});
});
context('handleModelRepoPostRequest', () => {
@ -73,88 +53,4 @@ describe('serverBase', () => {
expect(res.status).toHaveBeenCalledWith(500); // tslint:disable-line:no-magic-numbers
});
});
context('addPropertiesToCloudToDeviceMessage', () => {
it('add system properties to message', async () => {
const message: any = {properties: new Map()}; // tslint:disable-line:no-any
const properties = [
{
isSystemProperty: true,
key: 'ack',
value: '1',
},
{
isSystemProperty: true,
key: 'contentType',
value: 'json',
},
{
isSystemProperty: true,
key: 'correlationId',
value: '2',
},
{
isSystemProperty: true,
key: 'contentEncoding',
value: '3',
},
{
isSystemProperty: true,
key: 'expiryTimeUtc',
value: '4',
},
{
isSystemProperty: true,
key: 'messageId',
value: '5',
},
{
isSystemProperty: true,
key: 'lockToken',
value: '6',
}
];
ServerBase.addPropertiesToCloudToDeviceMessage(message, properties);
expect(message.ack).toEqual('1');
expect(message.contentType).toEqual('json');
expect(message.correlationId).toEqual('2');
expect(message.contentEncoding).toEqual('3');
expect(message.expiryTimeUtc).toEqual(4); // tslint:disable-line:no-magic-numbers
expect(message.messageId).toEqual('5');
expect(message.lockToken).toEqual('6');
});
});
context('findMatchingFile', () => {
const model = {
'@id': 'urn:azureiot:ModelDiscovery:ModelInformation;1',
'@type': 'Interface',
'displayName': 'Digital Twin Client SDK Information',
'contents': [
{
'@type': 'Property',
'name': 'language',
'displayName': 'SDK Language',
'schema': 'string',
'description': 'The language for the Digital Twin client SDK. For example, Java.'
}
],
'@context': 'http://azureiot.com/v1/contexts/IoTModel.json'
};
it('returns null when @id and expected file name does not match', async () => {
jest.spyOn(ServerBase, 'readFileFromLocal').mockReturnValue(JSON.stringify(model));
expect(ServerBase.findMatchingFile('', ['test.json'], 'urn:azureiot:test:ModelInformation;1')).toEqual(null);
});
it('returns model when @id and expected file name matches', async () => {
jest.spyOn(ServerBase, 'readFileFromLocal').mockReturnValue(JSON.stringify(model));
expect(ServerBase.findMatchingFile('', ['test.json'], 'urn:azureiot:ModelDiscovery:ModelInformation;1')).toEqual(JSON.stringify(model));
});
it('returns model when @id and expected file name matches and model is an array', async () => {
jest.spyOn(ServerBase, 'readFileFromLocal').mockReturnValue(JSON.stringify([model]));
expect(ServerBase.findMatchingFile('', ['test.json'], 'urn:azureiot:ModelDiscovery:ModelInformation;1')).toEqual(model);
});
});
});

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

@ -4,23 +4,26 @@
**********************************************************/
// this file is the legacy controller for local development, until we move server side code to use electron's IPC pattern and enable electron hot reloading
import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
import * as WebSocket from 'ws';
import express = require('express');
import request = require('request');
import bodyParser = require('body-parser');
import cors = require('cors');
import { Client as HubClient } from 'azure-iothub';
import { Message as CloudToDeviceMessage } from 'azure-iot-common';
import { EventHubClient, EventPosition, ReceiveHandler } from '@azure/event-hubs';
import { EventHubConsumerClient, Subscription, ReceivedEventData } from '@azure/event-hubs';
import { generateDataPlaneRequestBody, generateDataPlaneResponse } from './dataPlaneHelper';
import { convertIotHubToEventHubsConnectionString } from './eventHubHelper';
import { fetchDirectories, fetchDrivesOnWindows, findMatchingFile } from './utils';
const SERVER_ERROR = 500;
export const SERVER_ERROR = 500;
export const SUCCESS = 200;
const BAD_REQUEST = 400;
const SUCCESS = 200;
const NOT_FOUND = 404;
const NO_CONTENT_SUCCESS = 204;
let wss: WebSocket.Server;
let ws: WebSocket.WebSocket;
interface Message {
body: any; // tslint:disable-line:no-any
enqueuedTime: string;
@ -43,18 +46,32 @@ export class ServerBase {
}));
app.post(dataPlaneUri, handleDataPlanePostRequest);
app.post(cloudToDeviceUri, handleCloudToDevicePostRequest);
app.post(eventHubMonitorUri, handleEventHubMonitorPostRequest);
app.post(eventHubStopUri, handleEventHubStopPostRequest);
app.post(modelRepoUri, handleModelRepoPostRequest);
app.get(readFileUri, handleReadFileRequest);
app.get(getDirectoriesUri, handleGetDirectoriesRequest);
app.listen(this.port).on('error', () => { throw new Error(
//initialize a simple http server
const server = http.createServer(app);
//start the server
server.listen(this.port).on('error', () => { throw new Error(
`Failed to start the app on port ${this.port} as it is in use.
You can still view static pages, but requests cannot be made to the services if the port is still occupied.
To get around with the issue, configure a custom port by setting the system environment variable 'AZURE_IOT_EXPLORER_PORT' to an available port number.
To learn more, please visit https://github.com/Azure/azure-iot-explorer/wiki/FAQ`); });
//initialize the WebSocket server instance
wss = new WebSocket.Server({ server });
wss.on('connection', (_ws: WebSocket) => {
if (_ws && _ws.readyState === WebSocket.OPEN) {
ws = _ws;
}
else {
ws = null;
}
});
}
}
@ -89,49 +106,6 @@ export const handleReadFileRequest = (req: express.Request, res: express.Respons
}
};
// tslint:disable-next-line:cyclomatic-complexity
export const findMatchingFile = (filePath: string, fileNames: string[], expectedFileName: string): string => {
const filesWithParsingError = [];
for (const fileName of fileNames) {
if (isFileExtensionJson(fileName)) {
try {
const data = readFileFromLocal(filePath, fileName);
const parsedData = JSON.parse(data);
if (parsedData) {
if (Array.isArray(parsedData)) { // if parsedData is array, it is using expanded dtdl format
for (const pd of parsedData) {
if (pd['@id']?.toString() === expectedFileName) {
return pd;
}
}
}
else {
if (parsedData['@id']?.toString() === expectedFileName) {
return data;
}
}
}
}
catch (error) {
filesWithParsingError.push(`${fileName}: ${error.message}`); // swallow error and continue the loop
}
}
}
if (filesWithParsingError.length > 0) {
throw new Error(filesWithParsingError.join(', '));
}
return null;
};
export const readFileFromLocal = (filePath: string, fileName: string) => {
return fs.readFileSync(`${filePath}/${fileName}`, 'utf-8');
}
const isFileExtensionJson = (fileName: string) => {
const i = fileName.lastIndexOf('.');
return i > 0 && fileName.substr(i) === '.json';
};
const getDirectoriesUri = '/api/Directories/:path';
export const handleGetDirectoriesRequest = (req: express.Request, res: express.Response) => {
try {
@ -148,34 +122,6 @@ export const handleGetDirectoriesRequest = (req: express.Request, res: express.R
}
};
const fetchDrivesOnWindows = (res: express.Response) => {
const exec = require('child_process').exec;
exec('wmic logicaldisk get name', (error: any, stdout: any, stderr: any) => { // tslint:disable-line:no-any
if (!error && !stderr) {
res.status(SUCCESS).send(stdout);
}
else {
res.status(SERVER_ERROR).send();
}
});
};
const fetchDirectories = (dir: string, res: express.Response) => {
const result: string[] = [];
for (const item of fs.readdirSync(dir)) {
try {
const stat = fs.statSync(path.join(dir, item));
if (stat.isDirectory()) {
result.push(item);
}
}
catch {
// some item cannot be checked by isDirectory(), swallow error and continue the loop
}
}
res.status(SUCCESS).send(result);
};
const dataPlaneUri = '/api/DataPlane';
export const handleDataPlanePostRequest = (req: express.Request, res: express.Response) => {
try {
@ -196,52 +142,19 @@ export const handleDataPlanePostRequest = (req: express.Request, res: express.Re
}
};
const cloudToDeviceUri = '/api/CloudToDevice';
export const handleCloudToDevicePostRequest = (req: express.Request, res: express.Response) => {
try {
if (!req.body) {
res.status(BAD_REQUEST).send();
}
else {
const hubClient = HubClient.fromConnectionString(req.body.connectionString);
hubClient.open(() => {
const message = new CloudToDeviceMessage(req.body.body);
addPropertiesToCloudToDeviceMessage(message, req.body.properties);
hubClient.send(req.body.deviceId, message, (err, result) => {
if (err) {
res.status(SERVER_ERROR).send(err);
} else {
res.status(SUCCESS).send(result);
}
hubClient.close();
});
});
}
}
catch (error) {
res.status(SERVER_ERROR).send(error);
}
};
const eventHubMonitorUri = '/api/EventHub/monitor';
const IOTHUB_CONNECTION_DEVICE_ID = 'iothub-connection-device-id';
const IOTHUB_CONNECTION_MODULE_ID = 'iothub-connection-module-id';
let client: EventHubClient = null;
let messages: Message[] = [];
let receivers: ReceiveHandler[] = [];
let connectionString: string = ''; // would equal `${hubConnectionString}` or `${customEventHubConnectionString}/${customEventHubName}`
let deviceId: string = '';
let moduleId: string = '';
let client: EventHubConsumerClient = null;
let subscription: Subscription = null;
export const handleEventHubMonitorPostRequest = (req: express.Request, res: express.Response) => {
try {
if (!req.body) {
res.status(BAD_REQUEST).send();
return;
}
eventHubProvider(req.body).then(result => {
res.status(SUCCESS).send(result);
initializeEventHubClient(req.body).then(() => {
res.status(SUCCESS).send([]);
});
} catch (error) {
res.status(SERVER_ERROR).send(error);
@ -256,8 +169,9 @@ export const handleEventHubStopPostRequest = (req: express.Request, res: express
return;
}
stopClient();
res.status(SUCCESS).send();
stopClient().then(() => {
res.status(SUCCESS).send();
});
} catch (error) {
res.status(SERVER_ERROR).send(error);
}
@ -285,172 +199,54 @@ export const handleModelRepoPostRequest = (req: express.Request, res: express.Re
}
});
} catch (error) {
stopReceivers();
res.status(SERVER_ERROR).send(error);
}
};
// tslint:disable-next-line:cyclomatic-complexity
export const addPropertiesToCloudToDeviceMessage = (message: CloudToDeviceMessage, properties: Array<{key: string, value: string, isSystemProperty: boolean}>) => {
if (!properties || properties.length === 0) {
return;
}
for (const property of properties) {
if (property.isSystemProperty) {
switch (property.key) {
case 'ack':
message.ack = property.value;
break;
case 'contentType':
message.contentType = property.value as any; // tslint:disable-line:no-any
break;
case 'correlationId':
message.correlationId = property.value;
break;
case 'contentEncoding':
message.contentEncoding = property.value as any; // tslint:disable-line:no-any
break;
case 'expiryTimeUtc':
message.expiryTimeUtc = parseInt(property.value); // tslint:disable-line:radix
break;
case 'messageId':
message.messageId = property.value;
break;
case 'lockToken':
message.lockToken = property.value;
break;
default:
message.properties.add(property.key, property.value);
break;
}
}
else {
message.properties.add(property.key, property.value);
}
}
};
export const eventHubProvider = async (params: any) => {
await initializeEventHubClient(params);
updateEntityIdIfNecessary(params);
return listeningToMessages(params);
};
const initializeEventHubClient = async (params: any) => {
if (needToCreateNewEventHubClient(params))
{
// hub has changed, reinitialize client, receivers and messages
if (params.customEventHubConnectionString) {
client = await EventHubClient.createFromConnectionString(params.customEventHubConnectionString, params.customEventHubName);
}
else {
client = await EventHubClient.createFromConnectionString(await convertIotHubToEventHubsConnectionString(params.hubConnectionString));
}
connectionString = params.customEventHubConnectionString ?
`${params.customEventHubConnectionString}/${params.customEventHubName}` :
params.hubConnectionString;
receivers = [];
messages = [];
if (params.customEventHubConnectionString) {
client = new EventHubConsumerClient(params.consumerGroup, params.customEventHubConnectionString);
}
};
const listeningToMessages = async (params: any) => {
if (params.startListeners || !receivers) {
const partitionIds = await client.getPartitionIds();
const hubInfo = await client.getHubRuntimeInformation();
const startTime = params.startTime ? Date.parse(params.startTime) : Date.now();
partitionIds && partitionIds.forEach(async (partitionId: string) => {
const receiveOptions = {
consumerGroup: params.consumerGroup,
enableReceiverRuntimeMetric: true,
eventPosition: EventPosition.fromEnqueuedTime(startTime),
name: `${hubInfo.path}_${partitionId}`,
};
const receiver = client.receive(
partitionId,
onMessageReceived,
(err: object) => {},
receiveOptions);
receivers.push(receiver);
});
else {
client = new EventHubConsumerClient(params.consumerGroup, await convertIotHubToEventHubsConnectionString(params.hubConnectionString));
}
return handleMessages();
};
const handleMessages = () => {
let results: Message[] = [];
messages.forEach(message => {
if (!results.some(result => result.systemProperties?.['x-opt-sequence-number'] === message.systemProperties?.['x-opt-sequence-number'])) {
// if user click stop/start too frequently, it's possible duplicate receivers are created before the cleanup happens as it's async
// remove duplicate messages before proper cleanup is finished
results.push(message);
}
})
messages = []; // empty the array every time the result is returned
return results;
}
export const stopClient = async () => {
return stopReceivers().then(() => {
return client && client.close().catch(error => {
console.log(`client cleanup error: ${error}`); // swallow the error as we will cleanup anyways
});
}).finally (() => {
client = null;
receivers = [];
});
};
const stopReceivers = async () => {
return Promise.all(
receivers.map(receiver => {
if (receiver && (receiver.isReceiverOpen === undefined || receiver.isReceiverOpen)) {
return stopReceiver(receiver);
} else {
return null;
subscription = client.subscribe(
{
processEvents: async (events) => {
handleMessages(events, params)
},
processError: async (err) => {
console.log(err);
}
})
},
{ startPosition: params.startTime ? { enqueuedOn: new Date(params.startTime).getTime() } : { enqueuedOn: new Date() } }
);
};
const stopReceiver = async (receiver: ReceiveHandler) => {
receiver.stop().catch((err: object) => {
throw new Error(`receivers cleanup error: ${err}`);
});
}
const needToCreateNewEventHubClient = (parmas: any): boolean => {
return !client ||
parmas.hubConnectionString && parmas.hubConnectionString !== connectionString ||
parmas.customEventHubConnectionString && `${parmas.customEventHubConnectionString}/${parmas.customEventHubName}` !== connectionString;
}
const updateEntityIdIfNecessary = (parmas: any) => {
if (!deviceId || parmas.deviceId !== deviceId) {
deviceId = parmas.deviceId;
messages = [];
}
if (parmas.moduleId !== moduleId) {
moduleId = parmas.moduleId;
messages = [];
}
}
const onMessageReceived = async (eventData: any) => {
if (eventData?.annotations?.[IOTHUB_CONNECTION_DEVICE_ID] === deviceId) {
if (!moduleId || eventData?.annotations?.[IOTHUB_CONNECTION_MODULE_ID] === moduleId) {
const message: Message = {
body: eventData.body,
enqueuedTime: eventData.enqueuedTimeUtc.toString(),
properties: eventData.applicationProperties
};
message.systemProperties = eventData.annotations;
messages.push(message);
const handleMessages = (events: ReceivedEventData[], params: any) => {
const messages: Message[] = [];
events.forEach(event => {
if (event?.systemProperties?.[IOTHUB_CONNECTION_DEVICE_ID] === params.deviceId) {
if (!params.moduleId || event?.systemProperties?.[IOTHUB_CONNECTION_MODULE_ID] === params.moduleId) {
const message: Message = {
body: event.body,
enqueuedTime: event.enqueuedTimeUtc.toString(),
properties: event.properties
};
message.systemProperties = event.systemProperties;
if (messages.find(item => item.enqueuedTime >= message.enqueuedTime))
return; // do not push message if enqueuedTime is earlier than any existing message
messages.push(message);
}
}
});
if (messages.length >= 1) {
ws.send(JSON.stringify(messages));
}
};
};
export const stopClient = async () => {
console.log('stop client');
await subscription?.close();
await client?.close();
};

41
src/server/utils.spec.ts Normal file
Просмотреть файл

@ -0,0 +1,41 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as Utils from './utils';
describe('Utils', () => {
context('findMatchingFile', () => {
const model = {
'@id': 'urn:azureiot:ModelDiscovery:ModelInformation;1',
'@type': 'Interface',
'displayName': 'Digital Twin Client SDK Information',
'contents': [
{
'@type': 'Property',
'name': 'language',
'displayName': 'SDK Language',
'schema': 'string',
'description': 'The language for the Digital Twin client SDK. For example, Java.'
}
],
'@context': 'http://azureiot.com/v1/contexts/IoTModel.json'
};
it('returns null when @id and expected file name does not match', async () => {
jest.spyOn(Utils, 'readFileFromLocal').mockReturnValue(JSON.stringify(model));
expect(Utils.findMatchingFile('', ['test.json'], 'urn:azureiot:test:ModelInformation;1')).toEqual(null);
});
it('returns model when @id and expected file name matches', async () => {
jest.spyOn(Utils, 'readFileFromLocal').mockReturnValue(JSON.stringify(model));
expect(Utils.findMatchingFile('', ['test.json'], 'urn:azureiot:ModelDiscovery:ModelInformation;1')).toEqual(JSON.stringify(model));
});
it('returns model when @id and expected file name matches and model is an array', async () => {
jest.spyOn(Utils, 'readFileFromLocal').mockReturnValue(JSON.stringify([model]));
expect(Utils.findMatchingFile('', ['test.json'], 'urn:azureiot:ModelDiscovery:ModelInformation;1')).toEqual(model);
});
});
});

75
src/server/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,75 @@
import express = require('express');
import * as fs from 'fs';
import * as path from 'path';
import { SERVER_ERROR, SUCCESS } from './serverBase';
export const fetchDrivesOnWindows = (res: express.Response) => {
const exec = require('child_process').exec;
exec('wmic logicaldisk get name', (error: any, stdout: any, stderr: any) => { // tslint:disable-line:no-any
if (!error && !stderr) {
res.status(SUCCESS).send(stdout);
}
else {
res.status(SERVER_ERROR).send();
}
});
};
export const fetchDirectories = (dir: string, res: express.Response) => {
const result: string[] = [];
for (const item of fs.readdirSync(dir)) {
try {
const stat = fs.statSync(path.join(dir, item));
if (stat.isDirectory()) {
result.push(item);
}
}
catch {
// some item cannot be checked by isDirectory(), swallow error and continue the loop
}
}
res.status(SUCCESS).send(result);
};
// tslint:disable-next-line:cyclomatic-complexity
export const findMatchingFile = (filePath: string, fileNames: string[], expectedFileName: string): string => {
const filesWithParsingError = [];
for (const fileName of fileNames) {
if (isFileExtensionJson(fileName)) {
try {
const data = readFileFromLocal(filePath, fileName);
const parsedData = JSON.parse(data);
if (parsedData) {
if (Array.isArray(parsedData)) { // if parsedData is array, it is using expanded dtdl format
for (const pd of parsedData) {
if (pd['@id']?.toString() === expectedFileName) {
return pd;
}
}
}
else {
if (parsedData['@id']?.toString() === expectedFileName) {
return data;
}
}
}
}
catch (error) {
filesWithParsingError.push(`${fileName}: ${error.message}`); // swallow error and continue the loop
}
}
}
if (filesWithParsingError.length > 0) {
throw new Error(filesWithParsingError.join(', '));
}
return null;
};
const isFileExtensionJson = (fileName: string) => {
const i = fileName.lastIndexOf('.');
return i > 0 && fileName.substr(i) === '.json';
};
export const readFileFromLocal = (filePath: string, fileName: string) => {
return fs.readFileSync(`${filePath}/${fileName}`, 'utf-8');
}