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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

77.84
/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;
9✔
31

32
const sortingNotes = {
9✔
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
  resizeHandle: {
41
    background: 'initial',
42
    ['&.hovering']: {
43
      background: theme.palette.grey[600]
44
    },
45
    ['&.resizing']: {
46
      background: isDarkMode(theme.palette.mode) ? theme.palette.grey[200] : theme.palette.grey[900]
23!
47
    }
48
  }
49
}));
50

51
const HeaderItem = ({ column, columnCount, index, sortCol, sortDown, onSort, onResizeChange, onResizeFinish, resizable }) => {
9✔
52
  const [isHovering, setIsHovering] = useState(false);
54✔
53
  const resizeRef = useRef();
54✔
54
  const ref = useRef();
54✔
55
  const { classes } = useStyles();
54✔
56

57
  const onMouseOut = () => setIsHovering(false);
54✔
58
  const onMouseOver = () => setIsHovering(true);
54✔
59

60
  const mouseMove = useCallback(
54✔
61
    e => {
62
      if (resizable && resizeRef.current) {
×
63
        onResizeChange(e, { column, index, prev: resizeRef.current, ref });
×
64
        resizeRef.current = e.clientX;
×
65
      }
66
    },
67
    [onResizeChange, resizable, resizeRef.current]
68
  );
69

70
  const removeListeners = useCallback(() => {
54✔
71
    window.removeEventListener('mousemove', mouseMove);
54✔
72
    window.removeEventListener('mouseup', removeListeners);
54✔
73
  }, [mouseMove]);
74

75
  const mouseDown = e => {
54✔
76
    resizeRef.current = e.clientX;
×
77
  };
78

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

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

100
  let resizeHandleClassName = resizable && isHovering ? 'hovering' : '';
54!
101
  resizeHandleClassName = resizeRef.current ? 'resizing' : resizeHandleClassName;
54!
102

103
  const header = (
104
    <div className="columnHeader flexbox space-between relative" style={column.style} onMouseEnter={onMouseOver} onMouseLeave={onMouseOut} ref={ref}>
54✔
105
      <div className="flexbox center-aligned" onClick={() => onSort(column.attribute ? column.attribute : {})}>
×
106
        {column.title}
107
        {column.sortable && (
104✔
108
          <SortIcon
109
            className={`sortIcon ${sortCol === column.attribute.name ? 'selected' : ''} ${(sortDown === SORTING_OPTIONS.desc).toString()}`}
50✔
110
            style={{ fontSize: 16 }}
111
          />
112
        )}
113
      </div>
114
      <div className="flexbox center-aligned">
115
        {column.customize && <SettingsIcon onClick={column.customize} style={{ fontSize: 16, marginRight: 4 }} />}
64✔
116
        {index > 0 && index < columnCount - 2 && <span onMouseDown={mouseDown} className={`resize-handle ${classes.resizeHandle} ${resizeHandleClassName}`} />}
118✔
117
      </div>
118
    </div>
119
  );
120
  return column.sortable && sortingNotes[column.attribute.name] ? (
54!
121
    <MenderTooltip title={sortingNotes[column.attribute.name]} placement="top-start">
122
      {header}
123
    </MenderTooltip>
124
  ) : (
125
    header
126
  );
127
};
128

129
const getRelevantColumns = (columnElements, selectable) => [...columnElements].slice(selectable ? 2 : 1, columnElements.length - 1);
11✔
130

131
export const calculateResizeChange = ({ columnElements, columnHeaders, e, index, prev, selectable }) => {
9✔
132
  const isShrinkage = prev > e.clientX ? -1 : 1;
3✔
133
  const columnDelta = Math.abs(e.clientX - prev) * isShrinkage;
3✔
134
  const relevantColumns = getRelevantColumns(columnElements, selectable);
3✔
135
  const canModifyNextColumn = index >= 1 && index + 1 < columnHeaders.length - 1;
3✔
136

137
  return relevantColumns.reduce((accu, element, columnIndex) => {
3✔
138
    const currentWidth = element.offsetWidth;
12✔
139
    let column = { attribute: columnHeaders[columnIndex + 1].attribute, size: currentWidth };
12✔
140
    if (canModifyNextColumn && index - 1 === columnIndex) {
12✔
141
      column.size = currentWidth + columnDelta;
2✔
142
    } else if (canModifyNextColumn && index === columnIndex) {
10✔
143
      column.size = currentWidth - columnDelta;
2✔
144
    }
145
    accu.push(column);
12✔
146
    return accu;
12✔
147
  }, []);
148
};
149

150
export const minCellWidth = 150;
9✔
151
const getTemplateColumns = (columns, selectable) =>
9✔
152
  selectable
8✔
153
    ? `52px minmax(250px, 1fr) ${columns} minmax(${minCellWidth}px, max-content)`
154
    : `minmax(250px, 1fr) ${columns} minmax(${minCellWidth}px, max-content)`;
155

156
const getColumnsStyle = (columns, defaultSize, selectable) => {
9✔
157
  const template = columns.map(({ size }) => `minmax(${minCellWidth}px, ${size ? `${size}px` : defaultSize})`);
16✔
158
  // applying styles via state changes would lead to less smooth changes, so we set the style directly on the components
159
  return getTemplateColumns(template.join(' '), selectable);
8✔
160
};
161

162
export const DeviceList = ({
9✔
163
  columnHeaders,
164
  customColumnSizes,
165
  devices,
166
  deviceListState,
167
  idAttribute,
168
  onChangeRowsPerPage,
169
  PaginationProps = {},
11✔
170
  onExpandClick,
171
  onResizeColumns,
172
  onPageChange,
173
  onSelect,
174
  onSort,
175
  pageLoading,
176
  pageTotal
177
}) => {
178
  const { page: pageNo = defaultPage, perPage: pageLength = defaultPerPage, selection: selectedRows = [], sort = {} } = deviceListState;
11!
179
  const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol } = sort;
11!
180
  const deviceListRef = useRef();
11✔
181
  const selectedRowsRef = useRef(selectedRows);
11✔
182
  const initRef = useRef();
11✔
183
  const [resizeTrigger, setResizeTrigger] = useState(false);
11✔
184

185
  const size = useWindowSize();
11✔
186
  const selectable = !!onSelect;
11✔
187
  const { classes } = useStyles();
11✔
188

189
  useEffect(() => {
11✔
190
    selectedRowsRef.current = selectedRows;
5✔
191
  }, [selectedRows]);
192

193
  useEffect(() => {
11✔
194
    if (!deviceListRef.current) {
8!
195
      return;
×
196
    }
197
    const relevantColumns = getRelevantColumns(deviceListRef.current.querySelector('.deviceListRow').children, selectable);
8✔
198
    deviceListRef.current.style.gridTemplateColumns = getColumnsStyle(customColumnSizes.length ? customColumnSizes : relevantColumns, '1.5fr', selectable);
8✔
199
  }, [deviceListRef.current, customColumnSizes, columnHeaders, selectable, resizeTrigger, size.width]);
200

201
  useEffect(() => {
11✔
202
    clearTimeout(initRef.current);
5✔
203
    initRef.current = setTimeout(() => setResizeTrigger(toggle), TIMEOUTS.debounceDefault);
5✔
204
    return () => {
5✔
205
      clearTimeout(initRef.current);
5✔
206
    };
207
  }, [customColumnSizes.length]);
208

209
  const onRowSelection = selectedRow => {
11✔
210
    let updatedSelection = [...selectedRowsRef.current];
1✔
211
    const selectedIndex = updatedSelection.indexOf(selectedRow);
1✔
212
    if (selectedIndex === -1) {
1!
213
      updatedSelection.push(selectedRow);
1✔
214
    } else {
215
      updatedSelection.splice(selectedIndex, 1);
×
216
    }
217
    onSelect(updatedSelection);
1✔
218
  };
219

220
  const onSelectAllClick = () => {
11✔
221
    let newSelectedRows = Array.apply(null, { length: devices.length }).map(Number.call, Number);
2✔
222
    if (selectedRows.length && selectedRows.length <= devices.length) {
2!
223
      newSelectedRows = [];
×
224
    }
225
    onSelect(newSelectedRows);
2✔
226
  };
227

228
  const handleResizeChange = (e, { index, prev, ref }) => {
11✔
229
    const changedColumns = calculateResizeChange({ columnElements: [...ref.current.parentElement.children], columnHeaders, e, index, prev, selectable });
×
230
    // applying styles via state changes would lead to less smooth changes, so we set the style directly on the components
231
    deviceListRef.current.style.gridTemplateColumns = getColumnsStyle(changedColumns, undefined, selectable);
×
232
  };
233

234
  const handleResizeFinish = (e, { index, prev, ref }) => {
11✔
235
    const changedColumns = calculateResizeChange({ columnElements: ref.current.parentElement.children, columnHeaders, e, index, prev, selectable });
×
236
    onResizeColumns(changedColumns);
×
237
  };
238

239
  const numSelected = (selectedRows || []).length;
11!
240
  return (
11✔
241
    <div className={`deviceList ${selectable ? 'selectable' : ''}`} ref={deviceListRef}>
11✔
242
      <div className={`header ${classes.header}`}>
243
        <div className="deviceListRow">
244
          {selectable && (
21✔
245
            <div>
246
              <Checkbox indeterminate={numSelected > 0 && numSelected < devices.length} checked={numSelected === devices.length} onChange={onSelectAllClick} />
17✔
247
            </div>
248
          )}
249
          {columnHeaders.map((item, index) => (
250
            <HeaderItem
54✔
251
              column={item}
252
              columnCount={columnHeaders.length}
253
              index={index}
254
              key={`columnHeader-${index}`}
255
              onSort={onSort}
256
              resizable={!!onResizeColumns}
257
              sortCol={sortCol}
258
              sortDown={sortDown}
259
              onResizeChange={handleResizeChange}
260
              onResizeFinish={handleResizeFinish}
261
            />
262
          ))}
263
        </div>
264
      </div>
265
      <div className="body">
266
        {devices.map((device, index) => (
267
          <DeviceListItem
16✔
268
            columnHeaders={columnHeaders}
269
            device={device}
270
            deviceListState={deviceListState}
271
            idAttribute={idAttribute.attribute}
272
            index={index}
273
            key={device.id}
274
            onClick={onExpandClick}
275
            onRowSelect={onRowSelection}
276
            selectable={selectable}
277
            selected={selectedRows.indexOf(index) !== -1}
278
          />
279
        ))}
280
      </div>
281
      <div className="footer flexbox margin-top">
282
        <Pagination
283
          className="margin-top-none"
284
          count={pageTotal}
285
          rowsPerPage={pageLength}
286
          onChangeRowsPerPage={onChangeRowsPerPage}
287
          page={pageNo}
288
          onChangePage={onPageChange}
289
          {...PaginationProps}
290
        />
291
        <Loader show={pageLoading} small />
292
      </div>
293
    </div>
294
  );
295
};
296

297
const areEqual = (prevProps, nextProps) => {
9✔
298
  if (
16✔
299
    prevProps.pageTotal != nextProps.pageTotal ||
83✔
300
    prevProps.pageLoading != nextProps.pageLoading ||
301
    prevProps.idAttribute != nextProps.idAttribute ||
302
    !deepCompare(prevProps.columnHeaders, nextProps.columnHeaders) ||
303
    !deepCompare(prevProps.customColumnSizes, nextProps.customColumnSizes) ||
304
    !deepCompare(prevProps.devices, nextProps.devices)
305
  ) {
306
    return false;
4✔
307
  }
308
  return deepCompare(prevProps.deviceListState, nextProps.deviceListState);
12✔
309
};
310

311
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