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

mendersoftware / gui / 891984097

pending completion
891984097

Pull #3741

gitlab-ci

mzedel
chore: made use of common truth function across codebase to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3741: MEN-6487 - fix: added more granular check for device troubleshooting feature

4401 of 6401 branches covered (68.75%)

26 of 27 new or added lines in 8 files covered. (96.3%)

1702 existing lines in 165 files now uncovered.

8060 of 9779 relevant lines covered (82.42%)

123.44 hits per line

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

71.03
/src/js/components/releases/artifactdetails.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, { useCallback, useEffect, useMemo, useState } from 'react';
15
import { connect } from 'react-redux';
16

17
// material ui
18
import {
19
  Add as AddIcon,
20
  Cancel as CancelIcon,
21
  CancelOutlined as CancelOutlinedIcon,
22
  CheckCircleOutline as CheckCircleOutlineIcon,
23
  Check as CheckIcon,
24
  Edit as EditIcon,
25
  ExitToApp as ExitToAppIcon,
26
  Launch as LaunchIcon,
27
  Remove as RemoveIcon
28
} from '@mui/icons-material';
29
import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Input, InputAdornment, List, ListItem, ListItemText } from '@mui/material';
30
import { makeStyles } from 'tss-react/mui';
31

32
import pluralize from 'pluralize';
33

34
import { getArtifactInstallCount, getArtifactUrl } from '../../actions/releaseActions';
35
import { extractSoftware, extractSoftwareItem, toggle } from '../../helpers';
36
import { getUserCapabilities } from '../../selectors';
37
import ExpandableAttribute from '../common/expandable-attribute';
38
import ArtifactPayload from './artifactPayload';
39
import ArtifactMetadataList from './artifactmetadatalist';
40

41
const useStyles = makeStyles()(theme => ({
7✔
42
  editButton: {
43
    color: 'rgba(0, 0, 0, 0.54)',
44
    marginBottom: 10
45
  },
46
  link: { marginTop: theme.spacing() },
47
  listItemStyle: {
48
    bordered: {
49
      borderBottom: '1px solid',
50
      borderBottomColor: theme.palette.grey[500]
51
    },
52
    color: theme.palette.text.primary,
53
    fontSize: 13,
54
    marginRight: '2vw',
55
    minWidth: 200,
56
    padding: 0,
57
    width: 'initial'
58
  },
59
  paddingOverride: { paddingBottom: 4, paddingTop: 0 },
60
  accordPanel1: {
61
    background: theme.palette.grey[500],
62
    borderTop: 'none',
63
    padding: '0 15px',
64
    marginBottom: 30,
65
    [`&.Mui-expanded`]: {
66
      background: theme.palette.grey[500],
67
      marginBottom: 30
68
    }
69
  },
70
  accordSummary: {
71
    background: theme.palette.grey[500],
72
    padding: 0
73
  }
74
}));
75

76
export const transformArtifactCapabilities = (capabilities = {}) => {
7✔
77
  return Object.entries(capabilities).reduce((accu, [key, value]) => {
42✔
78
    if (!Array.isArray(value)) {
49✔
79
      accu.push({ key, primary: key, secondary: value });
25✔
80
    } else if (!key.startsWith('device_type')) {
24✔
81
      // we can expect this to be an array of artifacts or artifact groups this artifact depends on
82
      const dependencies = value.reduce((dependencies, dependency, index) => {
12✔
83
        const dependencyKey = value.length > 1 ? `${key}-${index + 1}` : key;
24!
84
        dependencies.push({ key: dependencyKey, primary: dependencyKey, secondary: dependency });
24✔
85
        return dependencies;
24✔
86
      }, []);
87
      accu.push(...dependencies);
12✔
88
    }
89
    return accu;
49✔
90
  }, []);
91
};
92

93
export const transformArtifactMetadata = (metadata = {}) => {
7✔
94
  return Object.entries(metadata).reduce((accu, [key, value]) => {
14✔
95
    const commonProps = { key, primary: key, secondaryTypographyProps: { component: 'div' } };
4✔
96
    if (Array.isArray(value)) {
4✔
97
      accu.push({ ...commonProps, secondary: value.length ? value.join(',') : '-' });
1!
98
    } else if (value instanceof Object) {
3✔
99
      accu.push({ ...commonProps, secondary: JSON.stringify(value) || '-' });
1!
100
    } else {
101
      accu.push({ ...commonProps, secondary: value || '-' });
2✔
102
    }
103
    return accu;
4✔
104
  }, []);
105
};
106

107
const DevicesLink = ({ artifact: { installCount }, softwareItem: { key, name, version } }) => {
7✔
UNCOV
108
  const { classes } = useStyles();
×
UNCOV
109
  const text = `${installCount} ${pluralize('device', installCount)}`;
×
UNCOV
110
  if (!installCount) {
×
UNCOV
111
    return <div className={classes.link}>{text}</div>;
×
112
  }
UNCOV
113
  const attribute = `${key}${name ? `.${name}` : ''}.version`;
×
UNCOV
114
  return (
×
115
    <a
116
      className={`flexbox center-aligned ${classes.link}`}
117
      href={`${window.location.origin}/ui/devices/accepted?inventory=${attribute}:eq:${version}`}
118
      target="_blank"
119
      rel="noreferrer"
120
    >
121
      {text}
122
      <LaunchIcon className="margin-left-small" fontSize="small" />
123
    </a>
124
  );
125
};
126

127
export const ArtifactDetails = ({ artifact, canManageReleases, editArtifact, getArtifactInstallCount, getArtifactUrl, open, showRemoveArtifactDialog }) => {
7✔
128
  const { classes } = useStyles();
13✔
129
  const [descEdit, setDescEdit] = useState(false);
13✔
130
  const [description, setDescription] = useState(artifact.description);
13✔
131
  const [showPayloads, setShowPayloads] = useState(false);
13✔
132
  const [showProvidesDepends, setShowProvidesDepends] = useState(false);
13✔
133

134
  const softwareVersions = useMemo(() => {
13✔
135
    const { software } = extractSoftware(artifact.artifact_provides);
4✔
136
    return software.reduce((accu, item) => {
4✔
137
      const infoItems = item[0].split('.');
2✔
138
      if (infoItems[infoItems.length - 1] !== 'version') {
2!
UNCOV
139
        return accu;
×
140
      }
141
      accu.push({ key: infoItems[0], name: infoItems.slice(1, infoItems.length - 1).join('.'), version: item[1], nestingLevel: infoItems.length });
2✔
142
      return accu;
2✔
143
    }, []);
144
  }, [JSON.stringify(artifact.artifact_provides)]);
145

146
  useEffect(() => {
13✔
147
    if (artifact.url || !open) {
4✔
148
      return;
2✔
149
    }
150
    getArtifactUrl(artifact.id);
2✔
151
  }, [artifact.id, artifact.url, open]);
152

153
  useEffect(() => {
13✔
154
    if (artifact.installCount || !open || softwareVersions.length > 1) {
4✔
155
      return;
2✔
156
    }
157
    const { version } = softwareVersions.sort((a, b) => a.nestingLevel - b.nestingLevel).reduce((accu, item) => accu ?? item, undefined) ?? {};
2!
158
    if (version) {
2!
159
      getArtifactInstallCount(artifact.id);
2✔
160
    }
161
  }, [artifact.id, artifact.installCount, open, softwareVersions.length]);
162

163
  const onToggleEditing = useCallback(
13✔
164
    event => {
UNCOV
165
      event.stopPropagation();
×
UNCOV
166
      if (event.keyCode === 13 || !event.keyCode) {
×
UNCOV
167
        if (descEdit) {
×
168
          // save change
UNCOV
169
          editArtifact(artifact.id, description);
×
170
        }
UNCOV
171
        setDescEdit(!descEdit);
×
172
      }
173
    },
174
    [descEdit, description, editArtifact, setDescEdit]
175
  );
176

177
  const softwareItem = extractSoftwareItem(artifact.artifact_provides);
13✔
178
  const softwareInformation = softwareItem
13✔
179
    ? {
180
        title: 'Software versioning information',
181
        content: [
182
          { primary: 'Software filesystem', secondary: softwareItem.key },
183
          { primary: 'Software name', secondary: softwareItem.name },
184
          { primary: 'Software version', secondary: softwareItem.version }
185
        ]
186
      }
187
    : { content: [] };
188

189
  const artifactMetaInfo = [
13✔
190
    { title: 'Depends', content: transformArtifactCapabilities(artifact.artifact_depends) },
191
    { title: 'Clears', content: transformArtifactCapabilities(artifact.artifact_clears) },
192
    { title: 'Provides', content: transformArtifactCapabilities(artifact.artifact_provides) },
193
    { title: 'Artifact metadata', content: transformArtifactMetadata(artifact.metaData) }
194
  ];
195
  const hasMetaInfo = artifactMetaInfo.some(item => !!item.content.length);
41✔
196
  const { installCount } = artifact;
13✔
197
  const itemProps = { classes: { root: 'attributes', disabled: 'opaque' }, className: classes.listItemStyle };
13✔
198
  return (
13✔
199
    <div className={artifact.name == null ? 'muted' : null}>
13✔
200
      <List className="list-horizontal-flex">
201
        <ListItem {...itemProps}>
202
          <ListItemText
203
            primary="Description"
204
            style={{ marginBottom: -3, minWidth: 600 }}
205
            primaryTypographyProps={{ style: { marginBottom: 3 } }}
206
            secondary={
207
              <Input
208
                id="artifact-description"
209
                type="text"
210
                disabled={!descEdit}
211
                value={description}
212
                placeholder="-"
213
                onKeyDown={onToggleEditing}
214
                style={{ width: '100%' }}
UNCOV
215
                onChange={e => setDescription(e.target.value)}
×
216
                endAdornment={
217
                  <InputAdornment position="end">
218
                    <IconButton className={classes.editButton} onClick={onToggleEditing} size="large">
219
                      {descEdit ? <CheckIcon /> : <EditIcon />}
13!
220
                    </IconButton>
221
                  </InputAdornment>
222
                }
223
              />
224
            }
225
            secondaryTypographyProps={{ component: 'div' }}
226
          />
227
        </ListItem>
228
        <ListItem {...itemProps} className={`${classes.listItemStyle} ${classes.listItemStyle.bordered}`}>
229
          <ListItemText primary="Signed" secondary={artifact.signed ? <CheckCircleOutlineIcon className="green" /> : <CancelOutlinedIcon className="red" />} />
13!
230
        </ListItem>
231
        {installCount !== undefined && softwareVersions.length === 1 && (
13!
232
          <ExpandableAttribute
233
            classes={{ root: classes.paddingOverride }}
234
            disableGutters
235
            primary="Installed on"
236
            secondary={<DevicesLink artifact={artifact} softwareItem={softwareItem} />}
237
            secondaryTypographyProps={{ title: `installed on ${installCount} ${pluralize('device', installCount)}` }}
238
            style={{ padding: 0 }}
239
          />
240
        )}
241
      </List>
242
      <ArtifactMetadataList metaInfo={softwareInformation} />
UNCOV
243
      <Accordion square expanded={showPayloads} onChange={() => setShowPayloads(toggle)} className={classes.accordPanel1}>
×
244
        <AccordionSummary className={classes.accordSummary}>
245
          <p>Artifact contents</p>
246
          <div style={{ marginLeft: 'auto' }}>{showPayloads ? <RemoveIcon /> : <AddIcon />}</div>
13!
247
        </AccordionSummary>
248
        <AccordionDetails className={classes.accordSummary}>
249
          {showPayloads &&
13!
250
            !!artifact.updates.length &&
UNCOV
251
            artifact.updates.map((update, index) => <ArtifactPayload index={index} payload={update} key={`artifact-update-${index}`} />)}
×
252
        </AccordionDetails>
253
      </Accordion>
254
      {hasMetaInfo && (
24✔
UNCOV
255
        <Accordion square expanded={showProvidesDepends} onChange={() => setShowProvidesDepends(!showProvidesDepends)} className={classes.accordPanel1}>
×
256
          <AccordionSummary className={classes.accordSummary}>
257
            <p>Provides and Depends</p>
258
            <div style={{ marginLeft: 'auto' }}>{showProvidesDepends ? <RemoveIcon /> : <AddIcon />}</div>
11!
259
          </AccordionSummary>
260
          <AccordionDetails className={classes.accordSummary}>
UNCOV
261
            {showProvidesDepends && artifactMetaInfo.map((info, index) => <ArtifactMetadataList metaInfo={info} key={`artifact-info-${index}`} />)}
×
262
          </AccordionDetails>
263
        </Accordion>
264
      )}
265
      <div className="two-columns margin-top-small" style={{ maxWidth: 'fit-content' }}>
266
        {canManageReleases && (
26✔
267
          <>
268
            <Button
269
              href={artifact.url}
270
              target="_blank"
271
              disabled={!artifact.url}
272
              download={artifact.name ? `${artifact.name}.mender` : true}
13✔
273
              startIcon={<ExitToAppIcon style={{ transform: 'rotateZ(90deg)' }} />}
274
            >
275
              Download Artifact
276
            </Button>
277
            <Button onClick={showRemoveArtifactDialog} startIcon={<CancelIcon className="red auth" />}>
278
              Remove this Artifact?
279
            </Button>
280
          </>
281
        )}
282
      </div>
283
    </div>
284
  );
285
};
286

287
const actionCreators = { getArtifactInstallCount, getArtifactUrl };
7✔
288

289
const mapStateToProps = state => {
7✔
290
  const { canManageReleases } = getUserCapabilities(state);
18✔
291
  return {
18✔
292
    canManageReleases
293
  };
294
};
295

296
export default connect(mapStateToProps, actionCreators)(ArtifactDetails);
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