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

mendersoftware / gui / 901187442

pending completion
901187442

Pull #3795

gitlab-ci

mzedel
feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

Ticket: None
Changelog: None
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3795: feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

4389 of 6365 branches covered (68.96%)

5 of 5 new or added lines in 1 file covered. (100.0%)

1729 existing lines in 165 files now uncovered.

8274 of 10019 relevant lines covered (82.58%)

144.86 hits per line

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

68.09
/src/js/components/dashboard/software-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, { useEffect, useMemo } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16

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

19
import {
20
  defaultReportType,
21
  defaultReports,
22
  deriveReportsData,
23
  getDeviceAttributes,
24
  getGroupDevices,
25
  getReportingLimits,
26
  getReportsData
27
} from '../../actions/deviceActions';
28
import { saveUserSettings } from '../../actions/userActions';
29
import { DEVICE_STATES, UNGROUPED_GROUP } from '../../constants/deviceConstants';
30
import { rootfsImageVersion, softwareTitleMap } from '../../constants/releaseConstants';
31
import { isEmpty } from '../../helpers';
32
import { getAttributesList, getFeatures, getIsEnterprise, getUserSettings } from '../../selectors';
33
import EnterpriseNotification from '../common/enterpriseNotification';
34
import { extractSoftwareInformation } from '../devices/device-details/installedsoftware';
35
import ChartAdditionWidget from './widgets/chart-addition';
36
import DistributionReport from './widgets/distribution';
37

38
const reportTypes = {
5✔
39
  distribution: DistributionReport
40
};
41

42
const getLayerKey = ({ title, key }, parent) => `${parent.length ? `${parent}.` : parent}${key.length <= title.length ? key : title}`;
18!
43

44
const generateLayer = (softwareLayer, parentKey = '', nestingLevel = 0) => {
5✔
45
  const { children, key, title } = softwareLayer;
18✔
46
  const suffix = title === key ? '.version' : '';
18!
47
  const layerKey = getLayerKey(softwareLayer, parentKey);
18✔
48
  const layerTitle = `${layerKey}${suffix}`;
18✔
49
  let headerItems = [{ title, nestingLevel, value: layerKey }];
18✔
50
  if (softwareTitleMap[layerTitle]) {
18!
51
    headerItems = [
18✔
52
      { subheader: title, nestingLevel, value: `${layerTitle}-subheader` },
53
      { title: softwareTitleMap[layerTitle].title, nestingLevel: nestingLevel + 1, value: layerTitle }
54
    ];
UNCOV
55
  } else if (!isEmpty(children)) {
×
UNCOV
56
    headerItems = [{ subheader: title, nestingLevel, value: `${layerTitle}-subheader` }];
×
57
  }
58
  return Object.values(softwareLayer.children).reduce((accu, childLayer) => {
18✔
UNCOV
59
    const layerData = generateLayer(childLayer, getLayerKey(softwareLayer, parentKey), nestingLevel + 1);
×
UNCOV
60
    accu.push(...layerData);
×
UNCOV
61
    return accu;
×
62
  }, headerItems);
63
};
64

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

79
  return (rootFs ? [rootFs, ...remainder] : remainder).flatMap(softwareLayer => generateLayer(softwareLayer));
18!
80
};
81

82
export const SoftwareDistribution = () => {
5✔
83
  const dispatch = useDispatch();
412✔
84

85
  const reports = useSelector(
412✔
86
    state =>
87
      getUserSettings(state).reports ||
842✔
88
      state.users.globalSettings[`${state.users.currentUser}-reports`] ||
89
      (Object.keys(state.devices.byId).length ? defaultReports : [])
842✔
90
  );
91
  // eslint-disable-next-line no-unused-vars
92
  const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = useSelector(state => state.devices.groups.byId);
842✔
93
  const { hasReporting } = useSelector(getFeatures);
412✔
94
  const attributes = useSelector(getAttributesList);
412✔
95
  const hasDevices = useSelector(state => state.devices.byStatus[DEVICE_STATES.accepted].total);
842✔
96
  const isEnterprise = useSelector(getIsEnterprise);
412✔
97
  const reportsData = useSelector(state => state.devices.reports);
842✔
98

99
  useEffect(() => {
412✔
100
    dispatch(getDeviceAttributes());
11✔
101
    if (hasReporting) {
11!
UNCOV
102
      dispatch(getReportingLimits());
×
103
    }
104
  }, []);
105

106
  useEffect(() => {
412✔
107
    if (hasReporting) {
14!
UNCOV
108
      dispatch(getReportsData());
×
UNCOV
109
      return;
×
110
    }
111
    dispatch(deriveReportsData());
14✔
112
  }, [JSON.stringify(reports)]);
113

114
  const addCurrentSelection = selection => {
412✔
UNCOV
115
    const newReports = [...reports, { ...defaultReports[0], ...selection }];
×
UNCOV
116
    dispatch(saveUserSettings({ reports: newReports }));
×
117
  };
118

119
  const onSaveChangedReport = (change, index) => {
412✔
UNCOV
120
    let newReports = [...reports];
×
UNCOV
121
    newReports.splice(index, 1, change);
×
UNCOV
122
    dispatch(saveUserSettings({ reports: newReports }));
×
123
  };
124

125
  const removeReport = removedReport => dispatch(saveUserSettings({ reports: reports.filter(report => report !== removedReport) }));
412✔
126

127
  const software = useMemo(() => listSoftware(hasReporting ? attributes : [rootfsImageVersion]), [JSON.stringify(attributes), hasReporting]);
412!
128

129
  if (!isEnterprise) {
412✔
130
    return (
411✔
131
      <div className="flexbox centered">
132
        <EnterpriseNotification isEnterprise={isEnterprise} benefit="actionable insights into the devices you are updating with Mender" />
133
      </div>
134
    );
135
  }
136
  return hasDevices ? (
1!
137
    <div className="dashboard margin-bottom-large">
138
      {reports.map((report, index) => {
139
        const Component = reportTypes[report.type || defaultReportType];
1!
140
        return (
1✔
141
          <Component
142
            key={`report-${report.group}-${index}`}
143
            data={reportsData[index]}
144
            getGroupDevices={(...args) => dispatch(getGroupDevices(...args))}
1✔
145
            groups={groups}
UNCOV
146
            onClick={() => removeReport(report)}
×
UNCOV
147
            onSave={change => onSaveChangedReport(change, index)}
×
148
            selection={report}
149
            software={software}
150
          />
151
        );
152
      })}
153
      <ChartAdditionWidget groups={groups} onAdditionClick={addCurrentSelection} software={software} />
154
    </div>
155
  ) : (
156
    <div className="dashboard-placeholder margin-top-large">
157
      <BarChartIcon style={{ transform: 'scale(5)' }} />
158
      <p className="margin-top-large">Software distribution charts will appear here once you connected a device. </p>
159
    </div>
160
  );
161
};
162

163
export default SoftwareDistribution;
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