Device list (#163)
* make device list simple detailed list with no collapse and wire up command bar * add max width to properties list * remove a comment
This commit is contained in:
Родитель
0c2c943a93
Коммит
8e8165ff6a
|
@ -0,0 +1,5 @@
|
|||
export const MEDIUM_COLUMN_WIDTH = 300;
|
||||
export const SMALL_COLUMN_WIDTH = 200;
|
||||
export const EXTRA_SMALL_COLUMN_WIDTH = 100;
|
||||
export const LARGE_COLUMN_WIDTH = 400;
|
||||
export const EXTRA_LARGE_COLUMN_WIDTH = 500;
|
|
@ -38,4 +38,25 @@
|
|||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-detail {
|
||||
.ms-DetailsHeader {
|
||||
@include themify($themes) {
|
||||
background: themed('cellBackground') !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ms-DetailsRow-cell {
|
||||
@include themify($themes) {
|
||||
background: themed('cellBackground') !important;
|
||||
}
|
||||
}
|
||||
|
||||
a:link, a:visited, a:hover, a:active {
|
||||
text-decoration: none;
|
||||
@include themify($themes) {
|
||||
color: themed('linkColor');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -143,6 +143,7 @@ exports[`devicePropertiesPerInterface matches snapshot 1`] = `
|
|||
"isMultiline": true,
|
||||
"isResizable": true,
|
||||
"key": "name",
|
||||
"maxWidth": 500,
|
||||
"minWidth": 100,
|
||||
"name": "deviceProperties.columns.name",
|
||||
},
|
||||
|
@ -150,6 +151,7 @@ exports[`devicePropertiesPerInterface matches snapshot 1`] = `
|
|||
"fieldName": "schema",
|
||||
"isResizable": true,
|
||||
"key": "schema",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "deviceProperties.columns.schema",
|
||||
},
|
||||
|
@ -157,6 +159,7 @@ exports[`devicePropertiesPerInterface matches snapshot 1`] = `
|
|||
"fieldName": "unit",
|
||||
"isResizable": true,
|
||||
"key": "unit",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "deviceProperties.columns.unit",
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ import ComplexReportedFormPanel from '../shared/complexReportedFormPanel';
|
|||
import { RenderSimplyTypeValue } from '../shared/simpleReportedSection';
|
||||
import { PropertyContent } from '../../../../api/models/modelDefinition';
|
||||
import { ParsedJsonSchema } from '../../../../api/models/interfaceJsonParserOutput';
|
||||
import { SMALL_COLUMN_WIDTH, EXTRA_LARGE_COLUMN_WIDTH } from '../../../../constants/columnWidth';
|
||||
|
||||
export interface DevicePropertiesDataProps {
|
||||
twinAndSchema: TwinWithSchema[];
|
||||
|
@ -67,9 +68,9 @@ export default class DevicePropertiesPerInterface
|
|||
|
||||
private readonly getColumns = (context: LocalizationContextInterface): IColumn[] => {
|
||||
return [
|
||||
{ key: 'name', name: context.t(ResourceKeys.deviceProperties.columns.name), fieldName: 'name', minWidth: 100, isResizable: true, isMultiline: true },
|
||||
{ key: 'schema', name: context.t(ResourceKeys.deviceProperties.columns.schema), fieldName: 'schema', minWidth: 100, isResizable: true },
|
||||
{ key: 'unit', name: context.t(ResourceKeys.deviceProperties.columns.unit), fieldName: 'unit', minWidth: 100, isResizable: true },
|
||||
{ key: 'name', name: context.t(ResourceKeys.deviceProperties.columns.name), fieldName: 'name', minWidth: 100, maxWidth: EXTRA_LARGE_COLUMN_WIDTH, isResizable: true, isMultiline: true },
|
||||
{ key: 'schema', name: context.t(ResourceKeys.deviceProperties.columns.schema), fieldName: 'schema', minWidth: 100, maxWidth: SMALL_COLUMN_WIDTH, isResizable: true },
|
||||
{ key: 'unit', name: context.t(ResourceKeys.deviceProperties.columns.unit), fieldName: 'unit', minWidth: 100, maxWidth: SMALL_COLUMN_WIDTH, isResizable: true },
|
||||
{ key: 'value', name: context.t(ResourceKeys.deviceProperties.columns.value), fieldName: 'value', minWidth: 150, isResizable: true }
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/devices/deviceListCell matches snapshot while loading 1`] = `
|
||||
<div
|
||||
className="device-list-cell-container"
|
||||
data-selection-index={1}
|
||||
>
|
||||
<div
|
||||
className="device-list-cell-container-content"
|
||||
>
|
||||
<span>
|
||||
common.loading
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -8,18 +8,21 @@ 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, DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Selection } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';
|
||||
import { Announced } from 'office-ui-fabric-react/lib/Announced';
|
||||
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import GroupedListWrapper from '../../../shared/components/groupedList';
|
||||
import { DeviceSummary } from '../../../api/models/deviceSummary';
|
||||
import DeviceQuery from '../../../api/models/deviceQuery';
|
||||
import DeviceListCommandBar from './deviceListCommandBar';
|
||||
import BreadcrumbContainer from '../../../shared/components/breadcrumbContainer';
|
||||
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 MultiLineShimmer from '../../../shared/components/multiLineShimmer';
|
||||
import { LARGE_COLUMN_WIDTH, EXTRA_SMALL_COLUMN_WIDTH, SMALL_COLUMN_WIDTH, MEDIUM_COLUMN_WIDTH } from '../../../constants/columnWidth';
|
||||
import '../../../css/_deviceList.scss';
|
||||
import '../../../css/_layouts.scss';
|
||||
|
||||
|
@ -41,6 +44,7 @@ interface DeviceListState {
|
|||
refreshQuery: number;
|
||||
}
|
||||
|
||||
const SHIMMER_COUNT = 10;
|
||||
class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceListDispatchProps & RouteComponentProps, DeviceListState> {
|
||||
constructor(props: DeviceListDataProps & DeviceListDispatchProps & RouteComponentProps) {
|
||||
super(props);
|
||||
|
@ -50,8 +54,18 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
|
|||
selectedDeviceIds: [],
|
||||
showDeleteConfirmation: false
|
||||
};
|
||||
|
||||
this.selection = new Selection({
|
||||
onSelectionChanged: () => {
|
||||
this.setState({
|
||||
selectedDeviceIds: this.selection.getSelection() && this.selection.getSelection().map(selection => (selection as DeviceSummary).deviceId)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private selection: Selection;
|
||||
|
||||
public render() {
|
||||
if (!this.props.connectionString) {
|
||||
return (<Redirect to="/" />);
|
||||
|
@ -118,132 +132,129 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
|
|||
}
|
||||
|
||||
private readonly showDeviceList = (context: LocalizationContextInterface) => {
|
||||
|
||||
const renderCell = (nestingDepth: number, item: DeviceSummary, itemIndex: number) => {
|
||||
return (
|
||||
<DeviceListCell
|
||||
connectionString={this.props.connectionString}
|
||||
device={item}
|
||||
itemIndex={itemIndex}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.showPaging()}
|
||||
<div className="list-detail">
|
||||
{this.props.isFetching ?
|
||||
<MultiLineShimmer shimmerCount={SHIMMER_COUNT}/> :
|
||||
(this.props.devices && this.props.devices.length !== 0 ?
|
||||
<MarqueeSelection selection={this.selection}>
|
||||
<DetailsList
|
||||
onRenderItemColumn={this.renderItemColumn(context)}
|
||||
items={!this.props.isFetching && this.props.devices}
|
||||
columns={this.getColumns(context)}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
selection={this.selection}
|
||||
/>
|
||||
</MarqueeSelection> :
|
||||
<>
|
||||
<h3>{context.t(ResourceKeys.deviceLists.noDevice)}</h3>
|
||||
<Announced
|
||||
message={context.t(ResourceKeys.deviceLists.noDevice)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly showPaging = () => {
|
||||
return (
|
||||
<ListPaging
|
||||
continuationTokens={this.props.query && this.props.query.continuationTokens}
|
||||
currentPageIndex={this.props.query && this.props.query.currentPageIndex}
|
||||
fetchPage={this.fetchPage}
|
||||
/>
|
||||
<GroupedListWrapper
|
||||
items={this.props.devices}
|
||||
nameKey="deviceId"
|
||||
isLoading={this.props.isFetching}
|
||||
noItemsMessage={context.t(ResourceKeys.deviceLists.noDevice)}
|
||||
onRenderCell={renderCell}
|
||||
onSelectionChanged={this.onRowSelection}
|
||||
columnInfo={[
|
||||
{
|
||||
infoText: context.t(ResourceKeys.deviceLists.columns.deviceId.infoText),
|
||||
name: context.t(ResourceKeys.deviceLists.columns.deviceId.label),
|
||||
onRenderColumn: (group, key) => {
|
||||
const path = this.props.location.pathname.replace(/\/devices\/.*/, `/${ROUTE_PARTS.DEVICES}`);
|
||||
return (
|
||||
<NavLink key={key} className={'deviceId-label'} to={`${path}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.IDENTITY}/?${ROUTE_PARAMS.DEVICE_ID}=${encodeURIComponent(group.name)}`}>
|
||||
{group.name}
|
||||
</NavLink>
|
||||
);
|
||||
},
|
||||
widthPercentage: 20
|
||||
},
|
||||
{
|
||||
infoText: context.t(ResourceKeys.deviceLists.columns.status.infoText),
|
||||
name: context.t(ResourceKeys.deviceLists.columns.status.label),
|
||||
onRenderColumn: (group, key) => {
|
||||
return (
|
||||
<Label
|
||||
key={key}
|
||||
>
|
||||
{(group.data as DeviceSummary).status}
|
||||
</Label>
|
||||
);
|
||||
},
|
||||
widthPercentage: 10
|
||||
},
|
||||
{
|
||||
name: context.t(ResourceKeys.deviceLists.columns.connection),
|
||||
onRenderColumn: (group, key) => {
|
||||
return (
|
||||
<Label
|
||||
key={key}
|
||||
>
|
||||
{(group.data as DeviceSummary).connectionState}
|
||||
</Label>
|
||||
);
|
||||
},
|
||||
widthPercentage: 10
|
||||
},
|
||||
{
|
||||
name: context.t(ResourceKeys.deviceLists.columns.authenticationType),
|
||||
onRenderColumn: (group, key) => {
|
||||
return (
|
||||
<Label
|
||||
key={key}
|
||||
>
|
||||
{(group.data as DeviceSummary).authenticationType}
|
||||
</Label>
|
||||
);
|
||||
},
|
||||
widthPercentage: 10
|
||||
},
|
||||
{
|
||||
name: context.t(ResourceKeys.deviceLists.columns.lastActivityTime),
|
||||
onRenderColumn: (group, key) => {
|
||||
return (
|
||||
<Label
|
||||
key={key}
|
||||
>
|
||||
{(group.data as DeviceSummary).lastActivityTime || '--'}
|
||||
</Label>
|
||||
);
|
||||
},
|
||||
widthPercentage: 15
|
||||
},
|
||||
{
|
||||
name: context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime),
|
||||
onRenderColumn: (group, key) => {
|
||||
return (
|
||||
<Label
|
||||
key={key}
|
||||
>
|
||||
{(group.data as DeviceSummary).statusUpdatedTime || '--'}
|
||||
</Label>
|
||||
);
|
||||
},
|
||||
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
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly getColumns = (context: LocalizationContextInterface): IColumn[] => {
|
||||
return [
|
||||
{ fieldName: 'id', isMultiline: true, isResizable: true, key: 'id',
|
||||
maxWidth: LARGE_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.deviceId.label) },
|
||||
{ fieldName: 'status', isResizable: true, key: 'status',
|
||||
maxWidth: EXTRA_SMALL_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.status.label)},
|
||||
{ fieldName: 'connection', isResizable: true, key: 'connection',
|
||||
maxWidth: SMALL_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.connection) },
|
||||
{ fieldName: 'authenticationType', isMultiline: true, isResizable: true, key: 'authenticationType',
|
||||
maxWidth: SMALL_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.authenticationType)},
|
||||
{ fieldName: 'lastActivityTime', isMultiline: true, isResizable: true, key: 'lastActivityTime',
|
||||
maxWidth: MEDIUM_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.lastActivityTime)},
|
||||
{ fieldName: 'statusUpdatedTime', isMultiline: true, isResizable: true, key: 'statusUpdatedTime',
|
||||
maxWidth: MEDIUM_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime)},
|
||||
{ fieldName: 'edge', isResizable: true, key: 'edge',
|
||||
minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.label)},
|
||||
];
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:cyclomatic-complexity
|
||||
private readonly renderItemColumn = (context: LocalizationContextInterface) => (item: DeviceSummary, index: number, column: IColumn) => {
|
||||
switch (column.key) {
|
||||
case 'id':
|
||||
const path = this.props.location.pathname.replace(/\/devices\/.*/, `/${ROUTE_PARTS.DEVICES}`);
|
||||
return (
|
||||
<NavLink key={column.key} to={`${path}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.IDENTITY}/?${ROUTE_PARAMS.DEVICE_ID}=${encodeURIComponent(item.deviceId)}`}>
|
||||
{item.deviceId}
|
||||
</NavLink>
|
||||
);
|
||||
case 'status':
|
||||
return (
|
||||
<Label
|
||||
key={column.key}
|
||||
>
|
||||
{item.status}
|
||||
</Label>
|
||||
);
|
||||
case 'connection':
|
||||
return (
|
||||
<Label
|
||||
key={column.key}
|
||||
>
|
||||
{item.connectionState}
|
||||
</Label>
|
||||
);
|
||||
case 'authenticationType':
|
||||
return (
|
||||
<Label
|
||||
key={column.key}
|
||||
>
|
||||
{item.authenticationType}
|
||||
</Label>
|
||||
);
|
||||
case 'lastActivityTime':
|
||||
return (
|
||||
<Label
|
||||
key={column.key}
|
||||
>
|
||||
{item.lastActivityTime || '--'}
|
||||
</Label>
|
||||
);
|
||||
case 'statusUpdatedTime':
|
||||
return (
|
||||
<Label
|
||||
key={column.key}
|
||||
>
|
||||
{item.statusUpdatedTime || '--'}
|
||||
</Label>
|
||||
);
|
||||
case 'edge':
|
||||
const isEdge = item.iotEdge;
|
||||
return (
|
||||
<Icon
|
||||
key={column.key}
|
||||
iconName={isEdge && CHECK}
|
||||
ariaLabel={isEdge ?
|
||||
context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.yes) : context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.no)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly fetchPage = (pageNumber: number) => {
|
||||
const { query } = this.props;
|
||||
return this.props.listDevices({
|
||||
|
@ -307,10 +318,6 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
|
|||
showDeleteConfirmation: false
|
||||
});
|
||||
}
|
||||
|
||||
private readonly onRowSelection = (devices: DeviceSummary[]) => {
|
||||
this.setState({ selectedDeviceIds: devices.map(device => device.deviceId) });
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(DeviceListComponent);
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { DeviceListCell, DeviceListCellProps } from './deviceListCell';
|
||||
import { mountWithLocalization, testSnapshot } from '../../../shared/utils/testHelpers';
|
||||
|
||||
describe('components/devices/deviceListCell', () => {
|
||||
const deviceListCellProps: DeviceListCellProps = {
|
||||
connectionString: 'string',
|
||||
device: {
|
||||
authenticationType: 'sas',
|
||||
cloudToDeviceMessageCount: '0',
|
||||
connectionState: 'connected',
|
||||
deviceId: 'testDeviceId',
|
||||
iotEdge: false,
|
||||
lastActivityTime: '0001-01-01T00:00:00Z',
|
||||
status: 'Enabled',
|
||||
statusUpdatedTime: '0001-01-01T00:00:00Z'
|
||||
},
|
||||
itemIndex: 1,
|
||||
};
|
||||
|
||||
const getComponent = (overrides = {}) => {
|
||||
const props = {
|
||||
...deviceListCellProps,
|
||||
...overrides,
|
||||
};
|
||||
return <DeviceListCell {...props} />;
|
||||
};
|
||||
|
||||
it('matches snapshot while loading', () => {
|
||||
testSnapshot(getComponent());
|
||||
});
|
||||
|
||||
it('render cell with pnp icon when interface ids are retrieved', () => {
|
||||
const wrapper = mountWithLocalization(getComponent());
|
||||
const cell = wrapper.find(DeviceListCell);
|
||||
cell.setState({isLoading: false, interfaceIds: ['interfaceId1']});
|
||||
wrapper.update();
|
||||
|
||||
const icon = wrapper.find(Icon).first();
|
||||
expect(icon.props().iconName).toEqual('pnp-svg');
|
||||
});
|
||||
});
|
|
@ -1,144 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { registerIcons } from 'office-ui-fabric-react/lib/Styling';
|
||||
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': (
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 64 64" enableBackground="new 0 0 64 64">
|
||||
<path fill="#2F76BC" d="M58.7,9.9L58.7,9.9c-0.4,0-0.8,0-0.8,0.4L50.2,20c-2.8-6.9-9.3-11.7-17.4-11.3c-8.1,0-15,4.9-17.8,12.2
|
||||
C6.9,21.6,0,27.7,0,37.4S8.5,54,17.8,54h31.6C57.5,54,64,47.1,64,39.8c0-6.1-4.1-11.3-9.7-13l9.3-12.2c0,0,0.4-0.4,0-0.8l0,0V9.9
|
||||
L58.7,9.9z"/>
|
||||
<path fill="#75D0EB" d="M17.8,32.9l-1.2-1.6c-0.4-0.4-0.4-1.2,0-1.6l4.1-3.6c0.4,0,0.4-0.4,0.8-0.4c0.4,0,0.4,0,0.8,0.4l10.9,11.7
|
||||
l24.7-32c0.4-0.4,0.4-0.4,0.8-0.4c0.4,0,0.4,0,0.4,0l4.5,3.2C64,8.6,64,9,64,9.5c0,0.4,0,0.4-0.4,0.8L34,48.3
|
||||
c-0.4,0.4-1.2,0.4-1.6,0L17.8,32.9z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
});
|
||||
//tslint: enable
|
||||
|
||||
export interface DeviceListCellProps {
|
||||
itemIndex: number | string;
|
||||
connectionString: string;
|
||||
device: DeviceSummary;
|
||||
}
|
||||
|
||||
export interface DeviceListCellState {
|
||||
isLoading: boolean;
|
||||
interfaceIds: string[];
|
||||
}
|
||||
|
||||
export class DeviceListCell extends React.PureComponent<DeviceListCellProps, DeviceListCellState> {
|
||||
|
||||
private isComponentMounted: boolean;
|
||||
constructor(props: DeviceListCellProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
interfaceIds: []
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { itemIndex, device } = this.props;
|
||||
|
||||
return (
|
||||
<LocalizationContextConsumer>
|
||||
{(context: LocalizationContextInterface) => (
|
||||
!this.state.isLoading ?
|
||||
<div className="device-list-cell-container" data-selection-index={itemIndex}>
|
||||
{this.renderCellDeviceInfo(device, context)}
|
||||
{this.state.interfaceIds.length !== 0 && this.renderCellInterfaceInfo(context)}
|
||||
</div>:
|
||||
<div className="device-list-cell-container" data-selection-index={itemIndex}>
|
||||
{this.renderLoadingInfo(context)}
|
||||
</div>
|
||||
)}
|
||||
</LocalizationContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
public componentWillMount() {
|
||||
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 (
|
||||
<div className="device-list-cell-container-content">
|
||||
<span className="device-list-cell-item first">{`${context.t(ResourceKeys.deviceLists.columns.lastActivityTime)}: `}<span className="data">{device.lastActivityTime || context.t(ResourceKeys.deviceLists.noData)}</span></span>
|
||||
<span className={`device-list-cell-item ${this.state.interfaceIds.length !== 0 ? '' : 'last'}`}>{`${context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime)}: `}<span className="data">{device.statusUpdatedTime || context.t(ResourceKeys.deviceLists.noData)}</span></span>
|
||||
{this.state.interfaceIds.length !== 0 &&
|
||||
<span className="device-list-cell-item last">
|
||||
<Icon
|
||||
iconName="pnp-svg"
|
||||
className="icon"
|
||||
ariaLabel={context.t(ResourceKeys.deviceLists.columns.isPnpDevice)}
|
||||
/>
|
||||
{context.t(ResourceKeys.deviceLists.columns.isPnpDevice)}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly renderCellInterfaceInfo = (context: LocalizationContextInterface) => {
|
||||
const listInterfaces = this.state.interfaceIds.join("; ");
|
||||
|
||||
return (
|
||||
<div className="device-list-cell-container-content">
|
||||
{<span className="device-list-cell-item first no-border">
|
||||
{`${context.t(ResourceKeys.deviceLists.columns.interfaces)}: `}
|
||||
<span className="data">{listInterfaces}</span>
|
||||
</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly renderLoadingInfo = (context: LocalizationContextInterface) => {
|
||||
return (
|
||||
<div className="device-list-cell-container-content">
|
||||
<span>{context.t(ResourceKeys.common.loading)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,409 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/GroupedList render GroupedList matches snapshot 1`] = `
|
||||
<div
|
||||
className="grouped-list"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="grouped-list-header"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"columnGap": "5px",
|
||||
"display": "grid",
|
||||
"gridTemplateColumns": "2% 10% 10% auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
key="0"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 2,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Name
|
||||
</Component>
|
||||
<Component
|
||||
key="1"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 3,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Body
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="grouped-list-header-border"
|
||||
/>
|
||||
</div>
|
||||
<StyledMarqueeSelectionBase
|
||||
isEnabled={true}
|
||||
selection={
|
||||
Selection {
|
||||
"_anchoredIndex": 0,
|
||||
"_canSelectItem": [Function],
|
||||
"_changeEventSuppressionCount": 0,
|
||||
"_exemptedCount": 0,
|
||||
"_exemptedIndices": Object {},
|
||||
"_getKey": [Function],
|
||||
"_isModal": false,
|
||||
"_items": Array [
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World",
|
||||
"Name": "Hello",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello0",
|
||||
"name": "Hello",
|
||||
"startIndex": 0,
|
||||
},
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World1",
|
||||
"Name": "Hello1",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello11",
|
||||
"name": "Hello1",
|
||||
"startIndex": 1,
|
||||
},
|
||||
],
|
||||
"_keyToIndexMap": Object {
|
||||
"Hello0": 0,
|
||||
"Hello11": 1,
|
||||
},
|
||||
"_onSelectionChanged": [Function],
|
||||
"_selectedItems": null,
|
||||
"_unselectableCount": 0,
|
||||
"_unselectableIndices": Object {
|
||||
"0": false,
|
||||
"1": false,
|
||||
},
|
||||
"count": 0,
|
||||
"mode": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
<SelectionZone
|
||||
isSelectedOnFocus={true}
|
||||
selection={
|
||||
Selection {
|
||||
"_anchoredIndex": 0,
|
||||
"_canSelectItem": [Function],
|
||||
"_changeEventSuppressionCount": 0,
|
||||
"_exemptedCount": 0,
|
||||
"_exemptedIndices": Object {},
|
||||
"_getKey": [Function],
|
||||
"_isModal": false,
|
||||
"_items": Array [
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World",
|
||||
"Name": "Hello",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello0",
|
||||
"name": "Hello",
|
||||
"startIndex": 0,
|
||||
},
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World1",
|
||||
"Name": "Hello1",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello11",
|
||||
"name": "Hello1",
|
||||
"startIndex": 1,
|
||||
},
|
||||
],
|
||||
"_keyToIndexMap": Object {
|
||||
"Hello0": 0,
|
||||
"Hello11": 1,
|
||||
},
|
||||
"_onSelectionChanged": [Function],
|
||||
"_selectedItems": null,
|
||||
"_unselectableCount": 0,
|
||||
"_unselectableIndices": Object {
|
||||
"0": false,
|
||||
"1": false,
|
||||
},
|
||||
"count": 0,
|
||||
"mode": 2,
|
||||
}
|
||||
}
|
||||
selectionMode={2}
|
||||
>
|
||||
<StyledGroupedListBase
|
||||
groupProps={
|
||||
Object {
|
||||
"onRenderHeader": [Function],
|
||||
}
|
||||
}
|
||||
groups={
|
||||
Array [
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World",
|
||||
"Name": "Hello",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello0",
|
||||
"name": "Hello",
|
||||
"startIndex": 0,
|
||||
},
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World1",
|
||||
"Name": "Hello1",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello11",
|
||||
"name": "Hello1",
|
||||
"startIndex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"Body": "World",
|
||||
"Name": "Hello",
|
||||
},
|
||||
Object {
|
||||
"Body": "World1",
|
||||
"Name": "Hello1",
|
||||
},
|
||||
]
|
||||
}
|
||||
onRenderCell={[Function]}
|
||||
onShouldVirtualize={[Function]}
|
||||
selection={
|
||||
Selection {
|
||||
"_anchoredIndex": 0,
|
||||
"_canSelectItem": [Function],
|
||||
"_changeEventSuppressionCount": 0,
|
||||
"_exemptedCount": 0,
|
||||
"_exemptedIndices": Object {},
|
||||
"_getKey": [Function],
|
||||
"_isModal": false,
|
||||
"_items": Array [
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World",
|
||||
"Name": "Hello",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello0",
|
||||
"name": "Hello",
|
||||
"startIndex": 0,
|
||||
},
|
||||
Object {
|
||||
"count": 1,
|
||||
"data": Object {
|
||||
"Body": "World1",
|
||||
"Name": "Hello1",
|
||||
},
|
||||
"isCollapsed": true,
|
||||
"key": "Hello11",
|
||||
"name": "Hello1",
|
||||
"startIndex": 1,
|
||||
},
|
||||
],
|
||||
"_keyToIndexMap": Object {
|
||||
"Hello0": 0,
|
||||
"Hello11": 1,
|
||||
},
|
||||
"_onSelectionChanged": [Function],
|
||||
"_selectedItems": null,
|
||||
"_unselectableCount": 0,
|
||||
"_unselectableIndices": Object {
|
||||
"0": false,
|
||||
"1": false,
|
||||
},
|
||||
"count": 0,
|
||||
"mode": 2,
|
||||
}
|
||||
}
|
||||
selectionMode={2}
|
||||
/>
|
||||
</SelectionZone>
|
||||
</StyledMarqueeSelectionBase>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/GroupedList render GroupedList matches snapshot when in a loading state 1`] = `
|
||||
<div
|
||||
className="grouped-list"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="grouped-list-header"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"columnGap": "5px",
|
||||
"display": "grid",
|
||||
"gridTemplateColumns": "2% 10% 10% auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
key="0"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 2,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Name
|
||||
</Component>
|
||||
<Component
|
||||
key="1"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 3,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Body
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="grouped-list-header-border"
|
||||
/>
|
||||
</div>
|
||||
<Component
|
||||
shimmerCount={10}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/GroupedList render GroupedList matches snapshot when no items 1`] = `
|
||||
<div
|
||||
className="grouped-list"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="grouped-list-header"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"columnGap": "5px",
|
||||
"display": "grid",
|
||||
"gridTemplateColumns": "2% 10% 10% auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
key="0"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 2,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Name
|
||||
</Component>
|
||||
<Component
|
||||
key="1"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 3,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Body
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="grouped-list-header-border"
|
||||
/>
|
||||
</div>
|
||||
<h3>
|
||||
No Items
|
||||
</h3>
|
||||
<StyledAnnouncedBase
|
||||
message="No Items"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/GroupedList render GroupedList matches snapshot with undefined items 1`] = `
|
||||
<div
|
||||
className="grouped-list"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="grouped-list-header"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"columnGap": "5px",
|
||||
"display": "grid",
|
||||
"gridTemplateColumns": "2% 10% 10% auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
key="0"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 2,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Name
|
||||
</Component>
|
||||
<Component
|
||||
key="1"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"gridColumnStart": 3,
|
||||
}
|
||||
}
|
||||
tooltipText="Text"
|
||||
>
|
||||
Body
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="grouped-list-header-border"
|
||||
/>
|
||||
</div>
|
||||
<h3>
|
||||
No Items
|
||||
</h3>
|
||||
<StyledAnnouncedBase
|
||||
message="No Items"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -1,81 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import GroupedList, { GroupedListProps } from './groupedList';
|
||||
|
||||
describe('components/GroupedList', () => {
|
||||
|
||||
const getComponent = (overrides = {}) => {
|
||||
const testItems = [
|
||||
{
|
||||
Body: 'World',
|
||||
Name: 'Hello',
|
||||
},
|
||||
{
|
||||
Body: 'World1',
|
||||
Name: 'Hello1'
|
||||
}
|
||||
];
|
||||
|
||||
const onRenderCell = () => (<div/>);
|
||||
const defaultColumnInfo = [
|
||||
{
|
||||
infoText: 'Text',
|
||||
name: 'Name',
|
||||
onRenderColumn: group => {
|
||||
return (<span>{group.name}</span>);
|
||||
},
|
||||
widthPercentage: 10
|
||||
},
|
||||
{
|
||||
infoText: 'Text',
|
||||
name: 'Body',
|
||||
onRenderColumn: group => {
|
||||
return (<span>{group.name}</span>);
|
||||
},
|
||||
widthPercentage: 10
|
||||
}
|
||||
];
|
||||
|
||||
const props: GroupedListProps<typeof testItems[0]> = {
|
||||
columnInfo: defaultColumnInfo,
|
||||
isLoading: false,
|
||||
items: testItems,
|
||||
nameKey: 'Name',
|
||||
noItemsMessage: 'No Items',
|
||||
onRenderCell,
|
||||
...overrides
|
||||
};
|
||||
|
||||
return <GroupedList {...props} />;
|
||||
};
|
||||
|
||||
context('render GroupedList', () => {
|
||||
it('matches snapshot', () => {
|
||||
const wrapper = shallow(getComponent());
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when no items', () => {
|
||||
const wrapper = shallow(getComponent({items: []}));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot with undefined items', () => {
|
||||
const wrapper = shallow(getComponent({items: undefined}));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when in a loading state', () => {
|
||||
const wrapper = shallow(getComponent({isLoading: true}));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,263 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
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 { 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 { Announced } from 'office-ui-fabric-react/lib/Announced';
|
||||
import LabelWithTooltip from '../labelWithTooltip';
|
||||
import { GroupedList as GroupedListIconNames } from '../../../constants/iconNames';
|
||||
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;
|
||||
|
||||
export interface GroupedListProps<T> {
|
||||
isLoading: boolean;
|
||||
items: T[];
|
||||
nameKey: keyof T;
|
||||
noItemsMessage: string;
|
||||
columnInfo?: GroupedListColumn[];
|
||||
onRenderCell: (nestingDepth?: number, item?: T, index?: number) => React.ReactNode;
|
||||
onSelectionChanged?: (selectedItems: T[]) => void;
|
||||
}
|
||||
|
||||
export interface GroupedListColumn {
|
||||
name: string;
|
||||
infoText?: string;
|
||||
widthPercentage: number;
|
||||
onRenderColumn: (group: IGroup, key?: string | number) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface GroupedListState {
|
||||
groups: IGroup[];
|
||||
selection: ISelection;
|
||||
selectionMode: SelectionMode;
|
||||
}
|
||||
|
||||
export default class GroupedListWrapper<T> extends React.Component<GroupedListProps<T>, GroupedListState> {
|
||||
|
||||
constructor(props: GroupedListProps<T>) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
groups: [],
|
||||
selection: new Selection({ onSelectionChanged: this.onSelectionChanged }),
|
||||
selectionMode: SelectionMode.multiple
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { columnInfo, items, isLoading, noItemsMessage } = this.props;
|
||||
const { selection, selectionMode } = this.state;
|
||||
|
||||
return (
|
||||
<div className="grouped-list">
|
||||
{this.renderListHeader(columnInfo)}
|
||||
{!!isLoading ?
|
||||
<MultiLineShimmer shimmerCount={SHIMMER_COUNT}/>
|
||||
: !items || items.length === 0 ? (
|
||||
<>
|
||||
<h3>{noItemsMessage}</h3>
|
||||
<Announced
|
||||
message={noItemsMessage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<MarqueeSelection selection={selection} isEnabled={selection.mode === SelectionMode.multiple}>
|
||||
<SelectionZone selection={selection}>
|
||||
<GroupedList
|
||||
items={this.props.items}
|
||||
groups={this.state.groups}
|
||||
groupProps={{
|
||||
onRenderHeader: this.onRenderHeader
|
||||
}}
|
||||
onRenderCell={this.onRenderCell}
|
||||
selection={selection}
|
||||
selectionMode={selectionMode}
|
||||
onShouldVirtualize={this.onShouldVirtualize}
|
||||
/>
|
||||
</SelectionZone>
|
||||
</MarqueeSelection>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private onShouldVirtualize = (props: IListProps): boolean => {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps<T>(props: GroupedListProps<T>, state: GroupedListState): Partial<GroupedListState> | null {
|
||||
if (typeof props.items !== 'undefined' && typeof props.nameKey !== 'undefined') {
|
||||
const groups = GroupedListWrapper.createGroups(props);
|
||||
|
||||
state.selection.setItems(groups, false);
|
||||
|
||||
return {
|
||||
groups
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private onSelectionChanged = (): void => {
|
||||
const { onSelectionChanged } = this.props;
|
||||
const { groups, selection } = this.state;
|
||||
if (!!onSelectionChanged) {
|
||||
const items = groups.filter(group => selection.isIndexSelected(group.startIndex)).map(group => {
|
||||
return group.data;
|
||||
});
|
||||
onSelectionChanged(items);
|
||||
}
|
||||
}
|
||||
|
||||
private static createGroups<T>(props: GroupedListProps<T>) {
|
||||
return (props.items && props.items.map((item, index): IGroup => {
|
||||
const itemName: string = item[props.nameKey] && item[props.nameKey].toString();
|
||||
|
||||
return {
|
||||
count: 1,
|
||||
data: item,
|
||||
isCollapsed: true,
|
||||
key: itemName + index,
|
||||
name: itemName,
|
||||
startIndex: index
|
||||
};
|
||||
})) || [];
|
||||
}
|
||||
|
||||
private readonly onRenderCell = (nestingDepth?: number, item?: T, index?: number) => {
|
||||
return (
|
||||
<div className="grouped-list-group-cell">
|
||||
{this.props.onRenderCell(nestingDepth, item, index)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly onRenderHeader = (props: IGroupDividerProps) => {
|
||||
const { columnInfo } = this.props;
|
||||
const { selection } = this.state;
|
||||
const columns = columnInfo || [];
|
||||
|
||||
const toggleCollapse = (event: Event | React.MouseEvent<HTMLDivElement | HTMLAnchorElement | HTMLButtonElement | BaseButton | Button | HTMLSpanElement>): void => {
|
||||
props.onToggleCollapse!(props.group!);
|
||||
|
||||
(event as Event).cancelBubble = true; // tslint:disable-line
|
||||
if (event.stopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const renderColumns = columns.map((column, index) => {
|
||||
return column.onRenderColumn(props.group, index);
|
||||
});
|
||||
|
||||
const customStyles = {
|
||||
height: GRID_STYLE_CONSTANTS.HEADER_HEIGHT
|
||||
};
|
||||
const styles = {...this.generateColumnStyle(columns), ...customStyles};
|
||||
|
||||
const onCheckboxChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
selection.setIndexSelected(props.group.startIndex, !!checked, false);
|
||||
};
|
||||
|
||||
const isSelected = selection && selection.isIndexSelected(props.group.startIndex);
|
||||
|
||||
return (
|
||||
<div className={'grouped-list-group-header'} key={props.group.key} onClick={toggleCollapse} data-selection-disabled={true} data-is-focusable={true} data-selection-index={props.group.startIndex}>
|
||||
<div
|
||||
className={`grouped-list-group-content ${props.group.isCollapsed ? '' : 'expanded'}`}
|
||||
style={styles}
|
||||
>
|
||||
<Checkbox
|
||||
className={'grouped-list-group-checkbox'}
|
||||
styles={
|
||||
{
|
||||
checkbox: {
|
||||
borderRadius: `${CHECKBOX_WIDTH_PIXELS}px`,
|
||||
height: `${CHECKBOX_WIDTH_PIXELS}px`,
|
||||
width: `${CHECKBOX_WIDTH_PIXELS}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
checked={isSelected}
|
||||
onChange={onCheckboxChange}
|
||||
/>
|
||||
{renderColumns}
|
||||
<IconButton
|
||||
className="collapse"
|
||||
iconProps={{
|
||||
iconName: props.group.isCollapsed ? GroupedListIconNames.OPEN : GroupedListIconNames.CLOSE,
|
||||
}}
|
||||
onClick={toggleCollapse}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly renderListHeader = (columns: GroupedListColumn[] = []) => {
|
||||
const columnOffset = 2;
|
||||
|
||||
const styles = this.generateColumnStyle(columns);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={'grouped-list-header'}
|
||||
style={styles}
|
||||
>
|
||||
{
|
||||
columns.map((column, index) => {
|
||||
return (
|
||||
<LabelWithTooltip
|
||||
key={index}
|
||||
style={{
|
||||
fontSize: LABEL_FONT_SIZE,
|
||||
gridColumnStart: index + columnOffset
|
||||
}}
|
||||
tooltipText={column.infoText}
|
||||
>
|
||||
{column.name}
|
||||
</LabelWithTooltip>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className="grouped-list-header-border" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly generateColumnString = (columns?: GroupedListColumn[]) => {
|
||||
let columnsString = `${CHECKBOX_WIDTH_PERCENTAGE}% `;
|
||||
|
||||
columns.forEach(column => {
|
||||
columnsString += column.widthPercentage + '% ';
|
||||
});
|
||||
|
||||
columnsString += 'auto';
|
||||
|
||||
return columnsString;
|
||||
}
|
||||
|
||||
private readonly generateColumnStyle = (columns?: GroupedListColumn[]) => {
|
||||
const columnString = this.generateColumnString(columns);
|
||||
|
||||
return {
|
||||
alignItems: GRID_STYLE_CONSTANTS.ALIGN_ITEMS,
|
||||
columnGap: GRID_STYLE_CONSTANTS.COLUMN_GAP,
|
||||
display: GRID_STYLE_CONSTANTS.DISPLAY,
|
||||
gridTemplateColumns: columnString
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export enum GRID_STYLE_CONSTANTS {
|
||||
ALIGN_ITEMS = 'center',
|
||||
COLUMN_GAP = '5px',
|
||||
DISPLAY = 'grid',
|
||||
GRID_TEMPLATE_COLUMNS = '2% auto',
|
||||
HEADER_HEIGHT = '40px'
|
||||
}
|
||||
export const LABEL_FONT_SIZE = 12;
|
||||
export const CHECKBOX_WIDTH_PIXELS = 14;
|
||||
export const CHECKBOX_WIDTH_PERCENTAGE = 2;
|
|
@ -1,7 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import GroupedList from './groupedList';
|
||||
|
||||
export default GroupedList;
|
Загрузка…
Ссылка в новой задаче