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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

96.53
/frontend/src/js/components/releases/ArtifactDetails.tsx
1
// Copyright 2015 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useCallback, useEffect, useMemo, useState } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16

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

2✔
30
import { EditableLongText } from '@northern.tech/common-ui/EditableLongText';
2✔
31
import ExpandableAttribute from '@northern.tech/common-ui/ExpandableAttribute';
2✔
32
import { getUserCapabilities } from '@northern.tech/store/selectors';
2✔
33
import { editArtifact, getArtifactInstallCount, getArtifactUrl } from '@northern.tech/store/thunks';
2✔
34
import { extractSoftware, extractSoftwareItem, toggle } from '@northern.tech/utils/helpers';
2✔
35
import pluralize from 'pluralize';
2✔
36

2✔
37
import ArtifactMetadataList from './ArtifactMetadataList';
2✔
38
import ArtifactPayload from './ArtifactPayload';
2✔
39

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

2✔
71
export const transformArtifactCapabilities = (capabilities = {}) =>
10✔
72
  Object.entries(capabilities).reduce((accu, [key, value]) => {
74✔
73
    if (!Array.isArray(value)) {
91✔
74
      accu.push({ key, primary: key, secondary: value });
47✔
75
    } else if (!key.startsWith('device_type')) {
46✔
76
      // we can expect this to be an array of artifacts or artifact groups this artifact depends on
2✔
77
      const dependencies = value.reduce((dependencies, dependency, index) => {
26✔
78
        const dependencyKey = value.length > 1 ? `${key}-${index + 1}` : key;
46✔
79
        dependencies.push({ key: dependencyKey, primary: dependencyKey, secondary: dependency });
46✔
80
        return dependencies;
46✔
81
      }, []);
2✔
82
      accu.push(...dependencies);
26✔
83
    }
2✔
84
    return accu;
91✔
85
  }, []);
2✔
86

2✔
87
export const transformArtifactMetadata = (metadata = {}) =>
10✔
88
  Object.entries(metadata).reduce((accu, [key, value]) => {
26✔
89
    const commonProps = { key, primary: key, secondaryTypographyProps: { component: 'div' } };
6✔
90
    if (Array.isArray(value)) {
6✔
91
      accu.push({ ...commonProps, secondary: value.length ? value.join(',') : '-' });
3!
92
    } else if (value instanceof Object) {
5✔
93
      accu.push({ ...commonProps, secondary: JSON.stringify(value) || '-' });
3!
94
    } else {
2✔
95
      accu.push({ ...commonProps, secondary: value || '-' });
4✔
96
    }
2✔
97
    return accu;
6✔
98
  }, []);
2✔
99

2✔
100
const DevicesLink = ({ artifact: { installCount }, softwareItem: { key, name, version } }) => {
10✔
101
  const { classes } = useStyles();
9✔
102
  const text = `${installCount} ${pluralize('device', installCount)}`;
9✔
103
  if (!installCount) {
9!
104
    return <div className={classes.link}>{text}</div>;
9✔
105
  }
2✔
UNCOV
106
  const attribute = `${key}${name ? `.${name}` : ''}.version`;
2!
107
  return (
9✔
108
    <a
2✔
109
      className={`flexbox center-aligned ${classes.link}`}
2✔
110
      href={`${window.location.origin}/ui/devices/accepted?inventory=${attribute}:eq:${version}`}
2✔
111
      target="_blank"
2✔
112
      rel="noreferrer"
2✔
113
    >
2✔
114
      {text}
2✔
115
      <LaunchIcon className="margin-left-small" fontSize="small" />
2✔
116
    </a>
2✔
117
  );
2✔
118
};
2✔
119

2✔
120
export const ArtifactDetails = ({ artifact, open, showRemoveArtifactDialog }) => {
10✔
121
  const { classes } = useStyles();
25✔
122
  const [showPayloads, setShowPayloads] = useState(false);
25✔
123
  const [showProvidesDepends, setShowProvidesDepends] = useState(false);
25✔
124

2✔
125
  const dispatch = useDispatch();
25✔
126

2✔
127
  const { canManageReleases } = useSelector(getUserCapabilities);
25✔
128

2✔
129
  const softwareVersions = useMemo(() => {
25✔
130
    const { software } = extractSoftware(artifact.artifact_provides);
8✔
131
    return software.reduce((accu, item) => {
8✔
132
      const infoItems = item[0].split('.');
7✔
133
      if (infoItems[infoItems.length - 1] !== 'version') {
7!
134
        return accu;
2✔
135
      }
2✔
136
      accu.push({ key: infoItems[0], name: infoItems.slice(1, infoItems.length - 1).join('.'), version: item[1], nestingLevel: infoItems.length });
7✔
137
      return accu;
7✔
138
    }, []);
2✔
139
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
140
  }, [JSON.stringify(artifact.artifact_provides)]);
2✔
141

2✔
142
  useEffect(() => {
25✔
143
    if (artifact.url || !open) {
9✔
144
      return;
8✔
145
    }
2✔
146
    dispatch(getArtifactUrl(artifact.id));
3✔
147
  }, [artifact.id, artifact.url, dispatch, open]);
2✔
148

2✔
149
  useEffect(() => {
25✔
150
    if (artifact.installCount || !open || softwareVersions.length > 1) {
10✔
151
      return;
8✔
152
    }
2✔
153
    const { version } = softwareVersions.sort((a, b) => a.nestingLevel - b.nestingLevel).reduce((accu, item) => accu ?? item, undefined) ?? {};
4!
154
    if (version) {
10✔
155
      dispatch(getArtifactInstallCount(artifact.id));
4✔
156
    }
2✔
157
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
158
  }, [artifact.id, artifact.installCount, dispatch, open, softwareVersions.length]);
2✔
159

2✔
160
  const onDescriptionChanged = useCallback(description => dispatch(editArtifact({ id: artifact.id, body: { description } })), [artifact.id, dispatch]);
25✔
161

2✔
162
  const softwareItem = extractSoftwareItem(artifact.artifact_provides);
25✔
163
  const softwareInformation = softwareItem
25✔
164
    ? {
2✔
165
        title: 'Software versioning information',
2✔
166
        content: [
2✔
167
          { key: 'software-filesystem', primary: 'Software filesystem', secondary: softwareItem.key },
2✔
168
          { key: 'software-name', primary: 'Software name', secondary: softwareItem.name },
2✔
169
          { key: 'software-version', primary: 'Software version', secondary: softwareItem.version }
2✔
170
        ]
2✔
171
      }
2✔
172
    : { content: [] };
2✔
173

2✔
174
  const artifactMetaInfo = [
25✔
175
    { title: 'Depends', content: transformArtifactCapabilities(artifact.artifact_depends) },
2✔
176
    { title: 'Clears', content: transformArtifactCapabilities(artifact.artifact_clears) },
2✔
177
    { title: 'Provides', content: transformArtifactCapabilities(artifact.artifact_provides) },
2✔
178
    { title: 'Artifact metadata', content: transformArtifactMetadata(artifact.metaData) }
2✔
179
  ];
2✔
180
  const hasMetaInfo = artifactMetaInfo.some(item => !!item.content.length);
71✔
181
  const { installCount } = artifact;
25✔
182
  const itemProps = { classes: { root: 'attributes', disabled: 'opaque' }, className: classes.listItemStyle };
25✔
183
  return (
25✔
184
    <div className={artifact.name == null ? 'muted' : null}>
2✔
185
      <List className="list-horizontal-flex">
2✔
186
        <ListItem {...itemProps}>
2✔
187
          <ListItemText
2✔
188
            primary="Description"
2✔
189
            style={{ marginBottom: -3, minWidth: 600 }}
2✔
190
            primaryTypographyProps={{ style: { marginBottom: 3 } }}
2✔
191
            secondary={<EditableLongText fullWidth original={artifact.description} onChange={onDescriptionChanged} />}
2✔
192
            secondaryTypographyProps={{ component: 'div' }}
2✔
193
          />
2✔
194
        </ListItem>
2✔
195
        <ListItem {...itemProps} className={`${classes.listItemStyle} ${classes.listItemStyle.bordered}`}>
2✔
196
          <ListItemText primary="Signed" secondary={artifact.signed ? <CheckCircleOutlineIcon className="green" /> : <CancelOutlinedIcon className="red" />} />
2!
197
        </ListItem>
2✔
198
        {installCount !== undefined && softwareVersions.length === 1 && (
2✔
199
          <ExpandableAttribute
2✔
200
            classes={{ root: classes.paddingOverride }}
2✔
201
            disableGutters
2✔
202
            primary="Installed on"
2✔
203
            secondary={<DevicesLink artifact={artifact} softwareItem={softwareItem} />}
2✔
204
            secondaryTypographyProps={{ title: `installed on ${installCount} ${pluralize('device', installCount)}` }}
2✔
205
            style={{ padding: 0 }}
2✔
206
          />
2✔
207
        )}
2✔
208
      </List>
2✔
209
      <ArtifactMetadataList metaInfo={softwareInformation} />
2✔
UNCOV
210
      <Accordion square expanded={showPayloads} onChange={() => setShowPayloads(toggle)} className={classes.accordPanel1}>
2✔
211
        <AccordionSummary className={classes.accordSummary}>
2✔
212
          <p>Artifact contents</p>
2✔
213
          <div style={{ marginLeft: 'auto' }}>{showPayloads ? <RemoveIcon /> : <AddIcon />}</div>
2!
214
        </AccordionSummary>
2✔
215
        <AccordionDetails className={classes.accordSummary}>
2✔
216
          {showPayloads &&
2!
217
            !!artifact.updates.length &&
2✔
218
            artifact.updates.map((update, index) => <ArtifactPayload index={index} payload={update} key={`artifact-update-${index}`} />)}
2✔
219
        </AccordionDetails>
2✔
220
      </Accordion>
2✔
221
      {hasMetaInfo && (
2✔
222
        <Accordion square expanded={showProvidesDepends} onChange={() => setShowProvidesDepends(!showProvidesDepends)} className={classes.accordPanel1}>
3✔
223
          <AccordionSummary className={classes.accordSummary}>
2✔
224
            <p>Provides and Depends</p>
2✔
225
            <div style={{ marginLeft: 'auto' }}>{showProvidesDepends ? <RemoveIcon /> : <AddIcon />}</div>
2✔
226
          </AccordionSummary>
2✔
227
          <AccordionDetails className={classes.accordSummary}>
2✔
228
            {showProvidesDepends && artifactMetaInfo.map((info, index) => <ArtifactMetadataList metaInfo={info} key={`artifact-info-${index}`} />)}
6✔
229
          </AccordionDetails>
2✔
230
        </Accordion>
2✔
231
      )}
2✔
232
      <div className="two-columns margin-top-small" style={{ maxWidth: 'fit-content' }}>
2✔
233
        {canManageReleases && (
2✔
234
          <>
2✔
235
            <Button
2✔
236
              href={artifact.url}
2✔
237
              target="_blank"
2✔
238
              disabled={!artifact.url}
2✔
239
              download={artifact.name ? `${artifact.name}.mender` : true}
2✔
240
              startIcon={<ExitToAppIcon style={{ transform: 'rotateZ(90deg)' }} />}
2✔
241
            >
2✔
242
              Download Artifact
2✔
243
            </Button>
2✔
244
            <Button onClick={showRemoveArtifactDialog} startIcon={<CancelIcon className="red auth" />}>
2✔
245
              Remove this Artifact?
2✔
246
            </Button>
2✔
247
          </>
2✔
248
        )}
2✔
249
      </div>
2✔
250
    </div>
2✔
251
  );
2✔
252
};
2✔
253

2✔
254
export default ArtifactDetails;
2✔
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