• 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

92.65
/src/js/components/releases/dialogs/addartifact.js
1
// Copyright 2020 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, useRef, useState } from 'react';
15
import Dropzone from 'react-dropzone';
16
import { useDispatch, useSelector } from 'react-redux';
17

18
import { CloudUpload, Delete as DeleteIcon, InsertDriveFile as InsertDriveFileIcon } from '@mui/icons-material';
19
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton } from '@mui/material';
20
import { makeStyles } from 'tss-react/mui';
21

22
import { setSnackbar } from '../../../actions/appActions';
23
import { createArtifact, uploadArtifact } from '../../../actions/releaseActions';
24
import { FileSize, unionizeStrings } from '../../../helpers';
25
import { getDeviceTypes } from '../../../selectors';
26
import Tracking from '../../../tracking';
27
import useWindowSize from '../../../utils/resizehook';
28
import { HELPTOOLTIPS, MenderHelpTooltip } from '../../helptips/helptooltips';
29
import ArtifactInformationForm from './artifactinformationform';
30
import ArtifactUploadConfirmation from './artifactupload';
31

32
const reFilename = new RegExp(/^[a-z0-9.,_-]+$/i);
6✔
33

34
const useStyles = makeStyles()(theme => ({
8✔
35
  dropzone: { ['&.dropzone']: { padding: theme.spacing(4) } },
36
  fileInfo: {
37
    alignItems: 'center',
38
    columnGap: theme.spacing(4),
39
    display: 'grid',
40
    gridTemplateColumns: 'max-content 1fr max-content max-content',
41
    marginBottom: theme.spacing(2),
42
    marginRight: theme.spacing(4)
43
  },
44
  fileSizeWrapper: { marginTop: 5 }
45
}));
46

47
const uploadTypes = {
6✔
48
  mender: {
49
    key: 'mender',
50
    component: ArtifactUploadConfirmation
51
  },
52
  singleFile: {
53
    key: 'singleFile',
54
    component: ArtifactInformationForm
55
  }
56
};
57

58
const fileInformationContent = {
6✔
59
  mender: {
60
    title: 'Mender Artifact',
61
    icon: InsertDriveFileIcon,
62
    infoId: 'menderArtifactUpload'
63
  },
64
  singleFile: {
65
    title: 'Single File',
66
    icon: InsertDriveFileIcon,
67
    infoId: 'singleFileUpload'
68
  }
69
};
70

71
export const FileInformation = ({ file, type, onRemove }) => {
6✔
72
  const { classes } = useStyles();
94✔
73
  if (!file) {
94✔
74
    return <div />;
1✔
75
  }
76
  const { icon: Icon, infoId, title } = fileInformationContent[type];
93✔
77
  return (
93✔
78
    <>
79
      <h4>Selected {title}</h4>
80
      <div className={classes.fileInfo}>
81
        <Icon size="large" />
82
        <div className="flexbox column">
83
          <div>{file.name}</div>
84
          <div className={`muted ${classes.fileSizeWrapper}`}>
85
            <FileSize fileSize={file.size} />
86
          </div>
87
        </div>
88
        <IconButton size="large" onClick={onRemove}>
89
          <DeleteIcon />
90
        </IconButton>
91
        <MenderHelpTooltip id={HELPTOOLTIPS[infoId].id} />
92
      </div>
93
      <Divider className="margin-right-large" />
94
    </>
95
  );
96
};
97

98
const commonExtensions = ['zip', 'txt', 'tar', 'html', 'tar.gzip', 'gzip'];
6✔
99
const shortenFileName = name => {
6✔
100
  const extension = commonExtensions.find(extension => name.endsWith(extension));
8✔
101
  if (extension) {
2✔
102
    const dotIndex = name.lastIndexOf(`.${extension}`);
1✔
103
    return name.substring(0, dotIndex);
1✔
104
  }
105
  return name;
1✔
106
};
107

108
export const ArtifactUpload = ({ setSnackbar, updateCreation }) => {
6✔
109
  const onboardingAnchor = useRef();
3✔
110
  const { classes } = useStyles();
3✔
111
  // eslint-disable-next-line no-unused-vars
112
  const size = useWindowSize();
3✔
113

114
  const onDrop = acceptedFiles => {
3✔
115
    const emptyFileInfo = { file: undefined, name: '', type: uploadTypes.mender.key };
2✔
116
    if (acceptedFiles.length === 1) {
2!
117
      if (!reFilename.test(acceptedFiles[0].name)) {
2!
118
        updateCreation(emptyFileInfo);
×
119
        setSnackbar('Only letters, digits and characters in the set ".,_-" are allowed in the filename.', null);
×
120
      } else {
121
        const { name } = acceptedFiles[0];
2✔
122
        updateCreation({
2✔
123
          file: acceptedFiles[0],
124
          name: shortenFileName(name),
125
          type: name.endsWith('.mender') ? uploadTypes.mender.key : uploadTypes.singleFile.key
2✔
126
        });
127
      }
128
    } else {
129
      updateCreation(emptyFileInfo);
×
130
      setSnackbar('The selected file is not supported.', null);
×
131
    }
132
  };
133

134
  return (
3✔
135
    <>
136
      <div className="flexbox column centered margin">
137
        Upload a premade Mender Artifact
138
        <p className="muted">OR</p>
139
        Upload a file to generate a single file application update Artifact
140
      </div>
141
      <Dropzone multiple={false} onDrop={onDrop}>
142
        {({ getRootProps, getInputProps }) => (
143
          <div {...getRootProps({ className: `fadeIn onboard dropzone ${classes.dropzone}` })} ref={onboardingAnchor}>
13✔
144
            <input {...getInputProps()} />
145
            <CloudUpload fontSize="large" className="muted" />
146
            <div>
147
              Drag and drop here or <b>browse</b> to upload
148
            </div>
149
          </div>
150
        )}
151
      </Dropzone>
152
    </>
153
  );
154
};
155

156
export const AddArtifactDialog = ({ onCancel, onUploadStarted, releases, selectedFile }) => {
6✔
157
  const [activeStep, setActiveStep] = useState(0);
88✔
158
  const [creation, setCreation] = useState({
88✔
159
    customDeviceTypes: '',
160
    destination: '',
161
    file: undefined,
162
    fileSystem: 'rootfs-image',
163
    finalStep: false,
164
    isValid: false,
165
    isValidDestination: false,
166
    name: '',
167
    selectedDeviceTypes: [],
168
    softwareName: '',
169
    softwareVersion: '',
170
    type: uploadTypes.mender.key
171
  });
172

173
  const deviceTypes = useSelector(getDeviceTypes);
88✔
174
  const dispatch = useDispatch();
88✔
175

176
  const onCreateArtifact = useCallback((meta, file) => dispatch(createArtifact(meta, file)), [dispatch]);
88✔
177
  const onSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
88✔
178
  const onUploadArtifact = useCallback((meta, file) => dispatch(uploadArtifact(meta, file)), [dispatch]);
88✔
179

180
  useEffect(() => {
88✔
181
    setCreation(current => ({ ...current, file: selectedFile }));
3✔
182
  }, [selectedFile]);
183

184
  const addArtifact = useCallback(
88✔
185
    (meta, file, type = 'upload') => {
×
186
      const upload = type === 'create' ? onCreateArtifact(meta, file) : onUploadArtifact(meta, file);
2✔
187
      onUploadStarted();
2✔
188
      // track in GA
189
      return upload.then(() => Tracking.event({ category: 'artifacts', action: 'create' }));
2✔
190
    },
191
    [onCreateArtifact, onUploadStarted, onUploadArtifact]
192
  );
193

194
  const onUpload = useCallback(() => {
88✔
195
    const { customDeviceTypes, destination, file, fileSystem, name, selectedDeviceTypes, softwareName, softwareVersion } = creation;
2✔
196
    const { name: filename = '' } = file;
2!
197
    let meta = { description: '' };
2✔
198
    if (filename.endsWith('.mender')) {
2✔
199
      return addArtifact(meta, file, 'upload');
1✔
200
    }
201
    const otherDeviceTypes = customDeviceTypes.split(',');
1✔
202
    const deviceTypes = unionizeStrings(selectedDeviceTypes, otherDeviceTypes);
1✔
203
    meta = {
1✔
204
      ...meta,
205
      device_types_compatible: deviceTypes,
206
      args: { dest_dir: destination, filename, software_filesystem: fileSystem, software_name: softwareName, software_version: softwareVersion },
207
      name
208
    };
209
    return addArtifact(meta, file, 'create');
1✔
210
    // eslint-disable-next-line react-hooks/exhaustive-deps
211
  }, [addArtifact, JSON.stringify(creation)]);
212

213
  const onUpdateCreation = useCallback(update => setCreation(current => ({ ...current, ...update })), []);
88✔
214

215
  const onNextClick = useCallback(() => {
88✔
216
    onUpdateCreation({ isValid: false });
1✔
217
    setActiveStep(activeStep + 1);
1✔
218
  }, [activeStep, onUpdateCreation]);
219

220
  const onRemove = () => onUpdateCreation({ file: undefined, isValid: false });
88✔
221

222
  const { file, finalStep, isValid, type } = creation;
88✔
223
  const { component: ComponentToShow } = uploadTypes[type];
88✔
224
  const commonProps = { releases, setSnackbar: onSetSnackbar, updateCreation: onUpdateCreation };
88✔
225

226
  return (
88✔
227
    <Dialog open={true} fullWidth={true} maxWidth="sm">
228
      <DialogTitle>Upload an Artifact</DialogTitle>
229
      <DialogContent className="dialog-content margin-top margin-left margin-right margin-bottom">
230
        {!file ? (
88✔
231
          <ArtifactUpload {...commonProps} />
232
        ) : (
233
          <ComponentToShow {...commonProps} activeStep={activeStep} creation={creation} deviceTypes={deviceTypes} onRemove={onRemove} />
234
        )}
235
      </DialogContent>
236
      <DialogActions>
237
        <Button onClick={onCancel}>Cancel</Button>
238
        {!!activeStep && <Button onClick={() => setActiveStep(activeStep - 1)}>Back</Button>}
✔
239
        <div style={{ flexGrow: 1 }} />
240
        {file && (
170✔
241
          <Button variant="contained" color="primary" disabled={!isValid} onClick={() => (finalStep ? onUpload() : onNextClick())}>
3✔
242
            {finalStep ? 'Upload artifact' : 'Next'}
82✔
243
          </Button>
244
        )}
245
      </DialogActions>
246
    </Dialog>
247
  );
248
};
249

250
export default AddArtifactDialog;
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