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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 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✔
108
  const { classes } = useStyles();
×
109
  const text = `${installCount} ${pluralize('device', installCount)}`;
×
110
  if (!installCount) {
×
111
    return <div className={classes.link}>{text}</div>;
×
112
  }
113
  const attribute = `${key}${name ? `.${name}` : ''}.version`;
×
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!
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 => {
165
      event.stopPropagation();
×
166
      if (event.keyCode === 13 || !event.keyCode) {
×
167
        if (descEdit) {
×
168
          // save change
169
          editArtifact(artifact.id, description);
×
170
        }
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%' }}
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} />
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 &&
251
            artifact.updates.map((update, index) => <ArtifactPayload index={index} payload={update} key={`artifact-update-${index}`} />)}
×
252
        </AccordionDetails>
253
      </Accordion>
254
      {hasMetaInfo && (
24✔
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}>
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