зеркало из
1
0
Форкнуть 0
* 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:
YingXue 2019-10-31 16:14:25 -07:00 коммит произвёл Paul Montgomery
Родитель 4d825c4655
Коммит c309efe32d
48 изменённых файлов: 1027 добавлений и 225 удалений

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

@ -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 well 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",