From 3560f0736456f127dd942001e5eac5ee92e4a6f1 Mon Sep 17 00:00:00 2001 From: ying xue Date: Fri, 26 Jul 2019 22:19:10 -0700 Subject: [PATCH 1/4] remove interface id from device summary --- .../dataTransforms/deviceSummaryTransform.ts | 9 - src/app/api/models/deviceSummary.ts | 9 - .../api/models/deviceSummaryListWrapper.ts | 5 +- src/app/devices/deviceContent/selectors.ts | 22 +- src/app/devices/deviceList/actions.ts | 6 - .../__snapshots__/deviceList.spec.tsx.snap | 5 +- .../__snapshots__/addDevice.spec.tsx.snap | 1 + .../addDevice/components/addDevice.tsx | 27 ++- .../components/addDeviceContainer.tsx | 13 +- .../deviceList/components/deviceList.spec.tsx | 12 +- .../deviceList/components/deviceList.tsx | 132 +----------- .../deviceList/components/deviceListCell.tsx | 93 ++++++--- .../components/deviceListCellContainer.tsx | 28 --- .../components/deviceListContainer.tsx | 8 +- src/app/devices/deviceList/reducer.spec.ts | 69 ++++++- src/app/devices/deviceList/reducer.ts | 194 +++++++----------- src/app/devices/deviceList/sagas.ts | 6 +- ...fetchDigitalTwinInterfacePropertiesSaga.ts | 35 ---- src/app/devices/deviceList/selectors.spec.ts | 44 ++++ src/app/devices/deviceList/selectors.ts | 38 ++-- src/app/devices/deviceList/state.ts | 5 +- src/app/login/components/connectivityPane.tsx | 6 +- .../components/connectivityPaneContainer.tsx | 4 +- .../components/groupedList/groupedList.tsx | 6 +- src/server/server.ts | 13 +- 25 files changed, 341 insertions(+), 449 deletions(-) delete mode 100644 src/app/devices/deviceList/components/deviceListCellContainer.tsx delete mode 100644 src/app/devices/deviceList/sagas/fetchDigitalTwinInterfacePropertiesSaga.ts create mode 100644 src/app/devices/deviceList/selectors.spec.ts diff --git a/src/app/api/dataTransforms/deviceSummaryTransform.ts b/src/app/api/dataTransforms/deviceSummaryTransform.ts index 5d6b53339..a6b4a5658 100644 --- a/src/app/api/dataTransforms/deviceSummaryTransform.ts +++ b/src/app/api/dataTransforms/deviceSummaryTransform.ts @@ -6,17 +6,12 @@ import { DeviceSummary } from '../models/deviceSummary'; import { parseDateTimeString } from './transformHelper'; import { Device } from '../models/device'; import { DeviceIdentity } from '../models/deviceIdentity'; -import { SynchronizationStatus } from '../models/synchronizationStatus'; export const transformDevice = (device: Device): DeviceSummary => { return { authenticationType: device.AuthenticationType, cloudToDeviceMessageCount: device.CloudToDeviceMessageCount, deviceId: device.DeviceId, - deviceSummarySynchronizationStatus: SynchronizationStatus.initialized, - interfaceIds: [], - isEdgeDevice: device.IotEdge, - isPnpDevice: undefined, lastActivityTime: parseDateTimeString(device.LastActivityTime), status: device.Status, statusUpdatedTime: parseDateTimeString(device.StatusUpdatedTime) @@ -28,10 +23,6 @@ export const transformDeviceIdentity = (device: DeviceIdentity): DeviceSummary = authenticationType: device.authentication.type, cloudToDeviceMessageCount: device.cloudToDeviceMessageCount, deviceId: device.deviceId, - deviceSummarySynchronizationStatus: SynchronizationStatus.initialized, - interfaceIds: [], - isEdgeDevice: device.capabilities.iotEdge, - isPnpDevice: false, // device created using add device api won't be pnp device lastActivityTime: parseDateTimeString(device.lastActivityTime), status: device.status, statusUpdatedTime: parseDateTimeString(device.statusUpdatedTime) diff --git a/src/app/api/models/deviceSummary.ts b/src/app/api/models/deviceSummary.ts index 76054247d..77a4a0e6b 100644 --- a/src/app/api/models/deviceSummary.ts +++ b/src/app/api/models/deviceSummary.ts @@ -2,20 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { SynchronizationStatus } from './synchronizationStatus'; - export interface DeviceSummary { deviceId: string; - isEdgeDevice?: boolean; - isPnpDevice: boolean; status: string; lastActivityTime: string; statusUpdatedTime: string; cloudToDeviceMessageCount: string; authenticationType: string; - - // Interfaces - interfaceIds: string[]; - - deviceSummarySynchronizationStatus: SynchronizationStatus; } diff --git a/src/app/api/models/deviceSummaryListWrapper.ts b/src/app/api/models/deviceSummaryListWrapper.ts index b7b80fdc1..42a291257 100644 --- a/src/app/api/models/deviceSummaryListWrapper.ts +++ b/src/app/api/models/deviceSummaryListWrapper.ts @@ -2,12 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { List, Map } from 'immutable'; +import { Map } from 'immutable'; import { ErrorResponse } from './errorResponse'; import { SynchronizationStatus } from './synchronizationStatus'; +import { DeviceSummary } from './deviceSummary'; export interface DeviceSummaryListWrapper { - deviceList?: List>; // tslint:disable-line:no-any + deviceList?: Map; deviceListSynchronizationStatus: SynchronizationStatus; error?: ErrorResponse; } diff --git a/src/app/devices/deviceContent/selectors.ts b/src/app/devices/deviceContent/selectors.ts index 4d22bd21b..cf68483d9 100644 --- a/src/app/devices/deviceContent/selectors.ts +++ b/src/app/devices/deviceContent/selectors.ts @@ -66,19 +66,23 @@ export const getDigitalTwinInterfacePropertiesSelector = (state: StateInterface) export const getDigitalTwinInterfaceNameAndIdsSelector = createSelector( getDigitalTwinInterfacePropertiesSelector, - // tslint:disable-next-line:cyclomatic-complexity properties => { - return properties && - properties.interfaces && - properties.interfaces[modelDiscoveryInterfaceName] && - properties.interfaces[modelDiscoveryInterfaceName].properties && - properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation && - properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported && - properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value && - properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces; + return getReportedInterfacesFromDigitalTwin(properties); } ); +// tslint:disable-next-line:cyclomatic-complexity +export const getReportedInterfacesFromDigitalTwin = (properties: DigitalTwinInterfaces) => { + return properties && + properties.interfaces && + properties.interfaces[modelDiscoveryInterfaceName] && + properties.interfaces[modelDiscoveryInterfaceName].properties && + properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation && + properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported && + properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value && + properties.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces; +}; + const getDigitalTwinInterfaceIdToNameMapSelector = createSelector( getDigitalTwinInterfaceNameAndIdsSelector, nameAndIds => { diff --git a/src/app/devices/deviceList/actions.ts b/src/app/devices/deviceList/actions.ts index 4b380e7b7..39b81ed2b 100644 --- a/src/app/devices/deviceList/actions.ts +++ b/src/app/devices/deviceList/actions.ts @@ -9,7 +9,6 @@ import { DeviceSummary } from '../../api/models/deviceSummary'; import { BulkRegistryOperationResult } from '../../api/models/bulkRegistryOperationResult'; import { DeviceIdentity } from '../../api/models/deviceIdentity'; import DeviceQuery from '../../api/models/deviceQuery'; -import { DigitalTwinInterfaces } from '../../api/models/digitalTwinModels'; const deviceListCreator = actionCreatorFactory(actionPrefixes.DEVICELISTS); const clearDevicesAction = deviceListCreator(actionTypes.CLEAR_DEVICES); @@ -17,14 +16,9 @@ const listDevicesAction = deviceListCreator.async( const deleteDevicesAction = deviceListCreator.async(actionTypes.DELETE_DEVICES); const addDeviceAction = deviceListCreator.async(actionTypes.ADD_DEVICE); -const setIsPnpAction = deviceListCreator.async(actionTypes.SET_ISPNP); -const fetchInterfacesAction = deviceListCreator.async(actionTypes.FETCH_INTERFACES); - export { addDeviceAction, clearDevicesAction, deleteDevicesAction, listDevicesAction, - setIsPnpAction, - fetchInterfacesAction }; diff --git a/src/app/devices/deviceList/components/__snapshots__/deviceList.spec.tsx.snap b/src/app/devices/deviceList/components/__snapshots__/deviceList.spec.tsx.snap index 5ceb9d241..33242a0b7 100644 --- a/src/app/devices/deviceList/components/__snapshots__/deviceList.spec.tsx.snap +++ b/src/app/devices/deviceList/components/__snapshots__/deviceList.spec.tsx.snap @@ -4,22 +4,19 @@ exports[`components/devices/DeviceList matches snapshot 1`] = ` diff --git a/src/app/devices/deviceList/components/addDevice/components/__snapshots__/addDevice.spec.tsx.snap b/src/app/devices/deviceList/components/addDevice/components/__snapshots__/addDevice.spec.tsx.snap index c072c64e6..2f48155ea 100644 --- a/src/app/devices/deviceList/components/addDevice/components/__snapshots__/addDevice.spec.tsx.snap +++ b/src/app/devices/deviceList/components/addDevice/components/__snapshots__/addDevice.spec.tsx.snap @@ -150,6 +150,7 @@ exports[`components/devices/addDevice matches snapshot 1`] = ` ], } } + value="" />
void; - listDevices: () => void; +} + +export interface AddDeviceDataProps { + deviceListSyncStatus: SynchronizationStatus; } export interface AddDeviceState { @@ -42,18 +46,18 @@ export interface AddDeviceState { secondaryThumbprintError?: string; } -export default class AddDevice extends React.Component { - constructor(props: AddDeviceActionProps & RouteComponentProps) { +export default class AddDevice extends React.Component { + constructor(props: AddDeviceActionProps & AddDeviceDataProps & RouteComponentProps) { super(props); this.state = { authenticationType: DeviceAuthenticationType.SymmetricKey, autoGenerateKeys: true, - deviceId: undefined, - primaryKey: undefined, - primaryThumbprint: undefined, - secondaryKey: undefined, - secondaryThumbprint: undefined, + deviceId: '', + primaryKey: '', + primaryThumbprint: '', + secondaryKey: '', + secondaryThumbprint: '', status: DeviceStatus.Enabled }; } @@ -81,8 +85,10 @@ export default class AddDevice extends React.Component { @@ -385,7 +391,6 @@ export default class AddDevice extends React.Component { this.props.handleSave(this.getDeviceIdentity()); - this.props.history.push(`/devices`); } private readonly handleCancel = () => { diff --git a/src/app/devices/deviceList/components/addDevice/components/addDeviceContainer.tsx b/src/app/devices/deviceList/components/addDevice/components/addDeviceContainer.tsx index c713a1a1a..f7f6cc8bd 100644 --- a/src/app/devices/deviceList/components/addDevice/components/addDeviceContainer.tsx +++ b/src/app/devices/deviceList/components/addDevice/components/addDeviceContainer.tsx @@ -4,17 +4,24 @@ **********************************************************/ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import AddDevice, { AddDeviceActionProps } from './addDevice'; +import AddDevice, { AddDeviceActionProps, AddDeviceDataProps } from './addDevice'; import { DeviceIdentity } from '../../../../../api/models/deviceIdentity'; import { addDeviceAction, listDevicesAction } from '../../../actions'; +import { StateType } from '../../../../../shared/redux/state'; +import { getDeviceSummaryListStatus } from '../../../selectors'; const mapDispatchToProps = (dispatch: Dispatch): AddDeviceActionProps => { return { handleSave: (deviceIdentity: DeviceIdentity) => dispatch(addDeviceAction.started(deviceIdentity)), - listDevices: () => dispatch(listDevicesAction.started(undefined)) }; }; -export default connect(undefined, mapDispatchToProps, undefined, { +const mapStateToProps = (state: StateType): AddDeviceDataProps => { + return { + deviceListSyncStatus: getDeviceSummaryListStatus(state) + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps, undefined, { pure: false, })(AddDevice); diff --git a/src/app/devices/deviceList/components/deviceList.spec.tsx b/src/app/devices/deviceList/components/deviceList.spec.tsx index 8a6454b41..10824d1f8 100644 --- a/src/app/devices/deviceList/components/deviceList.spec.tsx +++ b/src/app/devices/deviceList/components/deviceList.spec.tsx @@ -7,21 +7,19 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import DeviceListComponent from './deviceList'; import { testWithLocalizationContext } from '../../../shared/utils/testHelpers'; -import { SynchronizationStatus } from '../../../api/models/synchronizationStatus'; +import { DeviceSummary } from '../../../api/models/deviceSummary'; -const devices = [ +const devices: DeviceSummary[] = [ { authenticationType: 'sas', cloudToDeviceMessageCount: '0', deviceId: 'testDeviceId', - deviceSummarySynchronizationStatus: SynchronizationStatus.fetched, - interfaceIds: [], - isPnpDevice: true, lastActivityTime: '0001-01-01T00:00:00Z', status: 'Enabled', statusUpdatedTime: '0001-01-01T00:00:00Z' } ]; +jest.mock('./deviceListCell', () => <>); describe('components/devices/DeviceList', () => { it('matches snapshot', () => { @@ -31,7 +29,7 @@ describe('components/devices/DeviceList', () => { devices={devices} listDevices={jest.fn()} deleteDevices={jest.fn()} - deviceListSyncStatus={SynchronizationStatus.fetched} + isFetching={false} />); expect(wrapper).toMatchSnapshot(); @@ -44,7 +42,7 @@ describe('components/devices/DeviceList', () => { devices={[]} listDevices={jest.fn()} deleteDevices={jest.fn()} - deviceListSyncStatus={SynchronizationStatus.fetched} + isFetching={false} />); const child = shallow(wrapper.props().children()); diff --git a/src/app/devices/deviceList/components/deviceList.tsx b/src/app/devices/deviceList/components/deviceList.tsx index 2ee72cc73..068989129 100644 --- a/src/app/devices/deviceList/components/deviceList.tsx +++ b/src/app/devices/deviceList/components/deviceList.tsx @@ -6,30 +6,23 @@ import * as React from 'react'; import { Redirect, RouteComponentProps, withRouter, NavLink, Route } from 'react-router-dom'; import { Label } from 'office-ui-fabric-react/lib/Label'; import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react/lib/Dialog'; -import { IColumn } from 'office-ui-fabric-react/lib/DetailsList'; -import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext'; import { ResourceKeys } from '../../../../localization/resourceKeys'; import GroupedListWrapper from '../../../shared/components/groupedList'; -import { DEVICE_LIST_COLUMN_WIDTH, DEVICE_LIST_WIDE_COLUMN_WIDTH } from '../../../constants/devices'; -import { CHECK } from '../../../constants/iconNames'; -import { SynchronizationStatus } from '../../../api/models/synchronizationStatus'; import { DeviceSummary } from '../../../api/models/deviceSummary'; import DeviceQuery from '../../../api/models/deviceQuery'; import DeviceListCommandBar from './deviceListCommandBar'; -import { DeviceAuthenticationType } from '../../../api/models/deviceAuthenticationType'; -import { DeviceStatus } from '../../../api/models/deviceStatus'; import BreadcrumbContainer from '../../../shared/components/breadcrumbContainer'; import DeviceListQuery from './deviceListQuery'; +import { DeviceListCell } from './deviceListCell'; import '../../../css/_deviceList.scss'; import '../../../css/_layouts.scss'; -import DeviceListCellContainer from './deviceListCellContainer'; export interface DeviceListDataProps { connectionString: string; devices?: DeviceSummary[]; - deviceListSyncStatus: SynchronizationStatus; + isFetching: boolean; query?: DeviceQuery; } @@ -40,7 +33,7 @@ export interface DeviceListDispatchProps { interface DeviceListState { selectedDeviceIds: string[]; - showDeleteConfirmation?: boolean; + showDeleteConfirmation: boolean; query: DeviceQuery; } @@ -51,6 +44,7 @@ class DeviceListComponent extends React.Component {this.showDeviceList(context)} - {this.deleteConfirmationDialog(context)} + {this.state.showDeleteConfirmation && this.deleteConfirmationDialog(context)}
)} @@ -84,10 +78,6 @@ class DeviceListComponent extends React.Component { this.setState( { @@ -105,8 +95,8 @@ class DeviceListComponent extends React.Component { return ( { return ( - ); }; @@ -144,7 +134,7 @@ class DeviceListComponent extends React.Component { - return [ - this.getColumnProps('deviceId', this.onDeviceIdColumnRender), - this.getColumnProps('isEdgeDevice', this.onIsEdgeDeviceColumnRender), - this.getColumnProps('isPnpDevice', this.onIsPnpDeviceColumnRender), - this.getColumnProps('status', this.onStatusColumnRender), - this.getColumnProps('lastActivityTime', this.onLastActivityColumnRender, DEVICE_LIST_WIDE_COLUMN_WIDTH), - this.getColumnProps('statusUpdatedTime', this.onStatusUpdatedTimeColumnRender, DEVICE_LIST_WIDE_COLUMN_WIDTH), - this.getColumnProps('cloudToDeviceMessageCount', this.onC2DMessageCountColumnRender), - this.getColumnProps('authenticationType', this.onAuthTypeColumnRender) - ]; - } - private readonly onRowSelection = (devices: DeviceSummary[]) => { this.setState({ selectedDeviceIds: devices.map(device => device.deviceId) }); } - - private readonly getColumnProps = (key: string, onRender: any, columnWidth?: number): IColumn => { // tslint:disable-line:no-any - return { - isResizable: true, - key, - maxWidth: columnWidth || DEVICE_LIST_COLUMN_WIDTH, - minWidth: 50, - name: (ResourceKeys.deviceLists.columns as any)[key], // tslint:disable-line:no-any - onRender - }; - } - - private readonly onDeviceIdColumnRender = (item: DeviceSummary) => { - const path = this.props.location.pathname.replace(/\/devices\/.*/, '/devices'); - return ( - - {item.deviceId} - - ); - } - - private readonly onIsEdgeDeviceColumnRender = (item: DeviceSummary) => { - return item.isEdgeDevice && ; - } - - private readonly onIsPnpDeviceColumnRender = (item: DeviceSummary) => { - return item.isPnpDevice && ; - } - - private readonly onStatusColumnRender = (item: DeviceSummary) => { - let status: string; - switch (item.status && item.status.toLowerCase()) { - case DeviceStatus.Enabled.toLowerCase(): - status = ResourceKeys.deviceIdentity.hubConnectivity.enabled; - break; - case DeviceStatus.Disabled.toLowerCase(): - status = ResourceKeys.deviceIdentity.hubConnectivity.disabled; - break; - default: - status = undefined; - break; - } - - return ( - - {(context: LocalizationContextInterface) => ( - {status && context.t(status)} - )} - ); - } - - private readonly onLastActivityColumnRender = (item: DeviceSummary) => { - return {item.lastActivityTime}; - } - - private readonly onStatusUpdatedTimeColumnRender = (item: DeviceSummary) => { - return {item.statusUpdatedTime}; - } - - private readonly onC2DMessageCountColumnRender = (item: DeviceSummary) => { - return {item.cloudToDeviceMessageCount}; - } - - private readonly onAuthTypeColumnRender = (item: DeviceSummary) => { - let authentication: string; - switch (item.authenticationType && item.authenticationType.toLowerCase()) { - case DeviceAuthenticationType.CACertificate.toLowerCase(): - authentication = ResourceKeys.deviceIdentity.authenticationType.ca.type; - break; - case DeviceAuthenticationType.SelfSigned.toLowerCase(): - authentication = ResourceKeys.deviceIdentity.authenticationType.selfSigned.type; - break; - case DeviceAuthenticationType.SymmetricKey.toLowerCase(): - authentication = ResourceKeys.deviceIdentity.authenticationType.symmetricKey.type; - break; - default: - authentication = undefined; - break; - } - - return ( - - {(context: LocalizationContextInterface) => ( - {authentication && context.t(authentication)} - )} - ); - } } export default withRouter(DeviceListComponent); diff --git a/src/app/devices/deviceList/components/deviceListCell.tsx b/src/app/devices/deviceList/components/deviceListCell.tsx index b6b94680b..71e625289 100644 --- a/src/app/devices/deviceList/components/deviceListCell.tsx +++ b/src/app/devices/deviceList/components/deviceListCell.tsx @@ -3,13 +3,17 @@ import { Icon, registerIcons } from 'office-ui-fabric-react'; import { DeviceSummary } from '../../../api/models/deviceSummary'; import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext'; import { ResourceKeys } from '../../../../localization/resourceKeys'; +import { fetchDigitalTwinInterfaceProperties } from '../../../api/services/devicesService'; +import { DigitalTwinInterfaces } from '../../../api/models/digitalTwinModels'; +import { modelDiscoveryInterfaceName } from '../../../constants/modelDefinitionConstants'; +import { getReportedInterfacesFromDigitalTwin } from '../../deviceContent/selectors'; import '../../../css/_deviceListCell.scss'; // tslint:disable registerIcons({ icons: { 'pnp-svg': ( - + @@ -23,51 +27,85 @@ registerIcons({ //tslint: enable export interface DeviceListCellProps { - itemIndex: number | string; + connectionString: string; device: DeviceSummary; - deviceId: string; +} + +export interface DeviceListCellState { isLoading: boolean; interfaceIds: string[]; - fetchDeviceInfo: (deviceId: string) => void; } -export class DeviceListCell extends React.PureComponent { +export class DeviceListCell extends React.Component { + private isComponentMounted: boolean; constructor(props: DeviceListCellProps) { super(props); + + this.state = { + isLoading: true, + interfaceIds: [] + }; } public render() { - const { itemIndex, isLoading, device } = this.props; + const { device } = this.props; return ( {(context: LocalizationContextInterface) => ( - !isLoading ? -
- {this.renderCellDeviceInfo(device, context)} - {this.renderCellInterfaceInfo(device, context)} -
- : -
- {this.renderLoadingInfo(context)} -
+ !this.state.isLoading ? +
+ {this.renderCellDeviceInfo(device, context)} + {this.state.interfaceIds.length !== 0 && this.renderCellInterfaceInfo(context)} +
: +
+ {this.renderLoadingInfo(context)} +
)} ); } public componentWillMount() { - const { fetchDeviceInfo, device } = this.props; - fetchDeviceInfo(device.deviceId); + this.isComponentMounted = true; + fetchDigitalTwinInterfaceProperties({ + connectionString: this.props.connectionString, + digitalTwinId: this.props.device.deviceId + }).then((results: DigitalTwinInterfaces) => { + let interfaceIds = []; + if (getReportedInterfacesFromDigitalTwin(results)) { + interfaceIds = Object.keys(results.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces).map( + key => results.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces[key] + ); + } + + if (this.isComponentMounted) { + this.setState({ + interfaceIds, + isLoading: false + }); + } + }).catch(() => { + if (this.isComponentMounted) { + this.setState({ + interfaceIds: [], + isLoading: false + }); + } + }); + } + + public componentWillUnmount() { + this.isComponentMounted = false; } private readonly renderCellDeviceInfo = (device: DeviceSummary, context: LocalizationContextInterface) => { return (
{`${context.t(ResourceKeys.deviceLists.columns.lastActivityTime)}: `}{device.lastActivityTime || context.t(ResourceKeys.deviceLists.noData)} - {`${context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime)}: `}{device.statusUpdatedTime || context.t(ResourceKeys.deviceLists.noData)} - {!!device.isPnpDevice && + {`${context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime)}: `}{device.statusUpdatedTime || context.t(ResourceKeys.deviceLists.noData)} + {this.state.interfaceIds.length !== 0 && { ariaLabel={context.t(ResourceKeys.deviceLists.columns.isPnpDevice)} /> {context.t(ResourceKeys.deviceLists.columns.isPnpDevice)} - } + + }
); } - private readonly renderCellInterfaceInfo = (device: DeviceSummary, context: LocalizationContextInterface) => { - const listInterfaces = (deviceSummary: DeviceSummary) => { - return deviceSummary.interfaceIds && deviceSummary.interfaceIds.length > 0 ? deviceSummary.interfaceIds.join('; ') : ''; - }; + private readonly renderCellInterfaceInfo = (context: LocalizationContextInterface) => { + const listInterfaces = this.state.interfaceIds.join("; "); return (
- {!!device.isPnpDevice && {`${context.t(ResourceKeys.deviceLists.columns.interfaces)}: `}{device.interfaceIds && device.interfaceIds.length > 0 ? listInterfaces(device) : context.t(ResourceKeys.deviceLists.noData)}} + { + {`${context.t(ResourceKeys.deviceLists.columns.interfaces)}: `} + {listInterfaces} + }
); } @@ -95,9 +135,8 @@ export class DeviceListCell extends React.PureComponent { private readonly renderLoadingInfo = (context: LocalizationContextInterface) => { return (
- {`${context.t(ResourceKeys.common.loading)}: `} + {context.t(ResourceKeys.common.loading)}
); } - } diff --git a/src/app/devices/deviceList/components/deviceListCellContainer.tsx b/src/app/devices/deviceList/components/deviceListCellContainer.tsx deleted file mode 100644 index ed1ccba77..000000000 --- a/src/app/devices/deviceList/components/deviceListCellContainer.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { StateType } from '../../../shared/redux/state'; -import { DeviceListCellProps, DeviceListCell } from './deviceListCell'; -import { NonFunctionProperties, FunctionProperties } from '../../../shared/types/types'; -import { getDeviceSummaryWrapper } from '../selectors'; -import { fetchInterfacesAction } from '../actions'; -import { SynchronizationStatus } from '../../../api/models/synchronizationStatus'; - -const mapStateToProps = (state: StateType, ownProps: Partial): NonFunctionProperties => { - const device = getDeviceSummaryWrapper(state, ownProps.deviceId); - - return { - device, - deviceId: ownProps.deviceId, - interfaceIds: device.interfaceIds || [], - isLoading: !device.deviceSummarySynchronizationStatus || device.deviceSummarySynchronizationStatus === SynchronizationStatus.working, - itemIndex: ownProps.itemIndex - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch): FunctionProperties => { - return { - fetchDeviceInfo: (deviceId: string) => dispatch(fetchInterfacesAction.started(deviceId)) - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(DeviceListCell); diff --git a/src/app/devices/deviceList/components/deviceListContainer.tsx b/src/app/devices/deviceList/components/deviceListContainer.tsx index 0937c4eaf..16d2ec65a 100644 --- a/src/app/devices/deviceList/components/deviceListContainer.tsx +++ b/src/app/devices/deviceList/components/deviceListContainer.tsx @@ -8,15 +8,15 @@ import DeviceListComponent, { DeviceListDispatchProps } from './deviceList'; import { StateType } from '../../../shared/redux/state'; import { getConnectionStringSelector } from '../../../login/selectors'; import { listDevicesAction, deleteDevicesAction } from '../actions'; -import { getDeviceSummaryListWrapper, deviceSummaryListWrapperNoPNPSelector } from '../selectors'; +import { getDeviceSummaryListStatus, deviceSummaryListWrapperNoPNPSelector } from '../selectors'; import DeviceQuery from '../../../api/models/deviceQuery'; +import { SynchronizationStatus } from '../../../api/models/synchronizationStatus'; const mapStateToProps = (state: StateType) => { - const deviceSummaryListWrapper = getDeviceSummaryListWrapper(state); return { connectionString: getConnectionStringSelector(state), - deviceListSyncStatus: deviceSummaryListWrapper && deviceSummaryListWrapper.get('deviceListSynchronizationStatus'), - devices: deviceSummaryListWrapperNoPNPSelector(state) + devices: deviceSummaryListWrapperNoPNPSelector(state), + isFetching: getDeviceSummaryListStatus(state) === SynchronizationStatus.working }; }; diff --git a/src/app/devices/deviceList/reducer.spec.ts b/src/app/devices/deviceList/reducer.spec.ts index 373f28fbc..4620f26d8 100644 --- a/src/app/devices/deviceList/reducer.spec.ts +++ b/src/app/devices/deviceList/reducer.spec.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License **********************************************************/ import 'jest'; -import { Record, Map, List, fromJS } from 'immutable'; -import { LIST_DEVICES, CLEAR_DEVICES } from '../../constants/actionTypes'; -import { clearDevicesAction, listDevicesAction } from './actions'; +import { Record, Map as ImmutableMap, fromJS } from 'immutable'; +import { LIST_DEVICES, CLEAR_DEVICES, ADD_DEVICE, DELETE_DEVICES } from '../../constants/actionTypes'; +import { clearDevicesAction, listDevicesAction, addDeviceAction, deleteDevicesAction } from './actions'; import reducer from './reducer'; import { DeviceListStateInterface, deviceListStateInitial } from './state'; import { SynchronizationStatus } from '../../api/models/synchronizationStatus'; @@ -13,32 +13,79 @@ import { DeviceSummary } from '../../api/models/deviceSummary'; import { DeviceSummaryListWrapper } from '../../api/models/deviceSummaryListWrapper'; describe('deviceListStateReducer', () => { + const deviceId = 'testDeviceId'; const deviceSummary: DeviceSummary = { authenticationType: 'sas', cloudToDeviceMessageCount: '0', - deviceId: 'testDeviceId', - deviceSummarySynchronizationStatus: SynchronizationStatus.initialized, - interfaceIds: [], - isPnpDevice: false, + deviceId, lastActivityTime: 'Thu Apr 25 2019 16:48:19 GMT-0700 (Pacific Daylight Time)', status: 'Enabled', statusUpdatedTime: 'Thu Apr 25 2019 16:48:19 GMT-0700 (Pacific Daylight Time)', }; - const deviceSummaryMap: Map = fromJS(deviceSummary); // tslint:disable-line:no-any + + it (`handles ${LIST_DEVICES}/ACTION_START action`, () => { + const deviceSummaryMap = ImmutableMap(); + const action = listDevicesAction.started(undefined); + expect(reducer(deviceListStateInitial(), action).devices.deviceList).toEqual(deviceSummaryMap); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.working); + }); + it (`handles ${LIST_DEVICES}/ACTION_DONE action`, () => { + const deviceSummaryMap = new Map(); + deviceSummaryMap.set(deviceId, deviceSummary); const action = listDevicesAction.done({params: undefined, result: [deviceSummary]}); - expect(reducer(deviceListStateInitial(), action).devices.deviceList).toEqual(List([deviceSummaryMap])); + expect(reducer(deviceListStateInitial(), action).devices.deviceList).toEqual(fromJS(deviceSummaryMap)); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.fetched); + }); + + it (`handles ${LIST_DEVICES}/ACTION_FAIL action`, () => { + const deviceSummaryMap = ImmutableMap(); + const action = listDevicesAction.failed(undefined); + expect(reducer(deviceListStateInitial(), action).devices.deviceList).toEqual(deviceSummaryMap); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.failed); + }); + + it (`handles ${ADD_DEVICE}/ACTION_START action`, () => { + const action = addDeviceAction.started(undefined); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.updating); + }); + + it (`handles ${ADD_DEVICE}/ACTION_DONE action`, () => { + const action = addDeviceAction.done({params: undefined, result: deviceSummary}); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.upserted); + }); + + it (`handles ${ADD_DEVICE}/ACTION_FAIL action`, () => { + const action = addDeviceAction.failed(undefined); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.failed); + }); + + it (`handles ${DELETE_DEVICES}/ACTION_START action`, () => { + const action = deleteDevicesAction.started(undefined); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.updating); + }); + + it (`handles ${DELETE_DEVICES}/ACTION_DONE action`, () => { + const action = deleteDevicesAction.done({params: [deviceId], result: undefined}); + const deviceSummaryMap = ImmutableMap(); + expect(reducer(deviceListStateInitial(), action).devices.deviceList).toEqual(deviceSummaryMap); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.deleted); + }); + + it (`handles ${DELETE_DEVICES}/ACTION_FAIL action`, () => { + const action = deleteDevicesAction.failed(undefined); + expect(reducer(deviceListStateInitial(), action).devices.deviceListSynchronizationStatus).toEqual(SynchronizationStatus.failed); }); it (`handles ${CLEAR_DEVICES} action`, () => { const initialState = Record({ deviceQuery: null, devices: Record({ - deviceList: List(), + deviceList: ImmutableMap(), deviceListSynchronizationStatus: SynchronizationStatus.working })(), }); const action = clearDevicesAction(); - expect(reducer(initialState(), action).devices.deviceList).toEqual(List([])); + expect(reducer(initialState(), action).devices.deviceList).toEqual(ImmutableMap()); }); }); diff --git a/src/app/devices/deviceList/reducer.ts b/src/app/devices/deviceList/reducer.ts index 8c800a1bf..0ab3a8d85 100644 --- a/src/app/devices/deviceList/reducer.ts +++ b/src/app/devices/deviceList/reducer.ts @@ -2,160 +2,104 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { fromJS, Map } from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { deviceListStateInitial, DeviceListStateType } from './state'; -import { listDevicesAction, clearDevicesAction, deleteDevicesAction, addDeviceAction, fetchInterfacesAction } from './actions'; +import { listDevicesAction, clearDevicesAction, deleteDevicesAction, addDeviceAction } from './actions'; import { DeviceSummary } from './../../api/models/deviceSummary'; import { SynchronizationStatus } from '../../api/models/synchronizationStatus'; import { DeviceIdentity } from '../../api/models/deviceIdentity'; import DeviceQuery from '../../api/models/deviceQuery'; -import { DigitalTwinInterfaces } from '../../api/models/digitalTwinModels'; import { BulkRegistryOperationResult } from '../../api/models/bulkRegistryOperationResult'; -import { modelDiscoveryInterfaceName } from '../../constants/modelDefinitionConstants'; const reducer = reducerWithInitialState(deviceListStateInitial()) .case(listDevicesAction.started, (state: DeviceListStateType, payload: DeviceQuery) => { - return state.mergeIn( - ['deviceQuery'], - {...payload} - ).setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.working - ); + const devices = state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.working + }); + return state.merge({ + deviceQuery: {...payload}, + devices + }); }) .case(listDevicesAction.done, (state: DeviceListStateType, payload: {params: DeviceQuery} & {result: DeviceSummary[]}) => { - return state.mergeIn( - ['deviceQuery'], - {...payload.params} - ).setIn( - ['devices', 'deviceList'], - fromJS(payload.result) - ).setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.fetched - ); + const deviceList = new Map(); + payload.result.forEach(item => deviceList.set(item.deviceId, item)); + const devices = state.devices.merge({ + deviceList: fromJS(deviceList), + deviceListSynchronizationStatus: SynchronizationStatus.fetched + }); + return state.merge({ + deviceQuery: {...payload.params}, + devices + }); }) .case(listDevicesAction.failed, (state: DeviceListStateType) => { - return state.setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.failed - ); + const devices = state.devices.merge({ + deviceList: ImmutableMap(), + deviceListSynchronizationStatus: SynchronizationStatus.failed + }); + return state.merge({ + devices + }); }) .case(addDeviceAction.started, (state: DeviceListStateType) => { - return state.setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.working - ); + const devices = state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.updating + }); + return state.merge({ + devices + }); }) .case(addDeviceAction.done, (state: DeviceListStateType, payload: {params: DeviceIdentity} & {result: DeviceSummary}) => { - let deviceList = state.devices.deviceList; - deviceList = deviceList.unshift(fromJS(payload.result)); - return state.setIn( - ['devices', 'deviceList'], - deviceList - ).setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.upserted - ); + const devices = state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.upserted + }); + return state.merge({ + devices + }); }) .case(addDeviceAction.failed, (state: DeviceListStateType) => { - return state.setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.failed - ); + const devices = state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.failed + }); + return state.merge({ + devices + }); }) .case(deleteDevicesAction.started, (state: DeviceListStateType) => { - return state.setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.working - ); + const devices = state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.updating + }); + return state.merge({ + devices + }); }) .case(deleteDevicesAction.done, (state: DeviceListStateType, payload: {params: string[]} & {result: BulkRegistryOperationResult}) => { - let deviceList = state.devices.deviceList; - deviceList = deviceList.filter((device: Map) => payload.params.indexOf(device.get('deviceId')) === -1); // tslint:disable-line - return state.setIn( - ['devices', 'deviceList'], - deviceList - ).setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.deleted - ); + const deviceList = state.devices.deviceList; + payload.params.forEach(id => deviceList.delete(id)); + const devices = state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.deleted + }); + return state.merge({ + devices + }); }) .case(deleteDevicesAction.failed, (state: DeviceListStateType) => { - return state.setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.failed - ); - }) - .case(clearDevicesAction, (state: DeviceListStateType) => { - return state.setIn( - ['devices', 'deviceList'], - fromJS([]) - ).setIn( - ['devices', 'deviceListSynchronizationStatus'], - SynchronizationStatus.deleted - ); - }) - .case(fetchInterfacesAction.started, (state: DeviceListStateType, payload: string) => { - const devices: DeviceSummary[] = state.devices.get('deviceList').toArray().map((item: Map) => item.toJS() as DeviceSummary); // tslint:disable-line - const index = devices.findIndex(device => { - return device.deviceId === payload; + const devices = state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.failed }); - - devices[index].deviceSummarySynchronizationStatus = SynchronizationStatus.working; - - return state.mergeIn( - ['devices', 'deviceList', index], - fromJS(devices[index]) - ); - }) - .case(fetchInterfacesAction.done, (state: DeviceListStateType, payload: {params: string} & {result: DigitalTwinInterfaces}) => { - const devices: DeviceSummary[] = state.devices.get('deviceList').toArray().map((item: Map) => item.toJS() as DeviceSummary); // tslint:disable-line - const index = devices.findIndex(device => { - return device.deviceId === payload.params; + return state.merge({ + devices }); - - let interfaceNames = null; - // tslint:disable-next-line:cyclomatic-complexity - if ( - payload && - payload.result && - payload.result.interfaces && - payload.result.interfaces[modelDiscoveryInterfaceName] && - payload.result.interfaces[modelDiscoveryInterfaceName].properties && - payload.result.interfaces[modelDiscoveryInterfaceName].properties.modelInformation && - payload.result.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported && - payload.result.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value && - payload.result.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces) { - interfaceNames = Object.keys(payload.result.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces).map( - key => payload.result.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces[key] - ); - } - - devices[index].deviceSummarySynchronizationStatus = SynchronizationStatus.fetched; - const isPnp = interfaceNames && Object.keys(interfaceNames).length > 0; - devices[index].isPnpDevice = isPnp; - - if (isPnp) { - devices[index].interfaceIds = interfaceNames; - } - - return state.mergeIn( - ['devices', 'deviceList', index], - fromJS(devices[index]) - ); }) - .case(fetchInterfacesAction.failed, (state: DeviceListStateType, payload: {params: string}) => { - const devices: DeviceSummary[] = state.devices.get('deviceList').toArray().map((item: Map) => item.toJS() as DeviceSummary); // tslint:disable-line - const index = devices.findIndex(device => { - return device.deviceId === payload.params; + .case(clearDevicesAction, (state: DeviceListStateType) => { + const devices = state.devices.merge({ + deviceList: ImmutableMap(), + deviceListSynchronizationStatus: SynchronizationStatus.deleted + }); + return state.merge({ + devices }); - devices[index].deviceSummarySynchronizationStatus = SynchronizationStatus.failed; - - return state.updateIn( - ['devices', 'deviceList'], - () => fromJS(devices) - ); }); export default reducer; diff --git a/src/app/devices/deviceList/sagas.ts b/src/app/devices/deviceList/sagas.ts index d29e2c79d..df78630cd 100644 --- a/src/app/devices/deviceList/sagas.ts +++ b/src/app/devices/deviceList/sagas.ts @@ -6,12 +6,10 @@ import { takeEvery, takeLatest } from 'redux-saga/effects'; import { listDevicesSaga } from './sagas/listDeviceSaga'; import { deleteDevicesSaga } from './sagas/deleteDeviceSaga'; import { addDeviceSaga } from './sagas/addDeviceSaga'; -import { listDevicesAction, deleteDevicesAction, addDeviceAction, fetchInterfacesAction } from './actions'; -import { fetchDigitalTwinInterfacePropertiesSaga } from './sagas/fetchDigitalTwinInterfacePropertiesSaga'; +import { listDevicesAction, deleteDevicesAction, addDeviceAction } from './actions'; export default [ takeLatest(listDevicesAction.started.type, listDevicesSaga), takeEvery(deleteDevicesAction.started.type, deleteDevicesSaga), - takeEvery(addDeviceAction.started, addDeviceSaga), - takeEvery(fetchInterfacesAction.started, fetchDigitalTwinInterfacePropertiesSaga) + takeEvery(addDeviceAction.started, addDeviceSaga) ]; diff --git a/src/app/devices/deviceList/sagas/fetchDigitalTwinInterfacePropertiesSaga.ts b/src/app/devices/deviceList/sagas/fetchDigitalTwinInterfacePropertiesSaga.ts deleted file mode 100644 index 65934a1d0..000000000 --- a/src/app/devices/deviceList/sagas/fetchDigitalTwinInterfacePropertiesSaga.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { call, put, select } from 'redux-saga/effects'; -import { Action } from 'typescript-fsa'; -import { FetchDigitalTwinInterfacePropertiesParameters } from '../../../api/parameters/deviceParameters'; -import { getConnectionStringSelector } from '../../../login/selectors'; -import { fetchDigitalTwinInterfaceProperties } from '../../../api/services/devicesService'; -import { fetchInterfacesAction } from '../actions'; -import { addNotificationAction } from '../../../notifications/actions'; -import { ResourceKeys } from '../../../../localization/resourceKeys'; -import { NotificationType } from '../../../api/models/notification'; - -export function* fetchDigitalTwinInterfacePropertiesSaga(action: Action) { - try { - const parameters: FetchDigitalTwinInterfacePropertiesParameters = { - connectionString: yield select(getConnectionStringSelector), - digitalTwinId: action.payload - }; - - const interfaces = yield call(fetchDigitalTwinInterfaceProperties, parameters); - - yield put(fetchInterfacesAction.done({params: action.payload, result: interfaces})); - } catch (error) { - yield put(addNotificationAction.started({ - text: { - translationKey: ResourceKeys.notifications.getDigitalTwinInterfacePropertiesOnError, - translationOptions: { - deviceId: action.payload, - error - } - }, - type: NotificationType.error - })); - - yield put(fetchInterfacesAction.failed({params: action.payload, error})); - } -} diff --git a/src/app/devices/deviceList/selectors.spec.ts b/src/app/devices/deviceList/selectors.spec.ts new file mode 100644 index 000000000..324edab2b --- /dev/null +++ b/src/app/devices/deviceList/selectors.spec.ts @@ -0,0 +1,44 @@ +/*********************************************************** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License + **********************************************************/ +import 'jest'; +import { Record, Map } from 'immutable'; +import { SynchronizationStatus } from './../../api/models/synchronizationStatus'; +import { getDeviceSummaryListStatus, getDeviceSummaryWrapper, deviceSummaryListWrapperNoPNPSelector } from './selectors'; +import { getInitialState } from './../../api/shared/testHelper'; +import { DeviceSummary } from '../../api/models/deviceSummary'; + +describe('getDigitalTwinInterfacePropertiesSelector', () => { + const state = getInitialState(); + const deviceId = 'testDeviceId'; + const deviceSummary: DeviceSummary = { + authenticationType: 'sas', + cloudToDeviceMessageCount: '0', + deviceId, + lastActivityTime: 'Thu Apr 25 2019 16:48:19 GMT-0700 (Pacific Daylight Time)', + status: 'Enabled', + statusUpdatedTime: 'Thu Apr 25 2019 16:48:19 GMT-0700 (Pacific Daylight Time)', + }; + const deviceSummaryMap = Map(); + const newMap = deviceSummaryMap.set(deviceId, deviceSummary); + state.deviceListState = Record({ + deviceQuery: undefined, + devices: Record({ + deviceList: newMap, + deviceListSynchronizationStatus: SynchronizationStatus.fetched + })() + })(); + + it('returns device list status', () => { + expect(getDeviceSummaryListStatus(state)).toEqual(SynchronizationStatus.fetched); + }); + + it('returns device summary', () => { + expect(getDeviceSummaryWrapper(state, deviceId)).toEqual(deviceSummary); + }); + + it('returns device summary list', () => { + expect(deviceSummaryListWrapperNoPNPSelector(state)).toEqual([deviceSummary]); + }); +}); diff --git a/src/app/devices/deviceList/selectors.ts b/src/app/devices/deviceList/selectors.ts index 63eac060c..51245e31b 100644 --- a/src/app/devices/deviceList/selectors.ts +++ b/src/app/devices/deviceList/selectors.ts @@ -2,33 +2,35 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { Map } from 'immutable'; import { createSelector } from 'reselect'; -import { StateType } from '../../shared/redux/state'; +import { StateInterface } from '../../shared/redux/state'; import { DeviceSummaryListWrapper } from '../../api/models/deviceSummaryListWrapper'; import { DeviceSummary } from '../../api/models/deviceSummary'; -import { IM } from '../../shared/types/types'; -export const getDeviceSummaryListWrapper = (state: StateType): IM => { +const getDeviceSummaryListSelector = (state: StateInterface): DeviceSummaryListWrapper => { return state && - state.deviceListState && - state.deviceListState.devices; + state.deviceListState && + state.deviceListState.devices; }; -export const getDeviceSummaryWrapper = (state: StateType, deviceId: string): DeviceSummary => { - const devices = getDeviceSummaryListWrapper(state); - const mapDevice = devices.deviceList && devices.deviceList.find((item: Map) => { // tslint:disable-line - return item.get('deviceId') === deviceId; - }); - return mapDevice.toJS() as DeviceSummary; +export const getDeviceSummaryListStatus = createSelector( + getDeviceSummaryListSelector, + deviceListWrapper => { + return deviceListWrapper && deviceListWrapper.deviceListSynchronizationStatus; + } +); + +export const getDeviceSummaryWrapper = (state: StateInterface, deviceId: string): DeviceSummary => { + const devices = state && + state.deviceListState && + state.deviceListState.devices && + state.deviceListState.devices.deviceList; + return devices && devices.get(deviceId); }; export const deviceSummaryListWrapperNoPNPSelector = createSelector( - getDeviceSummaryListWrapper, - deviceList => { - const devicesOnly = deviceList && deviceList.deviceList && deviceList.deviceList.map((device: Map) => { // tslint:disable-line - return device.remove('interfaceIds').remove('deviceSummarySynchronizationStatus').remove('isPnpDevice'); - }); - return !!devicesOnly ? devicesOnly.toJS() : []; + getDeviceSummaryListSelector, + deviceListWrapper => { + return Array.from(deviceListWrapper && deviceListWrapper.deviceList && deviceListWrapper.deviceList.values()) || []; } ); diff --git a/src/app/devices/deviceList/state.ts b/src/app/devices/deviceList/state.ts index 4333d1c72..4a018b7ba 100644 --- a/src/app/devices/deviceList/state.ts +++ b/src/app/devices/deviceList/state.ts @@ -2,8 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { Record, List } from 'immutable'; +import { Record, Map } from 'immutable'; import { IM } from '../../shared/types/types'; +import { DeviceSummary } from './../../api/models/deviceSummary'; import DeviceQuery from '../../api/models/deviceQuery'; import { SynchronizationStatus } from '../../api/models/synchronizationStatus'; import { DeviceSummaryListWrapper } from '../../api/models/deviceSummaryListWrapper'; @@ -20,7 +21,7 @@ export const deviceListStateInitial = Record({ nextLink: '' }, devices: Record({ - deviceList: List([]), + deviceList: Map(), deviceListSynchronizationStatus: SynchronizationStatus.initialized, error: { error: { diff --git a/src/app/login/components/connectivityPane.tsx b/src/app/login/components/connectivityPane.tsx index 7cbdb4526..35b208c4a 100644 --- a/src/app/login/components/connectivityPane.tsx +++ b/src/app/login/components/connectivityPane.tsx @@ -15,7 +15,7 @@ import LabelWithTooltip from '../../shared/components/labelWithTooltip'; import { SetConnectionStringActionParameter } from '../actions'; import '../../css/_connectivityPane.scss'; -export interface ConnectivityPaneDisptachProps { +export interface ConnectivityPaneDispatchProps { saveConnectionInfo: (connectionStringSetting: SetConnectionStringActionParameter) => void; } @@ -30,8 +30,8 @@ export interface ConnectivityState { rememberConnectionString: boolean; } -export default class ConnectivityPane extends React.Component { - constructor(props: RouteComponentProps & ConnectivityPaneDataProps & ConnectivityPaneDisptachProps) { +export default class ConnectivityPane extends React.Component { + constructor(props: RouteComponentProps & ConnectivityPaneDataProps & ConnectivityPaneDispatchProps) { super(props); this.state = { connectionString: this.props.connectionString, diff --git a/src/app/login/components/connectivityPaneContainer.tsx b/src/app/login/components/connectivityPaneContainer.tsx index 274a3852d..bf1b10d5b 100644 --- a/src/app/login/components/connectivityPaneContainer.tsx +++ b/src/app/login/components/connectivityPaneContainer.tsx @@ -6,7 +6,7 @@ import { AnyAction } from 'typescript-fsa'; import { Dispatch, compose } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; -import ConnectivityPane, { ConnectivityPaneDataProps, ConnectivityPaneDisptachProps } from './connectivityPane'; +import ConnectivityPane, { ConnectivityPaneDataProps, ConnectivityPaneDispatchProps } from './connectivityPane'; import { StateType } from '../../shared/redux/state'; import { getConnectionStringSelector, getRememberConnectionStringValueSelector } from '../selectors'; import { clearDevicesAction } from '../../devices/deviceList/actions'; @@ -20,7 +20,7 @@ const mapStateToProps = (state: StateType): ConnectivityPaneDataProps => { }; }; -const mapDispatchToProps = (dispatch: Dispatch): ConnectivityPaneDisptachProps => { +const mapDispatchToProps = (dispatch: Dispatch): ConnectivityPaneDispatchProps => { return { saveConnectionInfo: (connectionStringSetting: SetConnectionStringActionParameter) => { dispatch(setConnectionStringAction(connectionStringSetting)); diff --git a/src/app/shared/components/groupedList/groupedList.tsx b/src/app/shared/components/groupedList/groupedList.tsx index b6037e939..2bffb8b92 100644 --- a/src/app/shared/components/groupedList/groupedList.tsx +++ b/src/app/shared/components/groupedList/groupedList.tsx @@ -91,10 +91,6 @@ export default class GroupedListWrapper extends React.Component): boolean { - return JSON.stringify(this.props.items) !== JSON.stringify(nextProps.items); // TODO: Write an array comparison instead of using stringify - } - public static getDerivedStateFromProps(props: GroupedListProps, state: GroupedListState): Partial | null { if (typeof props.items !== 'undefined' && typeof props.nameKey !== 'undefined') { const groups = GroupedListWrapper.createGroups(props); @@ -121,7 +117,7 @@ export default class GroupedListWrapper extends React.Component(props: GroupedListProps) { return (props.items && props.items.map((item, index): IGroup => { - const itemName: string = item[props.nameKey].toString(); + const itemName: string = item[props.nameKey] && item[props.nameKey].toString(); return { count: 1, diff --git a/src/server/server.ts b/src/server/server.ts index e685aa032..c1dc1a71b 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -61,12 +61,17 @@ app.post('/api/DataPlane', (req: express.Request, res: express.Response) => { uri: `https://${req.body.hostName}/${encodeURIComponent(req.body.path)}${queryString}`, }, (err, httpRes, body) => { - if (httpRes.headers && httpRes.headers[DEVICE_STATUS_HEADER]) { // handles happy failure cases when error code is returned as a header - // tslint:disable-next-line:radix - res.status(parseInt(httpRes.headers[DEVICE_STATUS_HEADER] as string)).send(body); + if (httpRes) { + if (httpRes.headers && httpRes.headers[DEVICE_STATUS_HEADER]) { // handles happy failure cases when error code is returned as a header + // tslint:disable-next-line:radix + res.status(parseInt(httpRes.headers[DEVICE_STATUS_HEADER] as string)).send(body); + } + else { + res.status(httpRes.statusCode).send(body); + } } else { - res.status(httpRes.statusCode).send(body); + res.send(body); } }); } From 01349cbf58e07fbb654d3a5838f21554dfa1ef6a Mon Sep 17 00:00:00 2001 From: ying xue Date: Mon, 29 Jul 2019 12:32:16 -0700 Subject: [PATCH 2/4] update reducer per feedback --- src/app/devices/deviceList/reducer.ts | 76 ++++++++++++--------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/src/app/devices/deviceList/reducer.ts b/src/app/devices/deviceList/reducer.ts index 0ab3a8d85..cd8074a1e 100644 --- a/src/app/devices/deviceList/reducer.ts +++ b/src/app/devices/deviceList/reducer.ts @@ -14,92 +14,82 @@ import { BulkRegistryOperationResult } from '../../api/models/bulkRegistryOperat const reducer = reducerWithInitialState(deviceListStateInitial()) .case(listDevicesAction.started, (state: DeviceListStateType, payload: DeviceQuery) => { - const devices = state.devices.merge({ - deviceListSynchronizationStatus: SynchronizationStatus.working - }); return state.merge({ deviceQuery: {...payload}, - devices + devices: state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.working + }) }); }) .case(listDevicesAction.done, (state: DeviceListStateType, payload: {params: DeviceQuery} & {result: DeviceSummary[]}) => { const deviceList = new Map(); payload.result.forEach(item => deviceList.set(item.deviceId, item)); - const devices = state.devices.merge({ - deviceList: fromJS(deviceList), - deviceListSynchronizationStatus: SynchronizationStatus.fetched - }); return state.merge({ deviceQuery: {...payload.params}, - devices + devices: state.devices.merge({ + deviceList: fromJS(deviceList), + deviceListSynchronizationStatus: SynchronizationStatus.fetched + }) }); }) .case(listDevicesAction.failed, (state: DeviceListStateType) => { - const devices = state.devices.merge({ - deviceList: ImmutableMap(), - deviceListSynchronizationStatus: SynchronizationStatus.failed - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceList: ImmutableMap(), + deviceListSynchronizationStatus: SynchronizationStatus.failed + }) }); }) .case(addDeviceAction.started, (state: DeviceListStateType) => { - const devices = state.devices.merge({ - deviceListSynchronizationStatus: SynchronizationStatus.updating - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.updating + }) }); }) .case(addDeviceAction.done, (state: DeviceListStateType, payload: {params: DeviceIdentity} & {result: DeviceSummary}) => { - const devices = state.devices.merge({ - deviceListSynchronizationStatus: SynchronizationStatus.upserted - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.upserted + }) }); }) .case(addDeviceAction.failed, (state: DeviceListStateType) => { - const devices = state.devices.merge({ - deviceListSynchronizationStatus: SynchronizationStatus.failed - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.failed + }) }); }) .case(deleteDevicesAction.started, (state: DeviceListStateType) => { - const devices = state.devices.merge({ - deviceListSynchronizationStatus: SynchronizationStatus.updating - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.updating + }) }); }) .case(deleteDevicesAction.done, (state: DeviceListStateType, payload: {params: string[]} & {result: BulkRegistryOperationResult}) => { const deviceList = state.devices.deviceList; payload.params.forEach(id => deviceList.delete(id)); - const devices = state.devices.merge({ - deviceListSynchronizationStatus: SynchronizationStatus.deleted - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.deleted + }) }); }) .case(deleteDevicesAction.failed, (state: DeviceListStateType) => { - const devices = state.devices.merge({ - deviceListSynchronizationStatus: SynchronizationStatus.failed - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceListSynchronizationStatus: SynchronizationStatus.failed + }) }); }) .case(clearDevicesAction, (state: DeviceListStateType) => { - const devices = state.devices.merge({ - deviceList: ImmutableMap(), - deviceListSynchronizationStatus: SynchronizationStatus.deleted - }); return state.merge({ - devices + devices: state.devices.merge({ + deviceList: ImmutableMap(), + deviceListSynchronizationStatus: SynchronizationStatus.deleted + }) }); }); export default reducer; From a36833cc65ab0d31ad947472f02fd83c9a120209 Mon Sep 17 00:00:00 2001 From: ying xue Date: Mon, 29 Jul 2019 14:32:40 -0700 Subject: [PATCH 3/4] address more comments --- .../components/addDevice/components/addDevice.tsx | 3 ++- src/app/devices/deviceList/components/deviceList.tsx | 1 + src/app/devices/deviceList/components/deviceListCell.tsx | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/devices/deviceList/components/addDevice/components/addDevice.tsx b/src/app/devices/deviceList/components/addDevice/components/addDevice.tsx index 0dd39607d..4d430cdd9 100644 --- a/src/app/devices/deviceList/components/addDevice/components/addDevice.tsx +++ b/src/app/devices/deviceList/components/addDevice/components/addDevice.tsx @@ -4,7 +4,7 @@ **********************************************************/ import * as React from 'react'; import { RouteComponentProps, Route } from 'react-router-dom'; -import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react'; +import { PrimaryButton, DefaultButton, Overlay } from 'office-ui-fabric-react'; import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup'; import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; @@ -79,6 +79,7 @@ export default class AddDevice extends React.Component + {this.props.deviceListSyncStatus === SynchronizationStatus.updating && } )} diff --git a/src/app/devices/deviceList/components/deviceList.tsx b/src/app/devices/deviceList/components/deviceList.tsx index 068989129..85565e441 100644 --- a/src/app/devices/deviceList/components/deviceList.tsx +++ b/src/app/devices/deviceList/components/deviceList.tsx @@ -126,6 +126,7 @@ class DeviceListComponent extends React.Component ); }; diff --git a/src/app/devices/deviceList/components/deviceListCell.tsx b/src/app/devices/deviceList/components/deviceListCell.tsx index 71e625289..c4f51b4b6 100644 --- a/src/app/devices/deviceList/components/deviceListCell.tsx +++ b/src/app/devices/deviceList/components/deviceListCell.tsx @@ -27,6 +27,7 @@ registerIcons({ //tslint: enable export interface DeviceListCellProps { + itemIndex: number | string; connectionString: string; device: DeviceSummary; } @@ -49,17 +50,17 @@ export class DeviceListCell extends React.Component {(context: LocalizationContextInterface) => ( !this.state.isLoading ? -
+
{this.renderCellDeviceInfo(device, context)} {this.state.interfaceIds.length !== 0 && this.renderCellInterfaceInfo(context)}
: -
+
{this.renderLoadingInfo(context)}
)} From fdc369d7d8f013d61fb22f6ad1fad6f3861705ec Mon Sep 17 00:00:00 2001 From: ying xue Date: Mon, 29 Jul 2019 16:38:21 -0700 Subject: [PATCH 4/4] more comments addressed --- src/app/devices/deviceList/components/deviceList.tsx | 8 +++----- src/app/devices/deviceList/components/deviceListCell.tsx | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/devices/deviceList/components/deviceList.tsx b/src/app/devices/deviceList/components/deviceList.tsx index 85565e441..3b07de41c 100644 --- a/src/app/devices/deviceList/components/deviceList.tsx +++ b/src/app/devices/deviceList/components/deviceList.tsx @@ -146,11 +146,9 @@ class DeviceListComponent extends React.Component { const path = this.props.location.pathname.replace(/\/devices\/.*/, '/devices'); return ( -
- - {group.name} - -
+ + {group.name} + ); }, widthPercentage: 30 diff --git a/src/app/devices/deviceList/components/deviceListCell.tsx b/src/app/devices/deviceList/components/deviceListCell.tsx index c4f51b4b6..959506243 100644 --- a/src/app/devices/deviceList/components/deviceListCell.tsx +++ b/src/app/devices/deviceList/components/deviceListCell.tsx @@ -37,7 +37,7 @@ export interface DeviceListCellState { interfaceIds: string[]; } -export class DeviceListCell extends React.Component { +export class DeviceListCell extends React.PureComponent { private isComponentMounted: boolean; constructor(props: DeviceListCellProps) {