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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

77.66
/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, 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: theme.palette.mode === 'dark' ? 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);
29✔
53
  const resizeRef = useRef();
29✔
54
  const ref = useRef();
29✔
55
  const { classes } = useStyles();
29✔
56

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

60
  const mouseMove = useCallback(
29✔
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(() => {
29✔
71
    window.removeEventListener('mousemove', mouseMove);
29✔
72
    window.removeEventListener('mouseup', removeListeners);
29✔
73
  }, [mouseMove]);
74

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

79
  const mouseUp = useCallback(
29✔
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(() => {
29✔
91
    if (resizeRef.current) {
29!
92
      window.addEventListener('mousemove', mouseMove);
×
93
      window.addEventListener('mouseup', mouseUp);
×
94
    }
95
    return () => {
29✔
96
      removeListeners();
29✔
97
    };
98
  }, [resizeRef.current, mouseMove, mouseUp, removeListeners]);
99

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

103
  const header = (
104
    <div className="columnHeader flexbox space-between relative" style={column.style} onMouseEnter={onMouseOver} onMouseLeave={onMouseOut} ref={ref}>
29✔
105
      <div className="flexbox center-aligned" onClick={() => onSort(column.attribute ? column.attribute : {})}>
×
106
        {column.title}
107
        {column.sortable && (
54✔
108
          <SortIcon
109
            className={`sortIcon ${sortCol === column.attribute.name ? 'selected' : ''} ${(sortDown === SORTING_OPTIONS.desc).toString()}`}
25✔
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 }} />}
34✔
116
        {index > 0 && index < columnCount - 2 && <span onMouseDown={mouseDown} className={`resize-handle ${classes.resizeHandle} ${resizeHandleClassName}`} />}
63✔
117
      </div>
118
    </div>
119
  );
120
  return column.sortable && sortingNotes[column.attribute.name] ? (
29!
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);
9✔
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
6✔
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})`);
11✔
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);
6✔
160
};
161

162
export const DeviceList = props => {
9✔
163
  const {
164
    columnHeaders,
165
    customColumnSizes,
166
    devices,
167
    deviceListState,
168
    idAttribute,
169
    onChangeRowsPerPage,
170
    PaginationProps = {},
6✔
171
    onExpandClick,
172
    onResizeColumns,
173
    onPageChange,
174
    onSelect,
175
    onSort,
176
    pageLoading,
177
    pageTotal
178
  } = props;
6✔
179

180
  const { page: pageNo = defaultPage, perPage: pageLength = defaultPerPage, selection: selectedRows = [], sort = {} } = deviceListState;
6!
181

182
  const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol } = sort;
6✔
183
  const deviceListRef = useRef();
6✔
184
  const selectedRowsRef = useRef(selectedRows);
6✔
185
  const initRef = useRef();
6✔
186
  const [resizeTrigger, setResizeTrigger] = useState(false);
6✔
187

188
  const size = useWindowSize();
6✔
189
  const selectable = !!onSelect;
6✔
190
  const { classes } = useStyles();
6✔
191

192
  useEffect(() => {
6✔
193
    selectedRowsRef.current = selectedRows;
4✔
194
  }, [selectedRows]);
195

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

204
  useEffect(() => {
6✔
205
    clearTimeout(initRef.current);
4✔
206
    initRef.current = setTimeout(() => setResizeTrigger(toggle), TIMEOUTS.debounceDefault);
4✔
207
    return () => {
4✔
208
      clearTimeout(initRef.current);
4✔
209
    };
210
  }, [customColumnSizes.length]);
211

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

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

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

237
  const handleResizeFinish = (e, { index, prev, ref }) => {
6✔
238
    const changedColumns = calculateResizeChange({ columnElements: ref.current.parentElement.children, columnHeaders, e, index, prev, selectable });
×
239
    onResizeColumns(changedColumns);
×
240
  };
241

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

300
const areEqual = (prevProps, nextProps) => {
9✔
301
  if (
3!
302
    prevProps.pageTotal != nextProps.pageTotal ||
18✔
303
    prevProps.pageLoading != nextProps.pageLoading ||
304
    prevProps.idAttribute != nextProps.idAttribute ||
305
    !deepCompare(prevProps.columnHeaders, nextProps.columnHeaders) ||
306
    !deepCompare(prevProps.customColumnSizes, nextProps.customColumnSizes) ||
307
    !deepCompare(prevProps.devices, nextProps.devices)
308
  ) {
309
    return false;
×
310
  }
311
  return deepCompare(prevProps.deviceListState, nextProps.deviceListState);
3✔
312
};
313

314
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