• 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

70.86
/src/js/components/dashboard/widgets/distribution.js
1
// Copyright 2020 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, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
import { useNavigate } from 'react-router-dom';
16

17
import { Clear as ClearIcon, Settings, Square } from '@mui/icons-material';
18
import { IconButton, LinearProgress, linearProgressClasses, svgIconClasses } from '@mui/material';
19
import { makeStyles } from 'tss-react/mui';
20

21
import { VictoryBar, VictoryContainer, VictoryPie, VictoryStack } from 'victory';
22

23
import { ensureVersionString } from '../../../actions/deviceActions';
24
import { TIMEOUTS, chartTypes } from '../../../constants/appConstants';
25
import { ALL_DEVICES } from '../../../constants/deviceConstants';
26
import { rootfsImageVersion, softwareTitleMap } from '../../../constants/releaseConstants';
27
import { isEmpty, toggle } from '../../../helpers';
28
import { chartColorPalette } from '../../../themes/Mender';
29
import Loader from '../../common/loader';
30
import { ChartEditWidget, RemovalWidget } from './chart-addition';
31

32
const seriesOther = '__OTHER__';
7✔
33

34
const createColorClassName = hexColor => `color-${hexColor.slice(1)}`;
79✔
35

36
const useStyles = makeStyles()(theme => ({
7✔
37
  header: { minHeight: 30, [`.${svgIconClasses.root}`]: { marginLeft: theme.spacing() } },
38
  indicator: { fontSize: 10, minWidth: 'initial', marginLeft: 4 },
39
  legendItem: {
40
    alignItems: 'center',
41
    display: 'grid',
42
    gridTemplateColumns: '1fr max-content',
43
    columnGap: theme.spacing(2),
44
    '&.indicating': {
45
      gridTemplateColumns: 'min-content 1fr max-content',
46
      columnGap: theme.spacing()
47
    }
48
  },
49
  wrapper: {
50
    display: 'grid',
51
    gridTemplateColumns: '200px 1fr',
52
    columnGap: theme.spacing(2),
53
    marginBottom: theme.spacing(2),
54
    '&>.flexbox.column > *': {
55
      height: 20
56
    },
57
    '.barchart': {
58
      [`.${linearProgressClasses.root}`]: {
59
        backgroundColor: theme.palette.grey[400]
60
      },
61
      ...Object.values(chartColorPalette).reduce(
62
        (accu, color) => ({
30✔
63
          ...accu,
64
          [`.${createColorClassName(color)} .${linearProgressClasses.barColorPrimary}`]: { backgroundColor: color }
65
        }),
66
        {
67
          [`.${createColorClassName(theme.palette.grey[400])} .${linearProgressClasses.barColorPrimary}`]: { backgroundColor: theme.palette.grey[400] }
68
        }
69
      )
70
    }
71
  }
72
}));
73

74
const ChartLegend = ({ classes, data, events = [], showIndicators = true }) => {
7!
75
  const { eventHandlers = {} } = events[0];
14!
76
  const { onClick } = eventHandlers;
14✔
77
  return (
14✔
78
    <div className="flexbox column">
79
      {data.map(({ fill, x, title, tip, value }) => (
80
        <div
41✔
81
          className={`clickable ${classes.legendItem} ${showIndicators ? 'indicating' : ''}`}
41!
82
          key={x}
83
          onClick={e => onClick(e, { datum: { x } })}
×
84
          title={tip}
85
        >
86
          {showIndicators && <Square className={classes.indicator} style={{ fill }} />}
41!
87
          <div className="text-overflow">{title}</div>
88
          <div>{value.toLocaleString()}</div>
89
        </div>
90
      ))}
91
    </div>
92
  );
93
};
94

95
const VictoryBarChart = ({ data, totals, ...remainder }) => (
7✔
96
  <VictoryStack {...remainder} animate={{ duration: 700, onLoad: { duration: 700 } }} horizontal padding={{ left: 0, right: 0, top: 0, bottom: 15 }}>
×
97
    <VictoryBar alignment="start" barWidth={16} sortKey={['y']} sortOrder="ascending" data={data} />
98
    <VictoryBar alignment="start" barWidth={16} sortKey={['y']} sortOrder="descending" data={totals} />
99
  </VictoryStack>
100
);
101

102
const BarChart = ({ data, events }) => {
7✔
103
  const [chartData, setChartData] = useState([]);
16✔
104
  const timer = useRef();
16✔
105

106
  useEffect(() => {
16✔
107
    setChartData(data.map(item => ({ ...item, y: 0 })));
5✔
108
    clearTimeout(timer.current);
2✔
109
    timer.current = setTimeout(() => setChartData(data), TIMEOUTS.debounceDefault);
2✔
110
    return () => {
2✔
111
      clearTimeout(timer.current);
2✔
112
    };
113
  }, [JSON.stringify(data)]);
114

115
  const { eventHandlers = {} } = events[0];
16!
116
  const { onClick } = eventHandlers;
16✔
117
  return (
16✔
118
    <div className="flexbox column">
119
      {chartData.map(({ fill, x, y, tip }) => (
120
        <div className="clickable flexbox column barchart" key={x} onClick={e => onClick(e, { datum: { x } })} title={tip} style={{ justifyContent: 'center' }}>
44✔
121
          <LinearProgress className={createColorClassName(fill)} variant="determinate" value={y} style={{ height: 8 }} />
122
        </div>
123
      ))}
124
    </div>
125
  );
126
};
127

128
const ChartContainer = ({ className, children, innerRef, style = {} }) => (
7✔
129
  <div className={className} ref={innerRef} style={style}>
14✔
130
    {children}
131
  </div>
132
);
133

134
const BarChartContainer = ({ classes = {}, data, events, ...remainder }) => (
7!
135
  <ChartContainer className={classes.wrapper}>
14✔
136
    <ChartLegend classes={classes} data={data} events={events} showIndicators={false} />
137
    <BarChart {...remainder} data={data} events={events} />
138
  </ChartContainer>
139
);
140

141
const PieChart = props => <VictoryPie {...props} padding={{ left: 0, right: 0, top: 0, bottom: 15 }} />;
7✔
142

143
const padding = 10;
7✔
144
const PieChartContainer = ({ classes = {}, ...chartProps }) => {
7!
145
  const ref = useRef();
×
146
  let height;
147
  if (ref.current) {
×
148
    // use the widget height, remove the space the header takes up and account for widget padding (top + padding) + own padding for the chart
149
    height = ref.current.parentElement.offsetHeight - ref.current.parentElement.children[0].offsetHeight - 3 * padding;
×
150
  }
151
  return (
×
152
    <ChartContainer className={classes.wrapper} innerRef={ref} style={{ height }}>
153
      <ChartLegend {...chartProps} classes={classes} />
154
      {height && <PieChart {...chartProps} containerComponent={<VictoryContainer style={{ height }} />} />}
×
155
    </ChartContainer>
156
  );
157
};
158

159
const VictoryBarChartContainer = ({ classes = {}, ...chartProps }) => (
7!
160
  <ChartContainer className={classes.wrapper}>
×
161
    <ChartLegend {...chartProps} classes={classes} />
162
    <VictoryBarChart {...chartProps} />
163
  </ChartContainer>
164
);
165

166
const chartTypeComponentMap = {
7✔
167
  [chartTypes.bar.key]: BarChartContainer,
168
  [`${chartTypes.bar.key}-alternative`]: VictoryBarChartContainer,
169
  [chartTypes.pie.key]: PieChartContainer
170
};
171

172
const initDistribution = ({ data, theme }) => {
7✔
173
  const { items, otherCount, total } = data;
2✔
174
  const numberOfItems = items.length > chartColorPalette.length ? chartColorPalette.length - 1 : items.length;
2!
175
  const colors = chartColorPalette.slice(0, numberOfItems).reverse();
2✔
176
  let distribution = items.slice(0, colors.length).reduce(
2✔
177
    (accu, { key, count }, index) => [
4✔
178
      {
179
        x: key || '-',
4!
180
        y: (count / total) * 100, //value,
181
        title: key || '-',
4!
182
        tip: key || '-',
4!
183
        fill: chartColorPalette[index],
184
        value: count
185
      },
186
      ...accu
187
    ],
188
    []
189
  );
190
  if (items.length > chartColorPalette.length || otherCount) {
2✔
191
    distribution.splice(0, 0, {
1✔
192
      x: seriesOther,
193
      title: 'Other',
194
      tip: 'Other',
195
      y: (otherCount / total) * 100,
196
      fill: chartColorPalette[chartColorPalette.length - 1],
197
      value: otherCount
198
    });
199
  }
200
  distribution.sort((pairA, pairB) => pairB.y - pairA.y);
4✔
201
  // y: formatValue(item.y, total)
202
  const totals = distribution.map(({ x, y }) => ({ value: total, x, y: 100 - y, fill: theme.palette.grey[400] }));
5✔
203
  return { distribution, totals };
2✔
204
};
205

206
export const Header = ({ chartType }) => {
7✔
207
  const { classes } = useStyles();
18✔
208
  const { Icon } = chartTypes[chartType];
18✔
209
  return (
18✔
210
    <div className={`flexbox center-aligned ${classes.header}`}>
211
      Software distribution
212
      <Icon />
213
    </div>
214
  );
215
};
216

217
export const DistributionReport = ({ data, getGroupDevices, groups, onClick, onSave, selection = {}, software: softwareTree }) => {
7✔
218
  const {
219
    attribute: attributeSelection,
220
    group: groupSelection = '',
2✔
221
    chartType: chartTypeSelection = chartTypes.bar.key,
2✔
222
    software: softwareSelection = rootfsImageVersion
2✔
223
  } = selection;
16✔
224
  const [editing, setEditing] = useState(false);
16✔
225
  const [removing, setRemoving] = useState(false);
16✔
226
  const [chartType, setChartType] = useState(chartTypes.bar.key);
16✔
227
  const [software, setSoftware] = useState('');
16✔
228
  const [group, setGroup] = useState('');
16✔
229
  const navigate = useNavigate();
16✔
230
  const { classes, theme } = useStyles();
16✔
231

232
  useEffect(() => {
16✔
233
    setSoftware(softwareSelection || attributeSelection);
2✔
234
    setGroup(groupSelection);
2✔
235
    setChartType(chartTypeSelection);
2✔
236
    setRemoving(false);
2✔
237
    getGroupDevices(groupSelection, { page: 1, perPage: 1 });
2✔
238
  }, [attributeSelection, groupSelection, chartTypeSelection, softwareSelection]);
239

240
  const { distribution, totals } = useMemo(() => {
16✔
241
    if (isEmpty(data)) {
3✔
242
      return { distribution: [], totals: [] };
1✔
243
    }
244
    return initDistribution({ data, theme });
2✔
245
  }, [JSON.stringify(data), JSON.stringify(selection)]);
246

247
  const onSliceClick = useCallback(
16✔
248
    (e, { datum: { x: target } }) => {
249
      if (target === seriesOther) {
×
250
        return;
×
251
      }
252
      navigate(`/devices/accepted?inventory=${group ? `group:eq:${group}&` : ''}${ensureVersionString(software, attributeSelection)}:eq:${target}`);
×
253
    },
254
    [attributeSelection, group, software]
255
  );
256

257
  const toggleRemoving = () => setRemoving(toggle);
16✔
258

259
  const onToggleEditClick = () => setEditing(toggle);
16✔
260

261
  const onSaveClick = selection => {
16✔
262
    setChartType(selection.chartType);
×
263
    setSoftware(selection.software);
×
264
    setGroup(selection.group);
×
265
    onSave(selection);
×
266
    setEditing(false);
×
267
  };
268

269
  const Chart = chartTypeComponentMap[chartType];
16✔
270
  const chartProps = {
16✔
271
    classes,
272
    data: distribution,
273
    domainPadding: 0,
274
    events: [{ target: 'data', eventHandlers: { onClick: onSliceClick } }],
275
    standalone: true,
276
    style: { data: { fill: ({ datum }) => datum.fill } },
×
277
    labels: () => null
×
278
  };
279
  const couldHaveDevices = !group || groups[group]?.deviceIds.length;
16!
280
  if (removing) {
16!
281
    return <RemovalWidget onCancel={toggleRemoving} onClick={onClick} />;
×
282
  }
283
  if (editing) {
16!
284
    return (
×
285
      <ChartEditWidget
286
        groups={groups}
287
        onSave={onSaveClick}
288
        onCancel={onToggleEditClick}
289
        selection={{ ...selection, chartType, group, software }}
290
        software={softwareTree}
291
      />
292
    );
293
  }
294
  return (
16✔
295
    <div className="widget chart-widget">
296
      <div className="margin-bottom-small">
297
        <div className="flexbox space-between margin-bottom-small">
298
          <Header chartType={chartType} />
299
          <div className="flexbox center-aligned" style={{ zIndex: 1 }}>
300
            <IconButton onClick={onToggleEditClick} size="small">
301
              <Settings fontSize="small" />
302
            </IconButton>
303
            <IconButton onClick={toggleRemoving} size="small">
304
              <ClearIcon fontSize="small" />
305
            </IconButton>
306
          </div>
307
        </div>
308
        <div className="flexbox space-between slightly-smaller">
309
          <div>{softwareTitleMap[software] ? softwareTitleMap[software].title : software}</div>
16✔
310
          <div>{group || ALL_DEVICES}</div>
32✔
311
        </div>
312
      </div>
313
      {distribution.length || totals.length ? (
34✔
314
        <Chart {...chartProps} totals={totals} />
315
      ) : couldHaveDevices ? (
2!
316
        <div className="muted flexbox centered">There are no devices that match the selected criteria.</div>
317
      ) : (
318
        <Loader show={true} />
319
      )}
320
    </div>
321
  );
322
};
323

324
export default DistributionReport;
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