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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 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]) => {
75✔
78
    if (!Array.isArray(value)) {
93✔
79
      accu.push({ key, primary: key, secondary: value });
47✔
80
    } else if (!key.startsWith('device_type')) {
46✔
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) => {
23✔
83
        const dependencyKey = value.length > 1 ? `${key}-${index + 1}` : key;
46!
84
        dependencies.push({ key: dependencyKey, primary: dependencyKey, secondary: dependency });
46✔
85
        return dependencies;
46✔
86
      }, []);
87
      accu.push(...dependencies);
23✔
88
    }
89
    return accu;
93✔
90
  }, []);
91
};
92

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

134
  const dispatch = useDispatch();
24✔
135

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

138
  const softwareVersions = useMemo(() => {
24✔
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!
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
  }, [JSON.stringify(artifact.artifact_provides)]);
149

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

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

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

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

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

291
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