Module identity (#141)
* add nav and route * refactor shimmer * add tests * add default class to shimmer and change it to react fc * rename and add tests
This commit is contained in:
Родитель
4d825c4655
Коммит
c309efe32d
|
@ -13,6 +13,7 @@ describe('utils', () => {
|
|||
const device: Device = {
|
||||
AuthenticationType: 'Sas',
|
||||
CloudToDeviceMessageCount: '0',
|
||||
ConnectionState: 'Disconnected',
|
||||
DeviceId: 'test',
|
||||
IotEdge: false,
|
||||
LastActivityTime: '2019-07-18T10:01:20.0568390Z',
|
||||
|
@ -23,7 +24,9 @@ describe('utils', () => {
|
|||
const deviceSummary: DeviceSummary = {
|
||||
authenticationType: 'Sas',
|
||||
cloudToDeviceMessageCount: '0',
|
||||
connectionState: 'Disconnected',
|
||||
deviceId: 'test',
|
||||
iotEdge: false,
|
||||
lastActivityTime: '3:01:20 AM, July 18, 2019',
|
||||
status: 'Enabled',
|
||||
statusUpdatedTime: null,
|
||||
|
@ -32,8 +35,10 @@ describe('utils', () => {
|
|||
const transformedDevice = transformDevice(device);
|
||||
expect(transformedDevice.authenticationType).toEqual(deviceSummary.authenticationType);
|
||||
expect(transformedDevice.cloudToDeviceMessageCount).toEqual(deviceSummary.cloudToDeviceMessageCount);
|
||||
expect(transformedDevice.connectionState).toEqual(deviceSummary.connectionState);
|
||||
expect(transformedDevice.deviceId).toEqual(deviceSummary.deviceId);
|
||||
const isLocalTime = new RegExp(/\d+:\d+:\d+ [AP]M, July 18, 2019/);
|
||||
expect(transformedDevice.iotEdge).toBeFalsy();
|
||||
expect(transformedDevice.lastActivityTime.match(isLocalTime)).toBeTruthy();
|
||||
expect(transformedDevice.status).toEqual(deviceSummary.status);
|
||||
expect(transformedDevice.statusUpdatedTime).toEqual(deviceSummary.statusUpdatedTime);
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { AuthenticationCredentials } from './deviceIdentity';
|
||||
|
||||
export interface ModuleIdentity {
|
||||
authentication: AuthenticationCredentials;
|
||||
cloudToDeviceMessageCount?: number;
|
||||
connectionState?: string;
|
||||
connectionStateUpdatedTime?: string;
|
||||
deviceId: string;
|
||||
etag?: string;
|
||||
generationId?: string;
|
||||
lastActivityTime?: string;
|
||||
managedBy?: string;
|
||||
moduleId: string;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { ModuleIdentity } from './moduleIdentity';
|
||||
import { SynchronizationStatus } from './synchronizationStatus';
|
||||
|
||||
export interface ModuleIdentityListWrapper {
|
||||
moduleIdentities?: ModuleIdentity[];
|
||||
synchronizationStatus: SynchronizationStatus;
|
||||
}
|
|
@ -78,3 +78,7 @@ export interface PatchDigitalTwinInterfacePropertiesParameters extends DataPlane
|
|||
digitalTwinId: string; // Format of digitalTwinId is DeviceId[~ModuleId]. ModuleId is optional.
|
||||
payload: DigitalTwinInterfaces;
|
||||
}
|
||||
|
||||
export interface FetchModuleIdentitiesParameters extends DataPlaneParameters {
|
||||
deviceId: string;
|
||||
}
|
||||
|
|
|
@ -945,4 +945,54 @@ describe('deviceTwinService', () => {
|
|||
expect(fetch).toBeCalledWith(DevicesService.EVENTHUB_STOP_ENDPOINT, serviceRequestParams);
|
||||
});
|
||||
});
|
||||
|
||||
context('fetchModuleIdentities', () => {
|
||||
const parameters = {
|
||||
connectionString,
|
||||
deviceId
|
||||
};
|
||||
|
||||
it('calls fetch with specified parameters and returns moduleIdentities when response is 200', async () => {
|
||||
jest.spyOn(DevicesService, 'dataPlaneConnectionHelper').mockReturnValue({
|
||||
connectionInfo: getConnectionInfoFromConnectionString(parameters.connectionString), sasToken});
|
||||
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {return {
|
||||
body: deviceIdentity
|
||||
}},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const connectionInformation = mockDataPlaneConnectionHelper({connectionString});
|
||||
const dataPlaneRequest: DevicesService.DataPlaneRequest = {
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${deviceId}/modules`,
|
||||
sharedAccessSignature: connectionInformation.sasToken
|
||||
};
|
||||
|
||||
const result = await DevicesService.fetchModuleIdentities(parameters);
|
||||
|
||||
const serviceRequestParams = {
|
||||
body: JSON.stringify(dataPlaneRequest),
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
method: HTTP_OPERATION_TYPES.Post,
|
||||
mode: 'cors',
|
||||
};
|
||||
|
||||
expect(fetch).toBeCalledWith(DevicesService.DATAPLANE_CONTROLLER_ENDPOINT, serviceRequestParams);
|
||||
expect(result).toEqual(deviceIdentity);
|
||||
});
|
||||
|
||||
it('throws Error when promise rejects', async done => {
|
||||
window.fetch = jest.fn().mockRejectedValueOnce(new Error('Not found'));
|
||||
await expect(DevicesService.fetchModuleIdentities(parameters)).rejects.toThrowError('Not found');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,8 @@ import { FetchDeviceTwinParameters,
|
|||
FetchDigitalTwinInterfacePropertiesParameters,
|
||||
InvokeDigitalTwinInterfaceCommandParameters,
|
||||
PatchDigitalTwinInterfacePropertiesParameters,
|
||||
CloudToDeviceMessageParameters } from '../parameters/deviceParameters';
|
||||
CloudToDeviceMessageParameters,
|
||||
FetchModuleIdentitiesParameters } from '../parameters/deviceParameters';
|
||||
import { CONTROLLER_API_ENDPOINT, DATAPLANE, EVENTHUB, DIGITAL_TWIN_API_VERSION, DataPlaneStatusCode, MONITOR, STOP, HEADERS, CLOUD_TO_DEVICE } from '../../constants/apiConstants';
|
||||
import { HTTP_OPERATION_TYPES } from '../constants';
|
||||
import { buildQueryString, getConnectionInfoFromConnectionString, generateSasToken } from '../shared/utils';
|
||||
|
@ -24,6 +25,7 @@ import { Message } from '../models/messages';
|
|||
import { Twin, Device, DataPlaneResponse } from '../models/device';
|
||||
import { DeviceIdentity } from '../models/deviceIdentity';
|
||||
import { DigitalTwinInterfaces } from '../models/digitalTwinModels';
|
||||
import { ModuleIdentity } from './../models/moduleIdentity';
|
||||
import { parseEventHubMessage } from './eventHubMessageHelper';
|
||||
|
||||
export const DATAPLANE_CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${DATAPLANE}`;
|
||||
|
@ -447,3 +449,22 @@ export const stopMonitoringEvents = async (): Promise<void> => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchModuleIdentities = async (parameters: FetchModuleIdentitiesParameters): Promise<DataPlaneResponse<ModuleIdentity[]>> => {
|
||||
try {
|
||||
const connectionInformation = dataPlaneConnectionHelper(parameters);
|
||||
|
||||
const dataPlaneRequest: DataPlaneRequest = {
|
||||
hostName: connectionInformation.connectionInfo.hostName,
|
||||
httpMethod: HTTP_OPERATION_TYPES.Get,
|
||||
path: `devices/${parameters.deviceId}/modules`,
|
||||
sharedAccessSignature: connectionInformation.sasToken,
|
||||
};
|
||||
|
||||
const response = await request(DATAPLANE_CONTROLLER_ENDPOINT, dataPlaneRequest);
|
||||
const result = await dataPlaneResponseHelper(response);
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ export const DELETE_DEVICES = 'DELETE_DEVICES';
|
|||
export const EXECUTE_METHOD = 'EXECUTE_METHOD';
|
||||
export const GET_DEVICE_IDENTITY = 'GET_DEVICE_IDENTITY';
|
||||
export const GET_DIGITAL_TWIN_INTERFACE_PROPERTIES = 'GET_DIGITAL_TWIN_INTERFACE_PROPERTIES';
|
||||
export const GET_MODULE_IDENTITIES = 'GET_MODULE_IDENTITIES';
|
||||
export const GET_TWIN = 'GET_TWIN';
|
||||
export const LIST_DEVICES = 'LIST_DEVICES';
|
||||
export const INVOKE_DEVICE_METHOD = 'INVOKE_DEVICE_METHOD';
|
||||
|
|
|
@ -13,6 +13,7 @@ export enum ROUTE_PARTS {
|
|||
IDENTITY = 'identity',
|
||||
INTERFACES = 'interfaces',
|
||||
METHODS = 'methods',
|
||||
MODULE_IDENTITY = 'moduleIdentity',
|
||||
PROPERTIES = 'properties',
|
||||
SETTINGS = 'settings',
|
||||
TWIN = 'twin'
|
||||
|
|
|
@ -70,3 +70,8 @@
|
|||
.view-scroll {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.view-scroll-vertical {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { InvokeMethodParameters, CloudToDeviceMessageParameters } from '../../ap
|
|||
import { Twin } from '../../api/models/device';
|
||||
import { DeviceIdentity } from '../../api/models/deviceIdentity';
|
||||
import { DigitalTwinInterfaces } from './../../api/models/digitalTwinModels';
|
||||
import { ModuleIdentity } from './../../api/models/moduleIdentity';
|
||||
import { REPOSITORY_LOCATION_TYPE } from './../../constants/repositoryLocationTypes';
|
||||
|
||||
const deviceContentCreator = actionCreatorFactory(actionPrefixes.DEVICECONTENT);
|
||||
|
@ -19,6 +20,7 @@ const getDeviceIdentityAction = deviceContentCreator.async<string, DeviceIdentit
|
|||
const getDigitalTwinInterfacePropertiesAction = deviceContentCreator.async<string, DigitalTwinInterfaces>(actionTypes.GET_DIGITAL_TWIN_INTERFACE_PROPERTIES);
|
||||
const getTwinAction = deviceContentCreator.async<string, Twin>(actionTypes.GET_TWIN);
|
||||
const getModelDefinitionAction = deviceContentCreator.async<GetModelDefinitionActionParameters, ModelDefinitionActionResult>(actionTypes.FETCH_MODEL_DEFINITION);
|
||||
const getModuleIdentitiesAction = deviceContentCreator.async<string, ModuleIdentity[]>(actionTypes.GET_MODULE_IDENTITIES);
|
||||
const invokeDirectMethodAction = deviceContentCreator.async<InvokeMethodParameters, string>(actionTypes.INVOKE_DEVICE_METHOD);
|
||||
const invokeDigitalTwinInterfaceCommandAction = deviceContentCreator.async<InvokeDigitalTwinInterfaceCommandActionParameters, string>(actionTypes.INVOKE_DIGITAL_TWIN_INTERFACE_COMMAND);
|
||||
const patchDigitalTwinInterfacePropertiesAction = deviceContentCreator.async<PatchDigitalTwinInterfacePropertiesActionParameters, DigitalTwinInterfaces>(actionTypes.PATCH_DIGITAL_TWIN_INTERFACE_PROPERTIES);
|
||||
|
@ -33,6 +35,7 @@ export {
|
|||
getDigitalTwinInterfacePropertiesAction,
|
||||
getTwinAction,
|
||||
getModelDefinitionAction,
|
||||
getModuleIdentitiesAction,
|
||||
invokeDirectMethodAction,
|
||||
invokeDigitalTwinInterfaceCommandAction,
|
||||
patchDigitalTwinInterfacePropertiesAction,
|
||||
|
|
|
@ -41,6 +41,21 @@ exports[`deviceContent matches snapshot 1`] = `
|
|||
>
|
||||
<DeviceContentNavComponent
|
||||
deviceId="testDevice"
|
||||
getDeviceIdentity={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"testDevice",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
getDigitalTwinInterfaceProperties={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -59,17 +74,19 @@ exports[`deviceContent matches snapshot 1`] = `
|
|||
history={
|
||||
Object {
|
||||
"location": Object {
|
||||
"pathname": "/#/devices/detail/?id=testDevice&testInterfaceId",
|
||||
"pathname": "/#/devices/detail/?id=testDevice",
|
||||
},
|
||||
}
|
||||
}
|
||||
identityWrapper={null}
|
||||
interfaceId="testInterfaceId"
|
||||
interfaceIds={Array []}
|
||||
isEdgeDevice={null}
|
||||
isLoading={false}
|
||||
isPnPDevice={true}
|
||||
location={
|
||||
Object {
|
||||
"pathname": "/#/devices/detail/?id=testDevice&testInterfaceId",
|
||||
"pathname": "/#/devices/detail/?id=testDevice",
|
||||
}
|
||||
}
|
||||
match={Object {}}
|
||||
|
@ -102,6 +119,10 @@ exports[`deviceContent matches snapshot 1`] = `
|
|||
component={[Function]}
|
||||
path="/devices/detail/cloudToDeviceMessage/"
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
path="/devices/detail/moduleIdentity/"
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
path="/devices/detail/digitalTwins/"
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import DeviceCommandPerInterface from './deviceCommandsPerInterface';
|
||||
|
@ -14,6 +13,7 @@ import { getDeviceIdFromQueryString, getInterfaceIdFromQueryString } from '../..
|
|||
import { CommandSchema } from './deviceCommandsPerInterfacePerCommand';
|
||||
import InterfaceNotFoundMessageBoxContainer from '../shared/interfaceNotFoundMessageBarContainer';
|
||||
import { REFRESH } from '../../../../constants/iconNames';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
|
||||
export interface DeviceCommandsProps extends DeviceInterfaceWithSchema{
|
||||
isLoading: boolean;
|
||||
|
@ -39,7 +39,7 @@ export default class DeviceCommands
|
|||
public render(): JSX.Element {
|
||||
if (this.props.isLoading) {
|
||||
return (
|
||||
<Shimmer className="fixed-shimmer" />
|
||||
<MultiLineShimmer/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { testSnapshot } from '../../../shared/utils/testHelpers';
|
|||
|
||||
describe('deviceContent', () => {
|
||||
|
||||
const pathname = `/#/devices/detail/?id=testDevice&testInterfaceId`;
|
||||
const pathname = `/#/devices/detail/?id=testDevice`;
|
||||
|
||||
const location: any = { // tslint:disable-line:no-any
|
||||
pathname
|
||||
|
@ -23,6 +23,7 @@ describe('deviceContent', () => {
|
|||
};
|
||||
const deviceContentProps: DeviceContentProps = {
|
||||
deviceId: 'testDevice',
|
||||
identityWrapper: null,
|
||||
interfaceId: 'testInterfaceId',
|
||||
interfaceIds: [],
|
||||
isLoading: false,
|
||||
|
@ -30,6 +31,7 @@ describe('deviceContent', () => {
|
|||
};
|
||||
|
||||
const deviceContentDispatchProps: DeviceContentDispatchProps = {
|
||||
getDeviceIdentity: jest.fn(),
|
||||
getDigitalTwinInterfaceProperties: jest.fn(),
|
||||
setInterfaceId: jest.fn()
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import DeviceTwinContainer from './deviceTwin/deviceTwinContainer';
|
|||
import DeviceEventsContainer from './deviceEvents/deviceEventsContainer';
|
||||
import DirectMethodContainer from './directMethod/directMethodContainer';
|
||||
import CloudToDeviceMessageContainer from './cloudToDeviceMessage/cloudToDeviceMessageContainer';
|
||||
import ModuleIdentityContainer from './moduleIdentity/moduleIdentityContainer';
|
||||
import DeviceContentNavComponent from './deviceContentNav';
|
||||
import BreadcrumbContainer from '../../../shared/components/breadcrumbContainer';
|
||||
import DigitalTwinsContentContainer from './digitalTwinContentContainer';
|
||||
|
@ -17,6 +18,9 @@ import { ResourceKeys } from '../../../../localization/resourceKeys';
|
|||
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext';
|
||||
import { NAV } from '../../../constants/iconNames';
|
||||
import { ROUTE_PARTS } from '../../../constants/routes';
|
||||
import { DeviceIdentityWrapper } from '../../../api/models/deviceIdentityWrapper';
|
||||
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
|
||||
import MultiLineShimmer from '../../../shared/components/multiLineShimmer';
|
||||
import '../../../css/_deviceContent.scss';
|
||||
import '../../../css/_layouts.scss';
|
||||
|
||||
|
@ -27,6 +31,7 @@ export interface DeviceContentDataProps {
|
|||
interfaceIds: string[];
|
||||
isLoading: boolean;
|
||||
isPnPDevice: boolean;
|
||||
identityWrapper: DeviceIdentityWrapper;
|
||||
}
|
||||
|
||||
export interface DeviceContentProps extends DeviceContentDataProps {
|
||||
|
@ -37,6 +42,7 @@ export interface DeviceContentProps extends DeviceContentDataProps {
|
|||
export interface DeviceContentDispatchProps {
|
||||
setInterfaceId: (interfaceId: string) => void;
|
||||
getDigitalTwinInterfaceProperties: (deviceId: string) => void;
|
||||
getDeviceIdentity: (deviceId: string) => void;
|
||||
}
|
||||
|
||||
export class DeviceContentComponent extends React.PureComponent<DeviceContentProps & DeviceContentDispatchProps, DeviceContentState> {
|
||||
|
@ -73,6 +79,7 @@ export class DeviceContentComponent extends React.PureComponent<DeviceContentPro
|
|||
|
||||
public componentDidMount() {
|
||||
this.props.getDigitalTwinInterfaceProperties(this.props.deviceId);
|
||||
this.props.getDeviceIdentity(this.props.deviceId);
|
||||
}
|
||||
|
||||
private readonly renderNav = (context: LocalizationContextInterface) => {
|
||||
|
@ -104,6 +111,7 @@ export class DeviceContentComponent extends React.PureComponent<DeviceContentPro
|
|||
<Route path={`/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.EVENTS}/`} component={DeviceEventsContainer}/>
|
||||
<Route path={`/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.METHODS}/`} component={DirectMethodContainer} />
|
||||
<Route path={`/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.CLOUD_TO_DEVICE_MESSAGE}/`} component={CloudToDeviceMessageContainer} />
|
||||
<Route path={`/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.MODULE_IDENTITY}/`} component={ModuleIdentityContainer} />
|
||||
<Route path={`/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.DIGITAL_TWINS}/`} component={DigitalTwinsContentContainer} />
|
||||
</div>
|
||||
);
|
||||
|
@ -117,9 +125,15 @@ export class DeviceContentComponent extends React.PureComponent<DeviceContentPro
|
|||
|
||||
private readonly createNavLinks = () => {
|
||||
return (
|
||||
<DeviceContentNavComponent
|
||||
{...this.props}
|
||||
selectedInterface={this.props.interfaceId}
|
||||
/>);
|
||||
this.props.identityWrapper && this.props.identityWrapper.deviceIdentitySynchronizationStatus === SynchronizationStatus.working ?
|
||||
<MultiLineShimmer/> :
|
||||
(
|
||||
<DeviceContentNavComponent
|
||||
{...this.props}
|
||||
isEdgeDevice={this.props.identityWrapper && this.props.identityWrapper.deviceIdentity && this.props.identityWrapper.deviceIdentity.capabilities.iotEdge}
|
||||
selectedInterface={this.props.interfaceId}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ import { connect } from 'react-redux';
|
|||
import { AnyAction } from 'typescript-fsa';
|
||||
import { DeviceContentComponent, DeviceContentDispatchProps, DeviceContentDataProps } from './deviceContent';
|
||||
import { StateType } from '../../../shared/redux/state';
|
||||
import { getIsDevicePnpSelector, getDigitalTwinInterfaceIdsSelector, getDigitalTwinInterfacePropertiesWrapperSelector } from '../selectors';
|
||||
import { setInterfaceIdAction, getDigitalTwinInterfacePropertiesAction } from '../actions';
|
||||
import { getIsDevicePnpSelector, getDigitalTwinInterfaceIdsSelector, getDigitalTwinInterfacePropertiesWrapperSelector, getDeviceIdentityWrapperSelector } from '../selectors';
|
||||
import { setInterfaceIdAction, getDigitalTwinInterfacePropertiesAction, getDeviceIdentityAction } from '../actions';
|
||||
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
|
||||
|
||||
const mapStateToProps = (state: StateType): DeviceContentDataProps => {
|
||||
const digitalTwinInterfacesWrapper = getDigitalTwinInterfacePropertiesWrapperSelector(state);
|
||||
return {
|
||||
identityWrapper: getDeviceIdentityWrapperSelector(state),
|
||||
interfaceIds: getDigitalTwinInterfaceIdsSelector(state),
|
||||
isLoading: digitalTwinInterfacesWrapper &&
|
||||
digitalTwinInterfacesWrapper.digitalTwinInterfacePropertiesSyncStatus === SynchronizationStatus.working,
|
||||
|
@ -23,6 +24,7 @@ const mapStateToProps = (state: StateType): DeviceContentDataProps => {
|
|||
|
||||
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>): DeviceContentDispatchProps => {
|
||||
return {
|
||||
getDeviceIdentity: (deviceId: string) => dispatch(getDeviceIdentityAction.started(deviceId)),
|
||||
getDigitalTwinInterfaceProperties: (deviceId: string) => dispatch(getDigitalTwinInterfacePropertiesAction.started(deviceId)),
|
||||
setInterfaceId: (interfaceId: string) => dispatch(setInterfaceIdAction(interfaceId))
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { Nav } from 'office-ui-fabric-react/lib/Nav';
|
||||
import DeviceContentNavComponent, { NAV_LINK_ITEMS_NONPNP, NAV_LINK_ITEMS_PNP } from './deviceContentNav';
|
||||
import DeviceContentNavComponent, { NAV_LINK_ITEMS_NONPNP, NAV_LINK_ITEMS_PNP, NAV_LINK_ITEMS_NONPNP_NONEDGE } from './deviceContentNav';
|
||||
import { mountWithLocalization, testSnapshot } from '../../../shared/utils/testHelpers';
|
||||
|
||||
describe('components/devices/deviceContentNav', () => {
|
||||
|
@ -15,6 +15,7 @@ describe('components/devices/deviceContentNav', () => {
|
|||
const props = {
|
||||
deviceId: 'test',
|
||||
interfaceIds: [],
|
||||
isEdgeDevice: true,
|
||||
isLoading: false,
|
||||
isPnPDevice: false,
|
||||
selectedInterface: '',
|
||||
|
@ -38,6 +39,13 @@ describe('components/devices/deviceContentNav', () => {
|
|||
expect(navigation.props().groups[0].links.length).toEqual(NAV_LINK_ITEMS_NONPNP.length);
|
||||
});
|
||||
|
||||
it('shows non-pnp non-edge nav if device is not edge', () => {
|
||||
const wrapper = mountWithLocalization(getComponent({isEdgeDevice: false}));
|
||||
|
||||
const navigation = wrapper.find(Nav);
|
||||
expect(navigation.props().groups[0].links.length).toEqual(NAV_LINK_ITEMS_NONPNP_NONEDGE.length);
|
||||
});
|
||||
|
||||
it('show non-pnp nav and pnp nav when device is pnp', () => {
|
||||
const interfaceId = 'urn:azureiot:com:DeviceInformation:1';
|
||||
const interfaceIds = [interfaceId];
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface DeviceContentNavDataProps {
|
|||
isLoading: boolean;
|
||||
isPnPDevice: boolean;
|
||||
selectedInterface: string;
|
||||
isEdgeDevice: boolean;
|
||||
}
|
||||
|
||||
export interface DeviceContentNavDispatchProps {
|
||||
|
@ -28,6 +29,7 @@ interface DeviceContentNavState {
|
|||
|
||||
export const NAV_LINK_ITEMS_PNP = [ROUTE_PARTS.INTERFACES, ROUTE_PARTS.SETTINGS, ROUTE_PARTS.PROPERTIES, ROUTE_PARTS.COMMANDS, ROUTE_PARTS.EVENTS];
|
||||
export const NAV_LINK_ITEMS_NONPNP = [ROUTE_PARTS.IDENTITY, ROUTE_PARTS.TWIN, ROUTE_PARTS.EVENTS, ROUTE_PARTS.METHODS, ROUTE_PARTS.CLOUD_TO_DEVICE_MESSAGE];
|
||||
export const NAV_LINK_ITEMS_NONPNP_NONEDGE = [ROUTE_PARTS.IDENTITY, ROUTE_PARTS.TWIN, ROUTE_PARTS.EVENTS, ROUTE_PARTS.METHODS, ROUTE_PARTS.CLOUD_TO_DEVICE_MESSAGE, ROUTE_PARTS.MODULE_IDENTITY];
|
||||
|
||||
export default class DeviceContentNavComponent extends React.Component<DeviceContentNavDataProps & DeviceContentNavDispatchProps, DeviceContentNavState> {
|
||||
constructor(props: DeviceContentNavDataProps & DeviceContentNavDispatchProps) {
|
||||
|
@ -53,9 +55,10 @@ export default class DeviceContentNavComponent extends React.Component<DeviceCon
|
|||
}
|
||||
|
||||
private readonly createNavLinks = (context: LocalizationContextInterface) => {
|
||||
const { deviceId, interfaceIds, isPnPDevice } = this.props;
|
||||
const { deviceId, interfaceIds, isPnPDevice, isEdgeDevice } = this.props;
|
||||
|
||||
const nonPnpNavLinks = NAV_LINK_ITEMS_NONPNP.map((nav: string) => ({
|
||||
const navItems = isEdgeDevice ? NAV_LINK_ITEMS_NONPNP : NAV_LINK_ITEMS_NONPNP_NONEDGE;
|
||||
const nonPnpNavLinks = navItems.map((nav: string) => ({
|
||||
key: nav,
|
||||
name: context.t((ResourceKeys.deviceContent.navBar as any)[nav]), // tslint:disable-line:no-any
|
||||
url: `#/${ROUTE_PARTS.DEVICES}/${ROUTE_PARTS.DETAIL}/${nav}/?${ROUTE_PARAMS.DEVICE_ID}=${encodeURIComponent(deviceId)}`
|
||||
|
|
|
@ -6,7 +6,6 @@ import * as React from 'react';
|
|||
import { Validator, ValidatorResult } from 'jsonschema';
|
||||
import { CommandBar, ICommandBarItemProps } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { TextField, ITextFieldProps } from 'office-ui-fabric-react/lib/TextField';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
@ -26,6 +25,7 @@ import LabelWithTooltip from '../../../../shared/components/labelWithTooltip';
|
|||
import { DEFAULT_CONSUMER_GROUP } from '../../../../constants/apiConstants';
|
||||
import ErrorBoundary from '../../../errorBoundary';
|
||||
import { getLocalizedData } from '../../../../api/dataTransforms/modelDefinitionTransform';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
import '../../../../css/_deviceEvents.scss';
|
||||
|
||||
const JSON_SPACES = 2;
|
||||
|
@ -82,9 +82,7 @@ export default class DeviceEventsPerInterfaceComponent extends React.Component<D
|
|||
|
||||
public render(): JSX.Element {
|
||||
if (this.props.isLoading) {
|
||||
return (
|
||||
<Shimmer className="fixed-shimmer" />
|
||||
);
|
||||
return <MultiLineShimmer/>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1563,9 +1563,7 @@ exports[`devices/components/deviceIdentity snapshot matches snapshot with Synchr
|
|||
<div
|
||||
className="device-detail"
|
||||
>
|
||||
<StyledShimmerBase
|
||||
className="fixed-shimmer"
|
||||
/>
|
||||
<Component />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
@ -18,13 +17,12 @@ import { DeviceStatus } from '../../../../api/models/deviceStatus';
|
|||
import { generateKey } from '../../../../shared/utils/utils';
|
||||
import { DeviceIdentityWrapper } from '../../../../api/models/deviceIdentityWrapper';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
import { getDeviceIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
|
||||
import { MaskedCopyableTextField } from '../../../../shared/components/maskedCopyableTextField';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
import '../../../../css/_deviceDetail.scss';
|
||||
|
||||
export interface DeviceIdentityDispatchProps {
|
||||
updateDeviceIdentity: (deviceIdentity: DeviceIdentity) => void;
|
||||
getDeviceIdentity: (deviceId: string) => void;
|
||||
}
|
||||
|
||||
export interface DeviceIdentityDataProps {
|
||||
|
@ -64,10 +62,6 @@ export default class DeviceIdentityInformation
|
|||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.getDeviceIdentity(getDeviceIdFromQueryString(this.props));
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:cyclomatic-complexity
|
||||
public static getDerivedStateFromProps(props: DeviceIdentityDataProps & DeviceIdentityDispatchProps & RouteComponentProps, state: DeviceIdentityState): Partial<DeviceIdentityState> | null {
|
||||
if (props.identityWrapper) {
|
||||
|
@ -122,7 +116,7 @@ export default class DeviceIdentityInformation
|
|||
return (
|
||||
<div className="device-detail">
|
||||
{ this.props.identityWrapper.deviceIdentitySynchronizationStatus === SynchronizationStatus.working ?
|
||||
<Shimmer className="fixed-shimmer" /> :
|
||||
<MultiLineShimmer/> :
|
||||
<>
|
||||
<MaskedCopyableTextField
|
||||
ariaLabel={context.t(ResourceKeys.deviceIdentity.deviceID)}
|
||||
|
|
|
@ -10,7 +10,7 @@ import DeviceIdentityInformation, { DeviceIdentityDataProps, DeviceIdentityDispa
|
|||
import { getDeviceIdentityWrapperSelector } from '../../selectors';
|
||||
import { getConnectionStringSelector } from '../../../../login/selectors';
|
||||
import { DeviceIdentity } from '../../../../api/models/deviceIdentity';
|
||||
import { updateDeviceIdentityAction, getDeviceIdentityAction } from '../../actions';
|
||||
import { updateDeviceIdentityAction } from '../../actions';
|
||||
|
||||
const mapStateToProps = (state: StateType): DeviceIdentityDataProps => {
|
||||
return {
|
||||
|
@ -21,7 +21,6 @@ const mapStateToProps = (state: StateType): DeviceIdentityDataProps => {
|
|||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DeviceIdentityDispatchProps => {
|
||||
return {
|
||||
getDeviceIdentity: (deviceId: string) => dispatch(getDeviceIdentityAction.started(deviceId)),
|
||||
updateDeviceIdentity: (deviceIdentity: DeviceIdentity) => dispatch(updateDeviceIdentityAction.started(deviceIdentity)),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/devices/deviceInterfaces show Shimmer when status is working 1`] = `
|
||||
<StyledShimmerBase
|
||||
className="fixed-shimmer"
|
||||
/>
|
||||
`;
|
||||
exports[`components/devices/deviceInterfaces shows Shimmer when status is working 1`] = `<Component />`;
|
||||
|
||||
exports[`components/devices/deviceInterfaces show interface information when status is failed 1`] = `
|
||||
exports[`components/devices/deviceInterfaces shows interface information when status is failed 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
|
@ -31,7 +27,7 @@ exports[`components/devices/deviceInterfaces show interface information when sta
|
|||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/devices/deviceInterfaces show interface information when status is fetched 1`] = `
|
||||
exports[`components/devices/deviceInterfaces shows interface information when status is fetched 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
|
@ -109,7 +105,7 @@ exports[`components/devices/deviceInterfaces show interface information when sta
|
|||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/devices/deviceInterfaces show interface information when status is fetched 2`] = `
|
||||
exports[`components/devices/deviceInterfaces shows interface information when status is fetched 2`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
|
@ -187,7 +183,7 @@ exports[`components/devices/deviceInterfaces show interface information when sta
|
|||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/devices/deviceInterfaces show interface information when status is fetched 3`] = `
|
||||
exports[`components/devices/deviceInterfaces shows interface information when status is fetched 3`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
|
@ -265,7 +261,7 @@ exports[`components/devices/deviceInterfaces show interface information when sta
|
|||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/devices/deviceInterfaces show interface information when status is fetched 4`] = `
|
||||
exports[`components/devices/deviceInterfaces shows interface information when status is fetched 4`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { ActionButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import DeviceInterfaces, { DeviceInterfaceProps, DeviceInterfaceDispatchProps } from './deviceInterfaces';
|
||||
|
@ -93,24 +92,23 @@ describe('components/devices/deviceInterfaces', () => {
|
|||
};
|
||||
/* tslint:enable */
|
||||
|
||||
it('show Shimmer when status is working', () => {
|
||||
const wrapper = getComponent();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(Shimmer)).toHaveLength(1);
|
||||
it('shows Shimmer when status is working', () => {
|
||||
const component = getComponent();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('show interface information when status is failed', () => {
|
||||
const wrapper = getComponent({
|
||||
it('shows interface information when status is failed', () => {
|
||||
const component = getComponent({
|
||||
isLoading: false,
|
||||
modelDefinitionWithSource: {
|
||||
modelDefinitionSynchronizationStatus: SynchronizationStatus.failed
|
||||
}
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('show interface information when status is fetched', () => {
|
||||
let wrapper = getComponent({
|
||||
it('shows interface information when status is fetched', () => {
|
||||
let component = getComponent({
|
||||
isLoading: false,
|
||||
modelDefinitionWithSource: {
|
||||
modelDefinition,
|
||||
|
@ -118,14 +116,14 @@ describe('components/devices/deviceInterfaces', () => {
|
|||
source: REPOSITORY_LOCATION_TYPE.Public
|
||||
}
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(ActionButton).props().onClick(null));
|
||||
expect(component).toMatchSnapshot();
|
||||
expect(component.find(ActionButton).props().onClick(null));
|
||||
expect(settingsVisibleToggle).toBeCalled();
|
||||
const command = wrapper.find(CommandBar);
|
||||
const command = component.find(CommandBar);
|
||||
command.props().items[0].onClick(null);
|
||||
expect(refresh).toBeCalled();
|
||||
|
||||
wrapper = getComponent({
|
||||
component = getComponent({
|
||||
isLoading: false,
|
||||
modelDefinitionWithSource: {
|
||||
modelDefinition,
|
||||
|
@ -133,9 +131,9 @@ describe('components/devices/deviceInterfaces', () => {
|
|||
source: REPOSITORY_LOCATION_TYPE.Private
|
||||
}
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
wrapper = getComponent({
|
||||
component = getComponent({
|
||||
isLoading: false,
|
||||
modelDefinitionWithSource: {
|
||||
modelDefinition,
|
||||
|
@ -143,9 +141,9 @@ describe('components/devices/deviceInterfaces', () => {
|
|||
source: REPOSITORY_LOCATION_TYPE.Device
|
||||
}
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
wrapper = getComponent({
|
||||
component = getComponent({
|
||||
isLoading: false,
|
||||
modelDefinitionWithSource: {
|
||||
modelDefinition,
|
||||
|
@ -153,6 +151,6 @@ describe('components/devices/deviceInterfaces', () => {
|
|||
source: undefined
|
||||
}
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@ import * as React from 'react';
|
|||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { ActionButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
|
||||
|
@ -19,6 +18,7 @@ import { REFRESH } from '../../../../constants/iconNames';
|
|||
import ErrorBoundary from '../../../errorBoundary';
|
||||
import { getLocalizedData } from '../../../../api/dataTransforms/modelDefinitionTransform';
|
||||
import { ThemeContextInterface, ThemeContextConsumer } from '../../../../shared/contexts/themeContext';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
|
||||
const EditorPromise = import('react-monaco-editor');
|
||||
const Editor = React.lazy(() => EditorPromise);
|
||||
|
@ -44,7 +44,7 @@ export default class DeviceInterfaces extends React.Component<DeviceInterfacePro
|
|||
return (
|
||||
<LocalizationContextConsumer>
|
||||
{(context: LocalizationContextInterface) => (
|
||||
this.props.isLoading ? <Shimmer className="fixed-shimmer" /> :
|
||||
this.props.isLoading ? <MultiLineShimmer/> :
|
||||
<>
|
||||
<CommandBar
|
||||
className="command"
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
|
@ -13,6 +12,7 @@ import { getInterfaceIdFromQueryString, getDeviceIdFromQueryString } from '../..
|
|||
import DevicePropertiesPerInterface from './devicePropertiesPerInterface';
|
||||
import InterfaceNotFoundMessageBoxContainer from '../shared/interfaceNotFoundMessageBarContainer';
|
||||
import { REFRESH } from '../../../../constants/iconNames';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
|
||||
export interface DevicePropertiesDataProps {
|
||||
twinAndSchema: TwinWithSchema[];
|
||||
|
@ -32,9 +32,7 @@ export default class DeviceProperties
|
|||
|
||||
public render(): JSX.Element {
|
||||
if (this.props.isLoading) {
|
||||
return (
|
||||
<Shimmer className="fixed-shimmer" />
|
||||
);
|
||||
return <MultiLineShimmer/>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import DeviceSettingPerInterface from './deviceSettingsPerInterface';
|
||||
|
@ -14,6 +13,7 @@ import { getDeviceIdFromQueryString, getInterfaceIdFromQueryString } from '../..
|
|||
import { PatchDigitalTwinInterfacePropertiesActionParameters } from '../../actions';
|
||||
import InterfaceNotFoundMessageBoxContainer from '../shared/interfaceNotFoundMessageBarContainer';
|
||||
import { REFRESH } from '../../../../constants/iconNames';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
|
||||
export interface DeviceSettingsProps extends DeviceInterfaceWithSchema{
|
||||
isLoading: boolean;
|
||||
|
@ -40,7 +40,7 @@ export default class DeviceSettings
|
|||
public render(): JSX.Element {
|
||||
if (this.props.isLoading) {
|
||||
return (
|
||||
<Shimmer className="fixed-shimmer" />
|
||||
<MultiLineShimmer/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ exports[`devices/components/deviceTwin snapshot matches snapshot 1`] = `
|
|||
<h3>
|
||||
deviceTwin.headerText
|
||||
</h3>
|
||||
<StyledShimmerBase
|
||||
<Component
|
||||
className="device-detail"
|
||||
/>
|
||||
</Fragment>
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { Twin } from '../../../../api/models/device';
|
||||
|
@ -14,8 +13,8 @@ import { getDeviceIdFromQueryString } from '../../../../shared/utils/queryString
|
|||
import { UpdateTwinActionParameters } from '../../actions';
|
||||
import { REFRESH, SAVE } from '../../../../constants/iconNames';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
import '../../../../css/_deviceDetail.scss';
|
||||
import { ThemeContextConsumer, ThemeContextInterface } from '../../../../shared/contexts/themeContext';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
|
||||
const EditorPromise = import('react-monaco-editor');
|
||||
const Editor = React.lazy(() => EditorPromise);
|
||||
|
@ -139,9 +138,7 @@ export default class DeviceTwin
|
|||
|
||||
private readonly renderTwinViewer = () => {
|
||||
if (this.props.twinState === SynchronizationStatus.working) {
|
||||
return (
|
||||
<Shimmer className="device-detail"/>
|
||||
);
|
||||
return <MultiLineShimmer className="device-detail"/>;
|
||||
}
|
||||
|
||||
const twin = this.state.twin;
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`devices/components/moduleIdentity snapshot matches snapshot when fetch failed 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.command.refresh",
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "Refresh",
|
||||
"name": "moduleIdentity.command.refresh",
|
||||
"onClick": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<h3>
|
||||
moduleIdentity.headerText
|
||||
</h3>
|
||||
<div
|
||||
className="device-detail"
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.moduleId",
|
||||
"fieldName": "moduleId",
|
||||
"isResizable": true,
|
||||
"key": "moduleId",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 50,
|
||||
"name": "moduleIdentity.columns.moduleId",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.connectionState",
|
||||
"fieldName": "connectionState",
|
||||
"isResizable": true,
|
||||
"key": "connectionState",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 50,
|
||||
"name": "moduleIdentity.columns.connectionState",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.connectionStateLastUpdated",
|
||||
"fieldName": "connectionStateLastUpdated",
|
||||
"isResizable": true,
|
||||
"key": "connectionStateLastUpdated",
|
||||
"maxWidth": 250,
|
||||
"minWidth": 150,
|
||||
"name": "moduleIdentity.columns.connectionStateLastUpdated",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.lastActivityTime",
|
||||
"fieldName": "lastActivityTime",
|
||||
"key": "lastActivityTime",
|
||||
"maxWidth": 250,
|
||||
"minWidth": 150,
|
||||
"name": "moduleIdentity.columns.lastActivityTime",
|
||||
"onRender": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
selectionMode={0}
|
||||
/>
|
||||
moduleIdentity.errorFetching
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`devices/components/moduleIdentity snapshot matches snapshot while loading 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.command.refresh",
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "Refresh",
|
||||
"name": "moduleIdentity.command.refresh",
|
||||
"onClick": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<h3>
|
||||
moduleIdentity.headerText
|
||||
</h3>
|
||||
<div
|
||||
className="device-detail"
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.moduleId",
|
||||
"fieldName": "moduleId",
|
||||
"isResizable": true,
|
||||
"key": "moduleId",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 50,
|
||||
"name": "moduleIdentity.columns.moduleId",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.connectionState",
|
||||
"fieldName": "connectionState",
|
||||
"isResizable": true,
|
||||
"key": "connectionState",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 50,
|
||||
"name": "moduleIdentity.columns.connectionState",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.connectionStateLastUpdated",
|
||||
"fieldName": "connectionStateLastUpdated",
|
||||
"isResizable": true,
|
||||
"key": "connectionStateLastUpdated",
|
||||
"maxWidth": 250,
|
||||
"minWidth": 150,
|
||||
"name": "moduleIdentity.columns.connectionStateLastUpdated",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.lastActivityTime",
|
||||
"fieldName": "lastActivityTime",
|
||||
"key": "lastActivityTime",
|
||||
"maxWidth": 250,
|
||||
"minWidth": 150,
|
||||
"name": "moduleIdentity.columns.lastActivityTime",
|
||||
"onRender": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Component />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`devices/components/moduleIdentity snapshot matches snapshot with moduleIdentityList 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.command.refresh",
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "Refresh",
|
||||
"name": "moduleIdentity.command.refresh",
|
||||
"onClick": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<h3>
|
||||
moduleIdentity.headerText
|
||||
</h3>
|
||||
<div
|
||||
className="device-detail"
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.moduleId",
|
||||
"fieldName": "moduleId",
|
||||
"isResizable": true,
|
||||
"key": "moduleId",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 50,
|
||||
"name": "moduleIdentity.columns.moduleId",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.connectionState",
|
||||
"fieldName": "connectionState",
|
||||
"isResizable": true,
|
||||
"key": "connectionState",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 50,
|
||||
"name": "moduleIdentity.columns.connectionState",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.connectionStateLastUpdated",
|
||||
"fieldName": "connectionStateLastUpdated",
|
||||
"isResizable": true,
|
||||
"key": "connectionStateLastUpdated",
|
||||
"maxWidth": 250,
|
||||
"minWidth": 150,
|
||||
"name": "moduleIdentity.columns.connectionStateLastUpdated",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "moduleIdentity.columns.lastActivityTime",
|
||||
"fieldName": "lastActivityTime",
|
||||
"key": "lastActivityTime",
|
||||
"maxWidth": 250,
|
||||
"minWidth": 150,
|
||||
"name": "moduleIdentity.columns.lastActivityTime",
|
||||
"onRender": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"authentication": null,
|
||||
"deviceId": "testDevice",
|
||||
"moduleId": "testModule",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectionMode={0}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,76 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import 'jest';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import ModuleIdentityComponent, { ModuleIdentityDataProps, ModuleIdentityDispatchProps } from './moduleIdentity';
|
||||
import { mountWithLocalization, testWithLocalizationContext, testSnapshot } from '../../../../shared/utils/testHelpers';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
|
||||
const pathname = `/`;
|
||||
|
||||
const location: any = { // tslint:disable-line:no-any
|
||||
pathname
|
||||
};
|
||||
const routerprops: any = { // tslint:disable-line:no-any
|
||||
history: {
|
||||
location
|
||||
},
|
||||
location,
|
||||
match: {}
|
||||
};
|
||||
|
||||
const moduleIdentityDataProps: ModuleIdentityDataProps = {
|
||||
moduleIdentityList: [],
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
};
|
||||
|
||||
const mockGetModuleIdentities = jest.fn();
|
||||
const moduleIdentityDispatchProps: ModuleIdentityDispatchProps = {
|
||||
getModuleIdentities: mockGetModuleIdentities
|
||||
};
|
||||
|
||||
const getComponent = (overrides = {}) => {
|
||||
const props = {
|
||||
...moduleIdentityDataProps,
|
||||
...moduleIdentityDispatchProps,
|
||||
...routerprops,
|
||||
...overrides
|
||||
};
|
||||
return <ModuleIdentityComponent {...props} />;
|
||||
};
|
||||
|
||||
describe('devices/components/moduleIdentity', () => {
|
||||
context('snapshot', () => {
|
||||
it('matches snapshot while loading', () => {
|
||||
testSnapshot(getComponent());
|
||||
});
|
||||
|
||||
it('matches snapshot when fetch failed', () => {
|
||||
testSnapshot(getComponent({
|
||||
synchronizationStatus: SynchronizationStatus.failed
|
||||
}));
|
||||
});
|
||||
|
||||
it('matches snapshot with moduleIdentityList', () => {
|
||||
testSnapshot(getComponent({
|
||||
moduleIdentityList: [{
|
||||
authentication: null,
|
||||
deviceId: 'testDevice',
|
||||
moduleId: 'testModule'
|
||||
}],
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}));
|
||||
});
|
||||
|
||||
it('calls refresh', () => {
|
||||
const wrapper = mountWithLocalization(getComponent());
|
||||
const commandBar = wrapper.find(CommandBar).first();
|
||||
commandBar.props().items[0].onClick(null);
|
||||
expect(mockGetModuleIdentities).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { DetailsList, IColumn, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { getDeviceIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
|
||||
import { REFRESH } from '../../../../constants/iconNames';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
import { parseDateTimeString } from '../../../../api/dataTransforms/transformHelper';
|
||||
import { ModuleIdentity } from '../../../../api/models/moduleIdentity';
|
||||
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
|
||||
import '../../../../css/_deviceDetail.scss';
|
||||
|
||||
export interface ModuleIdentityDataProps {
|
||||
moduleIdentityList: ModuleIdentity[];
|
||||
synchronizationStatus: SynchronizationStatus;
|
||||
}
|
||||
|
||||
export interface ModuleIdentityDispatchProps {
|
||||
getModuleIdentities: (deviceId: string) => void;
|
||||
}
|
||||
|
||||
export default class ModuleIdentityComponent
|
||||
extends React.Component<ModuleIdentityDataProps & ModuleIdentityDispatchProps & RouteComponentProps> {
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<LocalizationContextConsumer>
|
||||
{(context: LocalizationContextInterface) => (
|
||||
<>
|
||||
{this.showCommandBar(context)}
|
||||
<h3>{context.t(ResourceKeys.moduleIdentity.headerText)}</h3>
|
||||
{this.renderGrid(context)}
|
||||
</>
|
||||
)}
|
||||
</LocalizationContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.getModuleIdentities(getDeviceIdFromQueryString(this.props));
|
||||
}
|
||||
|
||||
private readonly showCommandBar = (context: LocalizationContextInterface) => {
|
||||
return (
|
||||
<CommandBar
|
||||
className="command"
|
||||
items={[
|
||||
{
|
||||
ariaLabel: context.t(ResourceKeys.moduleIdentity.command.refresh),
|
||||
iconProps: {iconName: REFRESH},
|
||||
key: REFRESH,
|
||||
name: context.t(ResourceKeys.moduleIdentity.command.refresh),
|
||||
onClick: this.handleRefresh
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly handleRefresh = () => {
|
||||
this.props.getModuleIdentities(getDeviceIdFromQueryString(this.props));
|
||||
}
|
||||
|
||||
private readonly renderGrid = (context: LocalizationContextInterface) => {
|
||||
const { moduleIdentityList, synchronizationStatus } = this.props;
|
||||
return (
|
||||
<div className="device-detail">
|
||||
<DetailsList
|
||||
columns={this.getColumns(context)}
|
||||
items={moduleIdentityList}
|
||||
selectionMode={SelectionMode.none}
|
||||
/>
|
||||
|
||||
{synchronizationStatus === SynchronizationStatus.working && <MultiLineShimmer/>}
|
||||
{synchronizationStatus === SynchronizationStatus.fetched && moduleIdentityList.length === 0 && context.t(ResourceKeys.moduleIdentity.noModules)}
|
||||
{synchronizationStatus === SynchronizationStatus.failed && context.t(ResourceKeys.moduleIdentity.errorFetching)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getColumns(localizationContext: LocalizationContextInterface): IColumn[] {
|
||||
const { t } = localizationContext;
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.moduleIdentity.columns.moduleId),
|
||||
fieldName: 'moduleId',
|
||||
isResizable: true,
|
||||
key: 'moduleId',
|
||||
maxWidth: 200,
|
||||
minWidth: 50,
|
||||
name: t(ResourceKeys.moduleIdentity.columns.moduleId)
|
||||
},
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.moduleIdentity.columns.connectionState),
|
||||
fieldName: 'connectionState',
|
||||
isResizable: true,
|
||||
key: 'connectionState',
|
||||
maxWidth: 200,
|
||||
minWidth: 50,
|
||||
name: t(ResourceKeys.moduleIdentity.columns.connectionState)
|
||||
},
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.moduleIdentity.columns.connectionStateLastUpdated),
|
||||
fieldName: 'connectionStateLastUpdated',
|
||||
isResizable: true,
|
||||
key: 'connectionStateLastUpdated',
|
||||
maxWidth: 250,
|
||||
minWidth: 150,
|
||||
name: t(ResourceKeys.moduleIdentity.columns.connectionStateLastUpdated),
|
||||
onRender: item => parseDateTimeString(item.connectionStateUpdatedTime) || '--'
|
||||
},
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.moduleIdentity.columns.lastActivityTime),
|
||||
fieldName: 'lastActivityTime',
|
||||
key: 'lastActivityTime',
|
||||
maxWidth: 250,
|
||||
minWidth: 150,
|
||||
name: t(ResourceKeys.moduleIdentity.columns.lastActivityTime),
|
||||
onRender: item => parseDateTimeString(item.lastActivityTime) || '--'
|
||||
}];
|
||||
|
||||
return columns;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { compose, Dispatch } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { StateType } from '../../../../shared/redux/state';
|
||||
import ModuleIdentityComponent, { ModuleIdentityDataProps, ModuleIdentityDispatchProps } from './moduleIdentity';
|
||||
import { getModuleIdentitiesAction } from '../../actions';
|
||||
import { getModuleIdentityListWrapperSelector } from '../../selectors';
|
||||
|
||||
const mapStateToProps = (state: StateType): ModuleIdentityDataProps => {
|
||||
const moduleIdentityListWrapper = getModuleIdentityListWrapperSelector(state);
|
||||
return {
|
||||
moduleIdentityList: moduleIdentityListWrapper && moduleIdentityListWrapper.moduleIdentities || [],
|
||||
synchronizationStatus: moduleIdentityListWrapper && moduleIdentityListWrapper.synchronizationStatus
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): ModuleIdentityDispatchProps => {
|
||||
return {
|
||||
getModuleIdentities: (deviceId: string) => dispatch(getModuleIdentitiesAction.started(deviceId)),
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps))(ModuleIdentityComponent);
|
|
@ -11,7 +11,9 @@ import { GET_TWIN,
|
|||
FETCH_MODEL_DEFINITION,
|
||||
GET_DIGITAL_TWIN_INTERFACE_PROPERTIES,
|
||||
PATCH_DIGITAL_TWIN_INTERFACE_PROPERTIES,
|
||||
UPDATE_DEVICE_IDENTITY
|
||||
UPDATE_DEVICE_IDENTITY,
|
||||
SET_INTERFACE_ID,
|
||||
GET_MODULE_IDENTITIES
|
||||
} from '../../constants/actionTypes';
|
||||
import { getTwinAction,
|
||||
getModelDefinitionAction,
|
||||
|
@ -20,7 +22,8 @@ import { getTwinAction,
|
|||
getDigitalTwinInterfacePropertiesAction,
|
||||
patchDigitalTwinInterfacePropertiesAction,
|
||||
updateDeviceIdentityAction,
|
||||
setInterfaceIdAction } from './actions';
|
||||
setInterfaceIdAction,
|
||||
getModuleIdentitiesAction } from './actions';
|
||||
import reducer from './reducer';
|
||||
import { deviceContentStateInitial, DeviceContentStateInterface } from './state';
|
||||
import { ModelDefinition } from '../../api/models/ModelDefinition';
|
||||
|
@ -28,7 +31,7 @@ import { DeviceIdentity } from '../../api/models/deviceIdentity';
|
|||
import { SynchronizationStatus } from '../../api/models/synchronizationStatus';
|
||||
import { DigitalTwinInterfaces } from '../../api/models/digitalTwinModels';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { SET_INTERFACE_ID } from './../../constants/actionTypes';
|
||||
import { ModuleIdentity } from '../../api/models/moduleIdentity';
|
||||
|
||||
describe('deviceContentStateReducer', () => {
|
||||
const deviceId = 'testDeviceId';
|
||||
|
@ -104,6 +107,7 @@ describe('deviceContentStateReducer', () => {
|
|||
digitalTwinInterfaceProperties: undefined,
|
||||
interfaceIdSelected: '',
|
||||
modelDefinitionWithSource,
|
||||
moduleIdentityList: undefined
|
||||
});
|
||||
const action = clearModelDefinitionsAction();
|
||||
expect(reducer(initialState(), action).modelDefinitionWithSource).toEqual(null);
|
||||
|
@ -327,4 +331,29 @@ describe('deviceContentStateReducer', () => {
|
|||
expect(reducer(initialState, action).digitalTwinInterfaceProperties.digitalTwinInterfacePropertiesSyncStatus).toEqual(SynchronizationStatus.failed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moduleIdentities scenarios', () => {
|
||||
const moduleIdentity: ModuleIdentity = {
|
||||
authentication: null,
|
||||
deviceId: 'testDevice',
|
||||
moduleId: 'testModule'
|
||||
};
|
||||
|
||||
it (`handles ${GET_MODULE_IDENTITIES}/ACTION_START action`, () => {
|
||||
const action = getModuleIdentitiesAction.started(deviceId);
|
||||
expect(reducer(deviceContentStateInitial(), action).moduleIdentityList.synchronizationStatus).toEqual(SynchronizationStatus.working);
|
||||
});
|
||||
|
||||
it (`handles ${GET_MODULE_IDENTITIES}/ACTION_DONE action`, () => {
|
||||
const action = getModuleIdentitiesAction.done({params: deviceId, result: [moduleIdentity]});
|
||||
expect(reducer(deviceContentStateInitial(), action).moduleIdentityList).toEqual({
|
||||
moduleIdentities: [moduleIdentity],
|
||||
synchronizationStatus: SynchronizationStatus.fetched});
|
||||
});
|
||||
|
||||
it (`handles ${GET_MODULE_IDENTITIES}/ACTION_FAILED action`, () => {
|
||||
const action = getModuleIdentitiesAction.failed({error: -1, params: deviceId});
|
||||
expect(reducer(deviceContentStateInitial(), action).moduleIdentityList.synchronizationStatus).toEqual(SynchronizationStatus.failed);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,12 +17,14 @@ import {
|
|||
patchDigitalTwinInterfacePropertiesAction,
|
||||
PatchDigitalTwinInterfacePropertiesActionParameters,
|
||||
ModelDefinitionActionResult,
|
||||
GetModelDefinitionActionParameters
|
||||
GetModelDefinitionActionParameters,
|
||||
getModuleIdentitiesAction
|
||||
} from './actions';
|
||||
import { Twin } from '../../api/models/device';
|
||||
import { DeviceIdentity } from '../../api/models/deviceIdentity';
|
||||
import { SynchronizationStatus } from '../../api/models/synchronizationStatus';
|
||||
import { DigitalTwinInterfaces } from '../../api/models/digitalTwinModels';
|
||||
import { ModuleIdentity } from './../../api/models/moduleIdentity';
|
||||
|
||||
const reducer = reducerWithInitialState<DeviceContentStateType>(deviceContentStateInitial())
|
||||
//#region DeviceIdentity-related actions
|
||||
|
@ -203,6 +205,30 @@ const reducer = reducerWithInitialState<DeviceContentStateType>(deviceContentSta
|
|||
digitalTwinInterfacePropertiesSyncStatus: SynchronizationStatus.failed
|
||||
}
|
||||
});
|
||||
})
|
||||
//#endregion
|
||||
//#region ModuleIdentity-related actions
|
||||
.case(getModuleIdentitiesAction.started, (state: DeviceContentStateType) => {
|
||||
return state.merge({
|
||||
moduleIdentityList: {
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
}
|
||||
});
|
||||
})
|
||||
.case(getModuleIdentitiesAction.done, (state: DeviceContentStateType, payload: {params: string} & {result: ModuleIdentity[]}) => {
|
||||
return state.merge({
|
||||
moduleIdentityList: {
|
||||
moduleIdentities: payload.result,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
})
|
||||
.case(getModuleIdentitiesAction.failed, (state: DeviceContentStateType) => {
|
||||
return state.merge({
|
||||
moduleIdentityList: {
|
||||
synchronizationStatus: SynchronizationStatus.failed
|
||||
}
|
||||
});
|
||||
});
|
||||
//#endregion
|
||||
export default reducer;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { invokeDigitalTwinInterfaceCommandSaga } from './sagas/digitalTwinInterf
|
|||
import { getDeviceIdentitySaga, updateDeviceIdentitySaga } from './sagas/deviceIdentitySaga';
|
||||
import { getDigitalTwinInterfacePropertySaga, patchDigitalTwinInterfacePropertiesSaga } from './sagas/digitalTwinInterfacePropertySaga';
|
||||
import { cloudToDeviceMessageSaga } from './sagas/cloudToDeviceMessageSaga';
|
||||
import { getModuleIdentitiesSaga } from './sagas/moduleIdentitySaga';
|
||||
import {
|
||||
cloudToDeviceMessageAction,
|
||||
getDeviceIdentityAction,
|
||||
|
@ -20,7 +21,8 @@ import {
|
|||
getModelDefinitionAction,
|
||||
patchDigitalTwinInterfacePropertiesAction,
|
||||
updateTwinAction,
|
||||
updateDeviceIdentityAction
|
||||
updateDeviceIdentityAction,
|
||||
getModuleIdentitiesAction
|
||||
} from './actions';
|
||||
|
||||
export default [
|
||||
|
@ -28,6 +30,7 @@ export default [
|
|||
takeLatest(getDeviceIdentityAction.started.type, getDeviceIdentitySaga),
|
||||
takeLatest(getDigitalTwinInterfacePropertiesAction.started.type, getDigitalTwinInterfacePropertySaga),
|
||||
takeLatest(getModelDefinitionAction.started.type, getModelDefinitionSaga),
|
||||
takeLatest(getModuleIdentitiesAction.started.type, getModuleIdentitiesSaga),
|
||||
takeLatest(getTwinAction.started.type, getDeviceTwinSaga),
|
||||
takeEvery(invokeDirectMethodAction.started.type, invokeDirectMethodSaga),
|
||||
takeEvery(invokeDigitalTwinInterfaceCommandAction.started.type, invokeDigitalTwinInterfaceCommandSaga),
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import { SagaIteratorClone, cloneableGenerator } from 'redux-saga/utils';
|
||||
import { select, call, put } from 'redux-saga/effects';
|
||||
import { getModuleIdentitiesSaga } from './moduleIdentitySaga';
|
||||
import { getModuleIdentitiesAction } from '../actions';
|
||||
import { getConnectionStringSelector } from '../../../login/selectors';
|
||||
import * as DevicesService from '../../../api/services/devicesService';
|
||||
import { addNotificationAction } from '../../../notifications/actions';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
import { ModuleIdentity } from '../../../api/models/moduleIdentity';
|
||||
|
||||
describe('moduleIdentitySaga', () => {
|
||||
let getModuleIdentitySagaGenerator: SagaIteratorClone;
|
||||
|
||||
const connectionString = 'connection_string';
|
||||
const deviceId = 'device_id';
|
||||
|
||||
const mockModuleIdentities: ModuleIdentity[] = [{
|
||||
authentication: null,
|
||||
deviceId: 'testDevice',
|
||||
moduleId: 'testModule'
|
||||
}];
|
||||
|
||||
describe('getModuleIdentitiesSaga', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
getModuleIdentitySagaGenerator = cloneableGenerator(getModuleIdentitiesSaga)(getModuleIdentitiesAction.started(deviceId));
|
||||
});
|
||||
|
||||
const mockFetchModuleIdentities = jest.spyOn(DevicesService, 'fetchModuleIdentities').mockImplementationOnce(parameters => {
|
||||
return null;
|
||||
});
|
||||
|
||||
it('fetches the connection string', () => {
|
||||
expect(getModuleIdentitySagaGenerator.next()).toEqual({
|
||||
done: false,
|
||||
value: select(getConnectionStringSelector)
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches the module identities', () => {
|
||||
expect(getModuleIdentitySagaGenerator.next(connectionString)).toEqual({
|
||||
done: false,
|
||||
value: call(mockFetchModuleIdentities, { connectionString, deviceId })
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the successful action', () => {
|
||||
const success = getModuleIdentitySagaGenerator.clone();
|
||||
expect(success.next(mockModuleIdentities)).toEqual({
|
||||
done: false,
|
||||
value: put(getModuleIdentitiesAction.done({params: deviceId, result: mockModuleIdentities}))
|
||||
});
|
||||
expect(success.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails on error', () => {
|
||||
const failure = getModuleIdentitySagaGenerator.clone();
|
||||
const error = { code: -1 };
|
||||
expect(failure.throw(error)).toEqual({
|
||||
done: false,
|
||||
value: put(addNotificationAction.started({
|
||||
text: {
|
||||
translationKey: ResourceKeys.notifications.getModuleIdentitiesOnError,
|
||||
translationOptions: {
|
||||
deviceId,
|
||||
error,
|
||||
},
|
||||
},
|
||||
type: NotificationType.error
|
||||
}))
|
||||
});
|
||||
|
||||
expect(failure.next(error)).toEqual({
|
||||
done: false,
|
||||
value: put(getModuleIdentitiesAction.failed({params: deviceId, error}))
|
||||
});
|
||||
expect(failure.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { call, put, select } from 'redux-saga/effects';
|
||||
import { Action } from 'typescript-fsa';
|
||||
import { fetchModuleIdentities } from '../../../api/services/devicesService';
|
||||
import { addNotificationAction } from '../../../notifications/actions';
|
||||
import { NotificationType } from '../../../api/models/notification';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { getConnectionStringSelector } from '../../../login/selectors';
|
||||
import { getModuleIdentitiesAction } from '../actions';
|
||||
|
||||
export function* getModuleIdentitiesSaga(action: Action<string>) {
|
||||
try {
|
||||
const parameters = {
|
||||
connectionString: yield select(getConnectionStringSelector),
|
||||
deviceId: action.payload,
|
||||
};
|
||||
|
||||
const moduleIdentities = yield call(fetchModuleIdentities, parameters);
|
||||
|
||||
yield put(getModuleIdentitiesAction.done({params: action.payload, result: moduleIdentities}));
|
||||
} catch (error) {
|
||||
yield put(addNotificationAction.started({
|
||||
text: {
|
||||
translationKey: ResourceKeys.notifications.getModuleIdentitiesOnError,
|
||||
translationOptions: {
|
||||
deviceId: action.payload,
|
||||
error,
|
||||
},
|
||||
},
|
||||
type: NotificationType.error
|
||||
}));
|
||||
|
||||
yield put(getModuleIdentitiesAction.failed({params: action.payload, error}));
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
import 'jest';
|
||||
import { Record } from 'immutable';
|
||||
import { SynchronizationStatus } from './../../api/models/synchronizationStatus';
|
||||
import { getDigitalTwinInterfacePropertiesSelector, getDigitalTwinInterfaceNameAndIdsSelector, getDigitalTwinInterfaceIdsSelector, getIsDevicePnpSelector, getInterfaceNameSelector } from './selectors';
|
||||
import { getDigitalTwinInterfacePropertiesSelector, getDigitalTwinInterfaceNameAndIdsSelector, getDigitalTwinInterfaceIdsSelector, getIsDevicePnpSelector, getInterfaceNameSelector, getModuleIdentityListWrapperSelector } from './selectors';
|
||||
import { getInitialState } from './../../api/shared/testHelper';
|
||||
|
||||
describe('getDigitalTwinInterfacePropertiesSelector', () => {
|
||||
|
@ -42,7 +42,15 @@ describe('getDigitalTwinInterfacePropertiesSelector', () => {
|
|||
digitalTwinInterfacePropertiesSyncStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
interfaceIdSelected: 'urn:contoso:com:environmentalsensor:2',
|
||||
modelDefinitionWithSource: null
|
||||
modelDefinitionWithSource: null,
|
||||
moduleIdentityList: {
|
||||
moduleIdentities: [{
|
||||
authentication: null,
|
||||
deviceId: 'testDevice',
|
||||
moduleId: 'testModule'
|
||||
}],
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
}
|
||||
})();
|
||||
|
||||
it('returns interface properties', () => {
|
||||
|
@ -76,4 +84,15 @@ describe('getDigitalTwinInterfacePropertiesSelector', () => {
|
|||
it('returns is correct interfaceName pnp', () => {
|
||||
expect(getInterfaceNameSelector(state)).toEqual('environmentalsensor');
|
||||
});
|
||||
|
||||
it('returns is correct interfaceName pnp', () => {
|
||||
expect(getModuleIdentityListWrapperSelector(state)).toEqual({
|
||||
moduleIdentities: [{
|
||||
authentication: null,
|
||||
deviceId: 'testDevice',
|
||||
moduleId: 'testModule'
|
||||
}],
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { DigitalTwinInterfaces } from '../../api/models/digitalTwinModels';
|
|||
import { StateType, StateInterface } from '../../shared/redux/state';
|
||||
import { ModelDefinitionWithSourceWrapper } from '../../api/models/modelDefinitionWithSourceWrapper';
|
||||
import { modelDiscoveryInterfaceName } from '../../constants/modelDefinitionConstants';
|
||||
import { ModuleIdentityListWrapper } from '../../api/models/moduleIdentityListWrapper';
|
||||
|
||||
export const getInterfaceIdSelector = (state: StateInterface): string => {
|
||||
return state && state.deviceContentState && state.deviceContentState.interfaceIdSelected;
|
||||
|
@ -113,3 +114,9 @@ export const getInterfaceNameSelector = createSelector(
|
|||
return idToNameMap.get(id);
|
||||
}
|
||||
);
|
||||
|
||||
export const getModuleIdentityListWrapperSelector = (state: StateInterface): ModuleIdentityListWrapper => {
|
||||
return state &&
|
||||
state.deviceContentState &&
|
||||
state.deviceContentState.moduleIdentityList;
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ModelDefinitionWithSourceWrapper } from '../../api/models/modelDefiniti
|
|||
import { DeviceIdentityWrapper } from '../../api/models/deviceIdentityWrapper';
|
||||
import { DeviceTwinWrapper } from './../../api/models/deviceTwinWrapper';
|
||||
import { DigitalTwinInterfacePropertiesWrapper } from '../../api/models/digitalTwinInterfacePropertiesWrapper';
|
||||
import { ModuleIdentityListWrapper } from './../../api/models/moduleIdentityListWrapper';
|
||||
|
||||
export interface DeviceContentStateInterface {
|
||||
deviceIdentity: DeviceIdentityWrapper;
|
||||
|
@ -15,6 +16,7 @@ export interface DeviceContentStateInterface {
|
|||
digitalTwinInterfaceProperties: DigitalTwinInterfacePropertiesWrapper;
|
||||
interfaceIdSelected: string;
|
||||
modelDefinitionWithSource: ModelDefinitionWithSourceWrapper;
|
||||
moduleIdentityList: ModuleIdentityListWrapper;
|
||||
}
|
||||
|
||||
export const deviceContentStateInitial = Record<DeviceContentStateInterface>({
|
||||
|
@ -23,6 +25,7 @@ export const deviceContentStateInitial = Record<DeviceContentStateInterface>({
|
|||
digitalTwinInterfaceProperties: null,
|
||||
interfaceIdSelected: '',
|
||||
modelDefinitionWithSource: null,
|
||||
moduleIdentityList: null
|
||||
});
|
||||
|
||||
export type DeviceContentStateType = IM<DeviceContentStateInterface>;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Redirect, RouteComponentProps, withRouter, NavLink, Route } from 'react-router-dom';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { PrimaryButton, ActionButton } from 'office-ui-fabric-react/lib/Button';
|
||||
|
@ -18,6 +19,7 @@ import DeviceListQuery from './deviceListQuery';
|
|||
import { DeviceListCell } from './deviceListCell';
|
||||
import ListPaging from './listPaging';
|
||||
import { ROUTE_PARTS, ROUTE_PARAMS } from '../../../constants/routes';
|
||||
import { CHECK } from '../../../constants/iconNames';
|
||||
import '../../../css/_deviceList.scss';
|
||||
import '../../../css/_layouts.scss';
|
||||
|
||||
|
@ -65,7 +67,7 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
|
|||
<div className="view-command">
|
||||
{this.showCommandBar()}
|
||||
</div>
|
||||
<div className="view-content view-scroll">
|
||||
<div className="view-content view-scroll-vertical">
|
||||
<DeviceListQuery
|
||||
executeQuery={this.executeQuery}
|
||||
query={this.state.query}
|
||||
|
@ -232,6 +234,21 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
|
|||
);
|
||||
},
|
||||
widthPercentage: 15
|
||||
},
|
||||
{
|
||||
name: context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.label),
|
||||
onRenderColumn: (group, key) => {
|
||||
const isEdge = (group.data as DeviceSummary).iotEdge;
|
||||
return (
|
||||
<Icon
|
||||
key={key}
|
||||
iconName={isEdge && CHECK}
|
||||
ariaLabel={isEdge ?
|
||||
context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.yes) : context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.no)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
widthPercentage: 5
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -294,118 +294,9 @@ exports[`components/GroupedList render GroupedList matches snapshot when in a lo
|
|||
className="grouped-list-header-border"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<StyledShimmerBase
|
||||
key="0"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="1"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="2"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="3"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="4"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="5"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="6"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="7"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="8"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<StyledShimmerBase
|
||||
key="9"
|
||||
shimmerElements={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"type": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Component
|
||||
shimmerCount={10}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
import * as React from 'react';
|
||||
import { GroupedList, IGroup, IGroupDividerProps } from 'office-ui-fabric-react/lib/GroupedList';
|
||||
import { IListProps } from 'office-ui-fabric-react/lib/List';
|
||||
import { Shimmer, ShimmerElementType } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
|
||||
import { IconButton, BaseButton, Button } from 'office-ui-fabric-react/lib/Button';
|
||||
import { ISelection, SelectionMode, Selection, SelectionZone } from 'office-ui-fabric-react/lib/Selection';
|
||||
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';
|
||||
import LabelWithTooltip from '../labelWithTooltip';
|
||||
import { GroupedList as GroupedListIconNames } from '../../../constants/iconNames';
|
||||
import { CHECKBOX_WIDTH_PERCENTAGE, GRID_STYLE_CONSTANTS, CHECKBOX_WIDTH_PIXELS, LABEL_FONT_SIZE, SHIMMER_HEIGHT } from './groupedListStyleConstants';
|
||||
import { CHECKBOX_WIDTH_PERCENTAGE, GRID_STYLE_CONSTANTS, CHECKBOX_WIDTH_PIXELS, LABEL_FONT_SIZE } from './groupedListStyleConstants';
|
||||
import MultiLineShimmer from '../multiLineShimmer';
|
||||
import '../../../css/_groupedList.scss';
|
||||
|
||||
const SHIMMER_COUNT = 10;
|
||||
|
@ -56,19 +56,12 @@ export default class GroupedListWrapper<T> extends React.Component<GroupedListPr
|
|||
const { columnInfo, items, isLoading, noItemsMessage } = this.props;
|
||||
const { selection, selectionMode } = this.state;
|
||||
|
||||
const shimmer = [];
|
||||
for (let index = 0; index < SHIMMER_COUNT; index++) {
|
||||
shimmer.push(this.getShimmer(index));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grouped-list">
|
||||
{this.renderListHeader(columnInfo)}
|
||||
{!!isLoading ? (
|
||||
<div>
|
||||
{shimmer}
|
||||
</div>
|
||||
) : !items || items.length === 0 ? (
|
||||
{!!isLoading ?
|
||||
<MultiLineShimmer shimmerCount={SHIMMER_COUNT}/>
|
||||
: !items || items.length === 0 ? (
|
||||
<h3>{noItemsMessage}</h3>
|
||||
) : (
|
||||
<MarqueeSelection selection={selection} isEnabled={selection.mode === SelectionMode.multiple}>
|
||||
|
@ -261,16 +254,4 @@ export default class GroupedListWrapper<T> extends React.Component<GroupedListPr
|
|||
gridTemplateColumns: columnString
|
||||
};
|
||||
}
|
||||
|
||||
private readonly getShimmer = (key: string | number) => {
|
||||
return (
|
||||
<Shimmer
|
||||
shimmerElements={[
|
||||
{ type: ShimmerElementType.line, height: SHIMMER_HEIGHT }
|
||||
]}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,4 +12,3 @@ export enum GRID_STYLE_CONSTANTS {
|
|||
export const LABEL_FONT_SIZE = 12;
|
||||
export const CHECKBOX_WIDTH_PIXELS = 14;
|
||||
export const CHECKBOX_WIDTH_PERCENTAGE = 2;
|
||||
export const SHIMMER_HEIGHT = 20;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import MultiLineShimmer from './multiLineShimmer';
|
||||
import { mountWithLocalization } from '../utils/testHelpers';
|
||||
|
||||
describe('shared/components/multiLineShimmer', () => {
|
||||
it('renders shimmers properly', () => {
|
||||
let wrapper = mountWithLocalization(<MultiLineShimmer/>).find(MultiLineShimmer);
|
||||
let shimmerDiv = wrapper.find('div.fixed-shimmer');
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
expect(shimmerDiv.find(Shimmer)).toHaveLength(3);
|
||||
|
||||
const shimmerCount = 10;
|
||||
wrapper = mountWithLocalization(<MultiLineShimmer className="non-fixed-shimmer" shimmerCount={shimmerCount}/>).find(MultiLineShimmer);
|
||||
shimmerDiv = wrapper.find('div.non-fixed-shimmer');
|
||||
expect(shimmerDiv.find(Shimmer)).toHaveLength(shimmerCount);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Shimmer, ShimmerElementType } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
|
||||
const SHIMMER_HEIGHT = 20;
|
||||
const SHIMMER_COUNT = 3;
|
||||
|
||||
export interface MultiLineShimmerProps {
|
||||
className?: string ;
|
||||
shimmerCount?: number;
|
||||
}
|
||||
|
||||
export default (props: MultiLineShimmerProps) => {
|
||||
const shimmerCount = props.shimmerCount || SHIMMER_COUNT;
|
||||
const className = props.className || 'fixed-shimmer';
|
||||
const shimmers = [];
|
||||
for (let i = 0; i < shimmerCount; i++) {
|
||||
shimmers.push(
|
||||
<Shimmer
|
||||
key={i}
|
||||
style={{paddingLeft: 10, paddingRight: 10}}
|
||||
shimmerElements={[
|
||||
{ type: ShimmerElementType.line, height: SHIMMER_HEIGHT }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
{shimmers}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -51,9 +51,9 @@
|
|||
}
|
||||
},
|
||||
"modelDefinitions": {
|
||||
"add": "New",
|
||||
"headerText": "We'll look for your model definition in the following locations (in order):",
|
||||
"helpText": "Drag and drop to change the order.",
|
||||
"add": "Add module definition source",
|
||||
"headerText": "Plug and Play configurations",
|
||||
"helpText": "We'll look for your model definition in the following order. Please drag and drop to change it.",
|
||||
"repositoryTypes": {
|
||||
"public": {
|
||||
"label":"Public repository",
|
||||
|
@ -113,7 +113,8 @@
|
|||
"nonpnp": "DEVICE",
|
||||
"add": "Create device identity",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand"
|
||||
"expand": "Expand",
|
||||
"moduleIdentity": "Module identity"
|
||||
},
|
||||
"value": "Value"
|
||||
},
|
||||
|
@ -132,7 +133,7 @@
|
|||
"ariaLabel": "check box for remember my connection string",
|
||||
"tooltip": "The connection string will be stored in the browser local storage. (We will remember up to 5 hub connection strings)"
|
||||
},
|
||||
"notes": "For Plug and Play devices, we'll look for your model definition in the public repository first, and then we’ll search your connected devices. To change how and where we search, open Settings.",
|
||||
"notes": "You can open Settings to change application configurations anytime.",
|
||||
"saveButton": {
|
||||
"label": "Connect"
|
||||
},
|
||||
|
@ -483,6 +484,20 @@
|
|||
},
|
||||
"output" : "Output"
|
||||
},
|
||||
"moduleIdentity": {
|
||||
"headerText": "Module Identities",
|
||||
"command" : {
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"columns": {
|
||||
"moduleId": "Module Id",
|
||||
"connectionState": "Connection state",
|
||||
"connectionStateLastUpdated": "Connection state last updated time",
|
||||
"lastActivityTime": "Last activity time"
|
||||
},
|
||||
"noModules": "There are no module identities for this device.",
|
||||
"errorFetching": "An error prevented this information from being loaded."
|
||||
},
|
||||
"noMatchError": {
|
||||
"title": "404 - Not found",
|
||||
"description": "The page you are looking is unavailable.",
|
||||
|
@ -495,6 +510,7 @@
|
|||
"getDeviceListGenericErrorHelp": "Failed to retrieve device list. It is possibly due to an invalid IoT hub connection string",
|
||||
"getDeviceTwinOnError": "Failed to get device {{deviceId}}'s twin: {{error}}",
|
||||
"getDigitalTwinInterfacePropertiesOnError": "Failed to get properties of digital twin interfaces on device {{deviceId}}: {{error}}",
|
||||
"getModuleIdentitiesOnError": "Failed to get module identities of device {{deviceId}}: {{error}}",
|
||||
"getInterfaceModelOnError": "Failed to get interface model definition {{interfaceId}}",
|
||||
"updateDeviceTwinOnError": "Failed to update device twin on device {{deviceId}}: {{error}}",
|
||||
"updateDeviceTwinOnSuccess": "Successfully updated device twin on device {{deviceId}}.",
|
||||
|
|
|
@ -108,6 +108,7 @@ export class ResourceKeys {
|
|||
identity : "deviceContent.navBar.identity",
|
||||
interfaces : "deviceContent.navBar.interfaces",
|
||||
methods : "deviceContent.navBar.methods",
|
||||
moduleIdentity : "deviceContent.navBar.moduleIdentity",
|
||||
nonpnp : "deviceContent.navBar.nonpnp",
|
||||
pnp : "deviceContent.navBar.pnp",
|
||||
properties : "deviceContent.navBar.properties",
|
||||
|
@ -437,6 +438,20 @@ export class ResourceKeys {
|
|||
launch : "header.settings.launch",
|
||||
},
|
||||
};
|
||||
public static moduleIdentity = {
|
||||
columns : {
|
||||
connectionState : "moduleIdentity.columns.connectionState",
|
||||
connectionStateLastUpdated : "moduleIdentity.columns.connectionStateLastUpdated",
|
||||
lastActivityTime : "moduleIdentity.columns.lastActivityTime",
|
||||
moduleId : "moduleIdentity.columns.moduleId",
|
||||
},
|
||||
command : {
|
||||
refresh : "moduleIdentity.command.refresh",
|
||||
},
|
||||
errorFetching : "moduleIdentity.errorFetching",
|
||||
headerText : "moduleIdentity.headerText",
|
||||
noModules : "moduleIdentity.noModules",
|
||||
};
|
||||
public static noMatchError = {
|
||||
description : "noMatchError.description",
|
||||
goHome : "noMatchError.goHome",
|
||||
|
@ -455,6 +470,7 @@ export class ResourceKeys {
|
|||
getDeviceTwinOnError : "notifications.getDeviceTwinOnError",
|
||||
getDigitalTwinInterfacePropertiesOnError : "notifications.getDigitalTwinInterfacePropertiesOnError",
|
||||
getInterfaceModelOnError : "notifications.getInterfaceModelOnError",
|
||||
getModuleIdentitiesOnError : "notifications.getModuleIdentitiesOnError",
|
||||
interfaceSchemaNotSupported : "notifications.interfaceSchemaNotSupported",
|
||||
invokeDigitalTwinCommandOnError : "notifications.invokeDigitalTwinCommandOnError",
|
||||
invokeDigitalTwinCommandOnSuccess : "notifications.invokeDigitalTwinCommandOnSuccess",
|
||||
|
|
Загрузка…
Ссылка в новой задаче