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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

82.81
/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
  ExitToApp as ExitToAppIcon,
24
  Launch as LaunchIcon,
25
  Remove as RemoveIcon
26
} from '@mui/icons-material';
27
import { Accordion, AccordionDetails, AccordionSummary, Button, List, ListItem, ListItemText } from '@mui/material';
28
import { makeStyles } from 'tss-react/mui';
29

30
import pluralize from 'pluralize';
31

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

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

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

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

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

122
export const ArtifactDetails = ({ artifact, open, showRemoveArtifactDialog }) => {
6✔
123
  const { classes } = useStyles();
28✔
124
  const [showPayloads, setShowPayloads] = useState(false);
28✔
125
  const [showProvidesDepends, setShowProvidesDepends] = useState(false);
28✔
126

127
  const dispatch = useDispatch();
28✔
128

129
  const { canManageReleases } = useSelector(getUserCapabilities);
28✔
130

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

144
  useEffect(() => {
28✔
145
    if (artifact.url || !open) {
6✔
146
      return;
4✔
147
    }
148
    dispatch(getArtifactUrl(artifact.id));
2✔
149
  }, [artifact.id, artifact.url, dispatch, open]);
150

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

162
  const onDescriptionChanged = useCallback(description => dispatch(editArtifact(artifact.id, { description })), [artifact.id, dispatch]);
28✔
163

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

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

256
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