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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

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

2✔
17
import { BarChart as BarChartIcon } from '@mui/icons-material';
2✔
18
import { Typography } from '@mui/material';
2✔
19

2✔
20
import Loader from '@northern.tech/common-ui/Loader';
2✔
21
import { SupportLink } from '@northern.tech/common-ui/SupportLink';
2✔
22
import { MAX_PAGE_SIZE, TIMEOUTS, defaultReports, rootfsImageVersion, softwareIndicator, softwareTitleMap } from '@northern.tech/store/constants';
2✔
23
import {
2✔
24
  getAcceptedDevices,
2✔
25
  getAttributesList,
2✔
26
  getDeviceReports,
2✔
27
  getDeviceReportsForUser,
2✔
28
  getGroupsByIdWithoutUngrouped,
2✔
29
  getIsEnterprise,
2✔
30
  getUserSettingsInitialized
2✔
31
} from '@northern.tech/store/selectors';
2✔
32
import { getDeviceAttributes, getReportDataWithoutBackendSupport, saveUserSettings } from '@northern.tech/store/thunks';
2✔
33
import { isEmpty } from '@northern.tech/utils/helpers';
2✔
34

2✔
35
import { extractSoftwareInformation } from '../devices/device-details/InstalledSoftware';
2✔
36
import BaseWidget from './widgets/BaseWidget';
2✔
37
import ChartAdditionWidget from './widgets/ChartAddition';
2✔
38
import { DistributionReport } from './widgets/Distribution';
2✔
39

2✔
40
const getLayerKey = ({ title, key }, parent) => `${parent.length ? `${parent}.` : parent}${key.length <= title.length ? key : title}`;
25!
41

2✔
42
const generateLayer = (softwareLayer, parentKey = '', nestingLevel = 0) => {
7✔
43
  const { children, key, title } = softwareLayer;
25✔
44
  const suffix = title === key ? softwareIndicator : '';
25!
45
  const layerKey = getLayerKey(softwareLayer, parentKey);
25✔
46
  const layerTitle = `${layerKey}${suffix}`;
25✔
47
  let headerItems = [{ title, nestingLevel, value: layerTitle }]; // this should be the case if there are no children == lowest level information, so we give the full version string
25✔
48
  if (softwareTitleMap[layerTitle]) {
25!
49
    headerItems = [
25✔
50
      { subheader: title, nestingLevel, value: `${layerTitle}-subheader` },
2✔
51
      { title: softwareTitleMap[layerTitle].title, nestingLevel: nestingLevel + 1, value: layerTitle }
2✔
52
    ];
2✔
UNCOV
53
  } else if (!isEmpty(children)) {
2!
54
    headerItems = [{ subheader: title, nestingLevel, value: `${layerTitle}-subheader` }];
2✔
55
  }
2✔
56
  return Object.values(softwareLayer.children).reduce((accu, childLayer) => {
25✔
57
    const layerData = generateLayer(childLayer, getLayerKey(softwareLayer, parentKey), nestingLevel + 1);
2✔
58
    accu.push(...layerData);
2✔
59
    return accu;
2✔
60
  }, headerItems);
2✔
61
};
2✔
62

2✔
63
const listSoftware = attributes => {
7✔
64
  const enhancedAttributes = attributes.reduce((accu, attribute) => ({ ...accu, [attribute]: attribute }), {});
202✔
65
  const softwareTree = extractSoftwareInformation(enhancedAttributes, false);
25✔
66
  const { rootFs, remainder } = Object.values(softwareTree).reduce(
25✔
67
    (accu, layer) => {
2✔
68
      if (layer.key.startsWith('rootfs-image')) {
25!
69
        return { ...accu, rootFs: layer };
25✔
70
      }
2✔
71
      accu.remainder.push(layer);
2✔
72
      return accu;
2✔
73
    },
2✔
74
    { rootFs: undefined, remainder: [] }
2✔
75
  );
2✔
76

2✔
77
  return (rootFs ? [rootFs, ...remainder] : remainder).flatMap(softwareLayer => generateLayer(softwareLayer));
25!
78
};
2✔
79

2✔
80
const DeviceDataLimitWarning = () => (
7✔
81
  <div className="dashboard margin-bottom-large">
2✔
82
    <Typography variant="subtitle2">Device and Group Limit Exceeded</Typography>
2✔
83
    <Typography variant="caption">
2✔
84
      Your current number of devices and groups exceeds the limits of our present implementation. To ensure you continue to gain optimal insights and to better
2✔
85
      understand your specific requirements, we encourage you to reach out to <SupportLink variant="ourTeam" />. By providing us with more details about your
2✔
86
      use case, we can improve potential solutions to best accommodate your needs when the feature gets added to our backend.
2✔
87
    </Typography>
2✔
88
  </div>
2✔
89
);
2✔
90

2✔
91
const checkRequestLimitReached = (reports, deviceRetrievalLimit, total) => {
7✔
92
  const requestLimit = deviceRetrievalLimit / MAX_PAGE_SIZE;
56✔
93
  const { hasTooManyDevices } = reports.reduce(
56✔
94
    (accu, report) => {
2✔
95
      let { hasTooManyDevices, requestCounter } = accu;
48✔
96
      // as the attribute per report can be different for a given group or for all devices, count them both
2✔
97
      // + we assume smaller (sub 500 device) groups for now - the staggered widget rendering should allow some flexibility with the rate limits
2✔
98
      requestCounter += report.group ? 1 : Math.ceil(total / MAX_PAGE_SIZE);
48!
99
      hasTooManyDevices = accu.hasTooManyDevices || accu.requestCounter > requestLimit;
48✔
100
      return { hasTooManyDevices, requestCounter };
48✔
101
    },
2✔
102
    { hasTooManyDevices: false, requestCounter: 0 }
2✔
103
  );
2✔
104
  return hasTooManyDevices;
56✔
105
};
2✔
106

2✔
107
export const SoftwareDistribution = () => {
7✔
108
  const reports = useSelector(getDeviceReportsForUser);
56✔
109
  const groups = useSelector(getGroupsByIdWithoutUngrouped);
56✔
110
  const attributes = useSelector(getAttributesList);
56✔
111
  const { total } = useSelector(getAcceptedDevices);
56✔
112
  const hasDevices = !!total;
56✔
113
  const isEnterprise = useSelector(getIsEnterprise);
56✔
114
  const hasUserSettingsInitialized = useSelector(getUserSettingsInitialized);
56✔
115
  const deviceRetrievalLimit = useSelector(state => state.deployments.deploymentDeviceLimit);
564✔
116
  const reportsData = useSelector(getDeviceReports);
56✔
117
  const hasReportsData = reportsData.reduce((accu, report) => accu && !isEmpty(report), true);
56✔
118
  const [visibleCount, setVisibleCount] = useState(hasReportsData ? reportsData.length : 1);
56!
119
  const dispatch = useDispatch();
56✔
120
  const hasTooManyDevices = checkRequestLimitReached(reports, deviceRetrievalLimit, total);
56✔
121

2✔
122
  useEffect(() => {
56✔
123
    dispatch(getDeviceAttributes());
16✔
124
  }, [dispatch]);
2✔
125

2✔
126
  useEffect(() => {
56✔
127
    if (visibleCount < reports.length) {
21✔
128
      // this is purely to stagger the device retrieval and reduce overlap between the repeated queries to the backend
2✔
129
      const timeout = setTimeout(() => setVisibleCount(visibleCount + 1), TIMEOUTS.oneSecond);
13✔
130
      return () => clearTimeout(timeout);
13✔
131
    }
2✔
132
  }, [reports.length, visibleCount]);
2✔
133

2✔
134
  const addCurrentSelection = selection => {
56✔
135
    const newReports = [...reports, { ...defaultReports[0], ...selection }];
2✔
136
    dispatch(saveUserSettings({ reports: newReports }));
2✔
137
  };
2✔
138

2✔
139
  const onSaveChangedReport = (change, index) => {
56✔
140
    const newReports = [...reports];
2✔
141
    newReports.splice(index, 1, change);
2✔
142
    dispatch(saveUserSettings({ reports: newReports }));
2✔
143
    dispatch(getReportDataWithoutBackendSupport(index));
2✔
144
  };
2✔
145

2✔
146
  const removeReport = removedReport => dispatch(saveUserSettings({ reports: reports.filter(report => report !== removedReport) }));
56✔
147

2✔
148
  // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
149
  const software = useMemo(() => listSoftware([rootfsImageVersion, ...attributes]), [JSON.stringify(attributes)]);
56✔
150

2✔
151
  if (!isEnterprise) {
56✔
152
    return (
46✔
153
      <div className="dashboard margin-bottom-large">
2✔
154
        <ChartAdditionWidget groups={groups} onAdditionClick={addCurrentSelection} software={software} />
2✔
155
      </div>
2✔
156
    );
2✔
157
  }
2✔
158
  if (hasTooManyDevices) {
12!
159
    return <DeviceDataLimitWarning />;
2✔
160
  }
2✔
161
  if (!hasUserSettingsInitialized) {
12✔
162
    return (
3✔
163
      <div className="dashboard margin-bottom-large">
2✔
164
        <BaseWidget className="chart-widget flexbox centered" main={<Loader show style={{ width: '100%' }} />} />
2✔
165
      </div>
2✔
166
    );
2✔
167
  }
2✔
168
  return hasDevices ? (
11!
169
    <div className="dashboard margin-bottom-large">
2✔
170
      {reports.slice(0, visibleCount).map((report, index) => (
2✔
171
        <DistributionReport
9✔
172
          key={`report-${report.group}-${index}`}
2✔
UNCOV
173
          onClick={() => removeReport(report)}
2✔
UNCOV
174
          onSave={change => onSaveChangedReport(change, index)}
2✔
175
          selection={{ ...report, index }}
2✔
176
          software={software}
2✔
177
        />
2✔
178
      ))}
2✔
179
      {visibleCount < reports.length && (
2✔
180
        <div className="widget chart-widget flexbox centered">
2✔
181
          <Loader show style={{ width: '100%' }} />
2✔
182
        </div>
2✔
183
      )}
2✔
184
      <ChartAdditionWidget groups={groups} onAdditionClick={addCurrentSelection} software={software} />
2✔
185
    </div>
2✔
186
  ) : (
2✔
187
    <div className="dashboard-placeholder margin-top-large">
2✔
188
      <BarChartIcon style={{ transform: 'scale(5)' }} />
2✔
189
      <p className="margin-top-large">Software distribution charts will appear here once you connected a device. </p>
2✔
190
    </div>
2✔
191
  );
2✔
192
};
2✔
193

2✔
194
export default SoftwareDistribution;
2✔
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