• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.76
/src/js/components/devices/devicelist.js
1
// Copyright 2015 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
15

16
// material ui
17
import { Settings as SettingsIcon, Sort as SortIcon } from '@mui/icons-material';
18
import { Checkbox } from '@mui/material';
19
import { makeStyles } from 'tss-react/mui';
20

21
import { SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
22
import { DEVICE_LIST_DEFAULTS } from '../../constants/deviceConstants';
23
import { deepCompare, isDarkMode, toggle } from '../../helpers';
24
import useWindowSize from '../../utils/resizehook';
25
import Loader from '../common/loader';
26
import MenderTooltip from '../common/mendertooltip';
27
import Pagination from '../common/pagination';
28
import DeviceListItem from './devicelistitem';
29

30
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
8✔
31

32
const sortingNotes = {
8✔
33
  name: 'Sorting by Name will only work properly with devices that already have a device name defined'
34
};
35

36
const useStyles = makeStyles()(theme => ({
23✔
37
  header: {
38
    color: theme.palette.text.hint
39
  },
40
  resizer: {
41
    cursor: 'col-resize',
42
    paddingLeft: 5,
43
    paddingRight: 5
44
  },
45
  resizeHandle: {
46
    width: 4,
47
    background: 'initial',
48
    ['&.hovering']: {
49
      background: theme.palette.grey[600]
50
    },
51
    ['&.resizing']: {
52
      background: isDarkMode(theme.palette.mode) ? theme.palette.grey[200] : theme.palette.grey[900]
23!
53
    }
54
  }
55
}));
56

57
const HeaderItem = ({ column, columnCount, index, sortCol, sortDown, onSort, onResizeChange, onResizeFinish, resizable }) => {
8✔
58
  const [isHovering, setIsHovering] = useState(false);
54✔
59
  const [shouldRemoveListeners, setShouldRemoveListeners] = useState(false);
54✔
60
  const resizeRef = useRef();
54✔
61
  const ref = useRef();
54✔
62
  const { classes } = useStyles();
54✔
63

64
  const onMouseOut = () => setIsHovering(false);
54✔
65

66
  const onMouseOver = () => setIsHovering(true);
54✔
67

68
  const mouseMove = useCallback(
54✔
69
    e => {
70
      if (resizable && resizeRef.current) {
82!
71
        onResizeChange(e, { index, prev: resizeRef.current, ref });
×
72
        resizeRef.current = e.clientX;
×
73
      }
74
    },
75
    [index, onResizeChange, resizable]
76
  );
77

78
  const mouseUp = useCallback(
54✔
79
    e => {
80
      if (resizeRef.current) {
82!
81
        onResizeFinish(e, { index, prev: resizeRef.current, ref });
×
82
        resizeRef.current = null;
×
83
        setShouldRemoveListeners(true);
×
84
      }
85
    },
86
    [index, onResizeFinish]
87
  );
88

89
  const mouseDown = e => (resizeRef.current = e.clientX);
54✔
90

91
  useEffect(() => {
54✔
92
    window.addEventListener('mousemove', mouseMove);
24✔
93
    window.addEventListener('mouseup', mouseUp);
24✔
94
    return () => {
24✔
95
      setShouldRemoveListeners(!!resizeRef.current);
24✔
96
    };
97
  }, [mouseMove, mouseUp]);
98

99
  useEffect(() => {
54✔
100
    if (shouldRemoveListeners) {
24!
101
      window.removeEventListener('mousemove', mouseMove);
×
102
      window.removeEventListener('mouseup', mouseUp);
×
103
      setShouldRemoveListeners(false);
×
104
    }
105
  }, [shouldRemoveListeners, mouseMove, mouseUp]);
106

107
  let resizeHandleClassName = resizable && isHovering ? 'hovering' : '';
54!
108
  resizeHandleClassName = resizeRef.current ? 'resizing' : resizeHandleClassName;
54!
109

110
  const header = (
111
    <div className="columnHeader flexbox space-between relative" style={column.style} onMouseEnter={onMouseOver} onMouseLeave={onMouseOut} ref={ref}>
54✔
112
      <div className="flexbox center-aligned" onClick={() => onSort(column.attribute ? column.attribute : {})}>
×
113
        {column.title}
114
        {column.sortable && (
104✔
115
          <SortIcon
116
            className={`sortIcon ${sortCol === column.attribute.name ? 'selected' : ''} ${(sortDown === SORTING_OPTIONS.desc).toString()}`}
50✔
117
            style={{ fontSize: 16 }}
118
          />
119
        )}
120
      </div>
121
      <div className="flexbox center-aligned full-height">
122
        {column.customize && <SettingsIcon onClick={column.customize} style={{ fontSize: 16 }} />}
64✔
123
        {index < columnCount - 2 && (
86✔
124
          <div onMouseDown={mouseDown} className={`${classes.resizer} full-height`}>
125
            <div className={`full-height ${classes.resizeHandle} ${resizeHandleClassName}`} />
126
          </div>
127
        )}
128
      </div>
129
    </div>
130
  );
131
  return column.sortable && sortingNotes[column.attribute.name] ? (
54!
132
    <MenderTooltip title={sortingNotes[column.attribute.name]} placement="top-start">
133
      {header}
134
    </MenderTooltip>
135
  ) : (
136
    header
137
  );
138
};
139

140
const getRelevantColumns = (columnElements, selectable) => [...columnElements].slice(selectable ? 1 : 0, columnElements.length - 1);
12✔
141

142
export const calculateResizeChange = ({ columnElements, columnHeaders, e, index, prev, selectable }) => {
8✔
143
  const isShrinkage = prev > e.clientX ? -1 : 1;
3✔
144
  const columnDelta = Math.abs(e.clientX - prev) * isShrinkage;
3✔
145
  const relevantColumns = getRelevantColumns(columnElements, selectable);
3✔
146
  const canModifyNextColumn = index + 1 < columnHeaders.length - 1;
3✔
147

148
  return relevantColumns.reduce((accu, element, columnIndex) => {
3✔
149
    const currentWidth = element.offsetWidth;
15✔
150
    let column = { attribute: columnHeaders[columnIndex + 1].attribute, size: currentWidth };
15✔
151
    if (canModifyNextColumn && index === columnIndex) {
15✔
152
      column.size = currentWidth + columnDelta;
2✔
153
    } else if (canModifyNextColumn && index + 1 === columnIndex) {
13✔
154
      column.size = currentWidth - columnDelta;
2✔
155
    }
156
    accu.push(column);
15✔
157
    return accu;
15✔
158
  }, []);
159
};
160

161
export const minCellWidth = 150;
8✔
162
const getTemplateColumns = (columns, selectable) =>
8✔
163
  selectable ? `52px ${columns} minmax(${minCellWidth}px, 1fr)` : `${columns} minmax(${minCellWidth}px, 1fr)`;
9✔
164

165
const getColumnsStyle = (columns, defaultSize, selectable) => {
8✔
166
  const template = columns.map(({ size }) => `minmax(${minCellWidth}px, ${size ? `${size}px` : defaultSize})`);
27✔
167
  // applying styles via state changes would lead to less smooth changes, so we set the style directly on the components
168
  return getTemplateColumns(template.join(' '), selectable);
9✔
169
};
170

171
export const DeviceList = ({
8✔
172
  columnHeaders,
173
  customColumnSizes,
174
  devices,
175
  deviceListState,
176
  idAttribute,
177
  onChangeRowsPerPage,
178
  PaginationProps = {},
13✔
179
  onExpandClick,
180
  onResizeColumns,
181
  onPageChange,
182
  onSelect,
183
  onSort,
184
  pageLoading,
185
  pageTotal
186
}) => {
187
  const { page: pageNo = defaultPage, perPage: pageLength = defaultPerPage, selection: selectedRows = [], sort = {} } = deviceListState;
13!
188
  const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol } = sort;
13!
189
  const deviceListRef = useRef();
13✔
190
  const selectedRowsRef = useRef(selectedRows);
13✔
191
  const initRef = useRef();
13✔
192
  const [resizeTrigger, setResizeTrigger] = useState(false);
13✔
193

194
  const size = useWindowSize();
13✔
195
  const selectable = !!onSelect;
13✔
196
  const { classes } = useStyles();
13✔
197

198
  useEffect(() => {
13✔
199
    selectedRowsRef.current = selectedRows;
5✔
200
  }, [selectedRows]);
201

202
  useEffect(() => {
13✔
203
    if (!deviceListRef.current) {
9!
204
      return;
×
205
    }
206
    const relevantColumns = getRelevantColumns(deviceListRef.current.querySelector('.deviceListRow').children, selectable);
9✔
207
    deviceListRef.current.style.gridTemplateColumns = getColumnsStyle(
9✔
208
      customColumnSizes.length && customColumnSizes.length === relevantColumns.length ? customColumnSizes : relevantColumns,
23✔
209
      '1.5fr',
210
      selectable
211
    );
212
  }, [customColumnSizes, columnHeaders, selectable, resizeTrigger, size.width]);
213

214
  useEffect(() => {
13✔
215
    clearTimeout(initRef.current);
5✔
216
    initRef.current = setTimeout(() => setResizeTrigger(toggle), TIMEOUTS.debounceDefault);
5✔
217
    return () => {
5✔
218
      clearTimeout(initRef.current);
5✔
219
    };
220
  }, [customColumnSizes.length]);
221

222
  const onRowSelection = selectedRow => {
13✔
223
    let updatedSelection = [...selectedRowsRef.current];
1✔
224
    const selectedIndex = updatedSelection.indexOf(selectedRow);
1✔
225
    if (selectedIndex === -1) {
1!
226
      updatedSelection.push(selectedRow);
1✔
227
    } else {
228
      updatedSelection.splice(selectedIndex, 1);
×
229
    }
230
    onSelect(updatedSelection);
1✔
231
  };
232

233
  const onSelectAllClick = () => {
13✔
234
    let newSelectedRows = Array.apply(null, { length: devices.length }).map(Number.call, Number);
2✔
235
    if (selectedRows.length && selectedRows.length <= devices.length) {
2!
236
      newSelectedRows = [];
×
237
    }
238
    onSelect(newSelectedRows);
2✔
239
  };
240

241
  const handleResizeChange = useCallback(
13✔
242
    (e, { index, prev, ref }) => {
243
      const changedColumns = calculateResizeChange({ columnElements: [...ref.current.parentElement.children], columnHeaders, e, index, prev, selectable });
×
244
      // applying styles via state changes would lead to less smooth changes, so we set the style directly on the components
245
      deviceListRef.current.style.gridTemplateColumns = getColumnsStyle(changedColumns, undefined, selectable);
×
246
    },
247
    [columnHeaders, selectable]
248
  );
249

250
  const handleResizeFinish = useCallback(
13✔
251
    (e, { index, prev, ref }) => {
252
      const changedColumns = calculateResizeChange({ columnElements: ref.current.parentElement.children, columnHeaders, e, index, prev, selectable });
×
253
      onResizeColumns(changedColumns);
×
254
    },
255
    [columnHeaders, onResizeColumns, selectable]
256
  );
257

258
  const numSelected = (selectedRows || []).length;
13!
259
  return (
13✔
260
    <div className={`deviceList ${selectable ? 'selectable' : ''}`} ref={deviceListRef}>
13✔
261
      <div className={`header ${classes.header}`}>
262
        <div className="deviceListRow">
263
          {selectable && (
25✔
264
            <div>
265
              <Checkbox indeterminate={numSelected > 0 && numSelected < devices.length} checked={numSelected === devices.length} onChange={onSelectAllClick} />
19✔
266
            </div>
267
          )}
268
          {columnHeaders.map((item, index) => (
269
            <HeaderItem
54✔
270
              column={item}
271
              columnCount={columnHeaders.length}
272
              index={index}
273
              key={`columnHeader-${index}`}
274
              onSort={onSort}
275
              resizable={!!onResizeColumns}
276
              sortCol={sortCol}
277
              sortDown={sortDown}
278
              onResizeChange={handleResizeChange}
279
              onResizeFinish={handleResizeFinish}
280
            />
281
          ))}
282
        </div>
283
      </div>
284
      <div className="body">
285
        {devices.map((device, index) => (
286
          <DeviceListItem
20✔
287
            columnHeaders={columnHeaders}
288
            device={device}
289
            deviceListState={deviceListState}
290
            idAttribute={idAttribute.attribute}
291
            index={index}
292
            key={device.id}
293
            onClick={onExpandClick}
294
            onRowSelect={onRowSelection}
295
            selectable={selectable}
296
            selected={selectedRows.indexOf(index) !== -1}
297
          />
298
        ))}
299
      </div>
300
      <div className="footer flexbox margin-top">
301
        <Pagination
302
          className="margin-top-none"
303
          count={pageTotal}
304
          rowsPerPage={pageLength}
305
          onChangeRowsPerPage={onChangeRowsPerPage}
306
          page={pageNo}
307
          onChangePage={onPageChange}
308
          {...PaginationProps}
309
        />
310
        <Loader show={pageLoading} small />
311
      </div>
312
    </div>
313
  );
314
};
315

316
const areEqual = (prevProps, nextProps) => {
8✔
317
  if (
18✔
318
    prevProps.pageTotal != nextProps.pageTotal ||
89✔
319
    prevProps.pageLoading != nextProps.pageLoading ||
320
    prevProps.idAttribute != nextProps.idAttribute ||
321
    !deepCompare(prevProps.columnHeaders, nextProps.columnHeaders) ||
322
    !deepCompare(prevProps.customColumnSizes, nextProps.customColumnSizes) ||
323
    !deepCompare(prevProps.devices, nextProps.devices)
324
  ) {
325
    return false;
6✔
326
  }
327
  return deepCompare(prevProps.deviceListState, nextProps.deviceListState);
12✔
328
};
329

330
export default memo(DeviceList, areEqual);
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc