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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3870: MEN-5413

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

70.63
/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 { useDispatch, useSelector } 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 { editArtifact, 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]) => {
66✔
78
    if (!Array.isArray(value)) {
81✔
79
      accu.push({ key, primary: key, secondary: value });
41✔
80
    } else if (!key.startsWith('device_type')) {
40✔
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) => {
20✔
83
        const dependencyKey = value.length > 1 ? `${key}-${index + 1}` : key;
40!
84
        dependencies.push({ key: dependencyKey, primary: dependencyKey, secondary: dependency });
40✔
85
        return dependencies;
40✔
86
      }, []);
87
      accu.push(...dependencies);
20✔
88
    }
89
    return accu;
81✔
90
  }, []);
91
};
92

93
export const transformArtifactMetadata = (metadata = {}) => {
7✔
94
  return Object.entries(metadata).reduce((accu, [key, value]) => {
22✔
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, open, showRemoveArtifactDialog }) => {
7✔
128
  const { classes } = useStyles();
21✔
129
  const [descEdit, setDescEdit] = useState(false);
21✔
130
  const [description, setDescription] = useState(artifact.description);
21✔
131
  const [showPayloads, setShowPayloads] = useState(false);
21✔
132
  const [showProvidesDepends, setShowProvidesDepends] = useState(false);
21✔
133

134
  const dispatch = useDispatch();
21✔
135

136
  const { canManageReleases } = useSelector(getUserCapabilities);
21✔
137

138
  const softwareVersions = useMemo(() => {
21✔
139
    const { software } = extractSoftware(artifact.artifact_provides);
4✔
140
    return software.reduce((accu, item) => {
4✔
141
      const infoItems = item[0].split('.');
2✔
142
      if (infoItems[infoItems.length - 1] !== 'version') {
2!
UNCOV
143
        return accu;
×
144
      }
145
      accu.push({ key: infoItems[0], name: infoItems.slice(1, infoItems.length - 1).join('.'), version: item[1], nestingLevel: infoItems.length });
2✔
146
      return accu;
2✔
147
    }, []);
148
    // eslint-disable-next-line react-hooks/exhaustive-deps
149
  }, [JSON.stringify(artifact.artifact_provides)]);
150

151
  useEffect(() => {
21✔
152
    if (artifact.url || !open) {
4✔
153
      return;
2✔
154
    }
155
    dispatch(getArtifactUrl(artifact.id));
2✔
156
  }, [artifact.id, artifact.url, dispatch, open]);
157

158
  useEffect(() => {
21✔
159
    if (artifact.installCount || !open || softwareVersions.length > 1) {
4✔
160
      return;
2✔
161
    }
162
    const { version } = softwareVersions.sort((a, b) => a.nestingLevel - b.nestingLevel).reduce((accu, item) => accu ?? item, undefined) ?? {};
2!
163
    if (version) {
2!
164
      dispatch(getArtifactInstallCount(artifact.id));
2✔
165
    }
166
    // eslint-disable-next-line react-hooks/exhaustive-deps
167
  }, [artifact.id, artifact.installCount, dispatch, open, softwareVersions.length]);
168

169
  const onToggleEditing = useCallback(
21✔
170
    event => {
UNCOV
171
      event.stopPropagation();
×
UNCOV
172
      if (event.keyCode === 13 || !event.keyCode) {
×
UNCOV
173
        if (descEdit) {
×
174
          // save change
UNCOV
175
          dispatch(editArtifact(artifact.id, { description }));
×
176
        }
UNCOV
177
        setDescEdit(!descEdit);
×
178
      }
179
    },
180
    [artifact.id, descEdit, description, dispatch]
181
  );
182

183
  const softwareItem = extractSoftwareItem(artifact.artifact_provides);
21✔
184
  const softwareInformation = softwareItem
21✔
185
    ? {
186
        title: 'Software versioning information',
187
        content: [
188
          { primary: 'Software filesystem', secondary: softwareItem.key },
189
          { primary: 'Software name', secondary: softwareItem.name },
190
          { primary: 'Software version', secondary: softwareItem.version }
191
        ]
192
      }
193
    : { content: [] };
194

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

293
export default 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