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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 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