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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3870: MEN-5413

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

90.43
/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 InfoHint from '../../common/info-hint';
29
import ArtifactInformationForm from './artifactinformationform';
30
import ArtifactUploadConfirmation from './artifactupload';
31

32
const reFilename = new RegExp(/^[a-z0-9.,_-]+$/i);
7✔
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',
41
    marginBottom: theme.spacing(2),
42
    marginRight: theme.spacing(4)
43
  },
44
  fileSizeWrapper: { marginTop: 5 },
45
  infoIcon: { '> svg': { alignSelf: 'flex-start', marginTop: theme.spacing(0.5) } }
46
}));
47

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

59
const fileInformationContent = {
7✔
60
  mender: {
61
    title: 'Mender Artifact',
62
    icon: InsertDriveFileIcon,
63
    info: (
64
      <>
65
        If there is no Release matching this Artifact’s name, a new Release will be created for this Artifact.
66
        <br />
67
        <br />
68
        If there is already a Release matching this Artifact’s name, the Artifact will be grouped in that Release.
69
      </>
70
    )
71
  },
72
  singleFile: {
73
    title: 'Single File',
74
    icon: InsertDriveFileIcon,
75
    info: `This will generate a single file application update Artifact, which requires some additional metadata to be entered.`
76
  }
77
};
78

79
export const FileInformation = ({ file, type, onRemove }) => {
7✔
80
  const { classes } = useStyles();
74✔
81
  if (!file) {
74✔
82
    return <div />;
1✔
83
  }
84
  const { icon: Icon, info, title } = fileInformationContent[type];
73✔
85
  return (
73✔
86
    <>
87
      <h4>Selected {title}</h4>
88
      <div className={classes.fileInfo}>
89
        <Icon size="large" />
90
        <div className="flexbox column">
91
          <div>{file.name}</div>
92
          <div className={`muted ${classes.fileSizeWrapper}`}>
93
            <FileSize fileSize={file.size} />
94
          </div>
95
        </div>
96
        <IconButton size="large" onClick={onRemove}>
97
          <DeleteIcon />
98
        </IconButton>
99
      </div>
100
      <Divider className="margin-right-large" />
101
      <InfoHint className={classes.infoIcon} content={info} />
102
    </>
103
  );
104
};
105

106
const commonExtensions = ['zip', 'txt', 'tar', 'html', 'tar.gzip', 'gzip'];
7✔
107
const shortenFileName = name => {
7✔
108
  const extension = commonExtensions.find(extension => name.endsWith(extension));
8✔
109
  if (extension) {
2✔
110
    const dotIndex = name.lastIndexOf(`.${extension}`);
1✔
111
    return name.substring(0, dotIndex);
1✔
112
  }
113
  return name;
1✔
114
};
115

116
export const ArtifactUpload = ({ setSnackbar, updateCreation }) => {
7✔
117
  const onboardingAnchor = useRef();
3✔
118
  const { classes } = useStyles();
3✔
119
  // eslint-disable-next-line no-unused-vars
120
  const size = useWindowSize();
3✔
121

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

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

164
export const AddArtifactDialog = ({ onCancel, onUploadStarted, releases, selectedFile }) => {
7✔
165
  const [activeStep, setActiveStep] = useState(0);
78✔
166
  const [creation, setCreation] = useState({
78✔
167
    customDeviceTypes: '',
168
    destination: '',
169
    file: undefined,
170
    fileSystem: 'rootfs-image',
171
    finalStep: false,
172
    isValid: false,
173
    isValidDestination: false,
174
    name: '',
175
    selectedDeviceTypes: [],
176
    softwareName: '',
177
    softwareVersion: '',
178
    type: uploadTypes.mender.key
179
  });
180

181
  const deviceTypes = useSelector(getDeviceTypes);
78✔
182
  const dispatch = useDispatch();
78✔
183

184
  const onCreateArtifact = useCallback((meta, file) => dispatch(createArtifact(meta, file)), [dispatch]);
78✔
185
  const onSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
78✔
186
  const onUploadArtifact = useCallback((meta, file) => dispatch(uploadArtifact(meta, file)), [dispatch]);
78✔
187

188
  useEffect(() => {
78✔
189
    setCreation(current => ({ ...current, file: selectedFile }));
3✔
190
  }, [selectedFile]);
191

192
  const addArtifact = useCallback(
78✔
193
    (meta, file, type = 'upload') => {
×
194
      const upload = type === 'create' ? onCreateArtifact(meta, file) : onUploadArtifact(meta, file);
2✔
195
      onUploadStarted();
2✔
196
      // track in GA
197
      return upload.then(() => Tracking.event({ category: 'artifacts', action: 'create' }));
2✔
198
    },
199
    [onCreateArtifact, onUploadStarted, onUploadArtifact]
200
  );
201

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

221
  const onUpdateCreation = useCallback(update => setCreation(current => ({ ...current, ...update })), []);
106✔
222

223
  const onNextClick = useCallback(() => {
78✔
224
    onUpdateCreation({ isValid: false });
1✔
225
    setActiveStep(activeStep + 1);
1✔
226
  }, [activeStep, onUpdateCreation]);
227

228
  const onRemove = () => onUpdateCreation({ file: undefined, isValid: false });
78✔
229

230
  const { file, finalStep, isValid, type } = creation;
78✔
231
  const { component: ComponentToShow } = uploadTypes[type];
78✔
232
  const commonProps = { releases, setSnackbar: onSetSnackbar, updateCreation: onUpdateCreation };
78✔
233

234
  return (
78✔
235
    <Dialog open={true} fullWidth={true} maxWidth="sm">
236
      <DialogTitle>Upload an Artifact</DialogTitle>
237
      <DialogContent className="dialog-content margin-top margin-left margin-right margin-bottom">
238
        {!file ? (
78✔
239
          <ArtifactUpload {...commonProps} />
240
        ) : (
241
          <ComponentToShow {...commonProps} activeStep={activeStep} creation={creation} deviceTypes={deviceTypes} onRemove={onRemove} />
242
        )}
243
      </DialogContent>
244
      <DialogActions>
245
        <Button onClick={onCancel}>Cancel</Button>
UNCOV
246
        {!!activeStep && <Button onClick={() => setActiveStep(activeStep - 1)}>Back</Button>}
✔
247
        <div style={{ flexGrow: 1 }} />
248
        {file && (
150✔
249
          <Button variant="contained" color="primary" disabled={!isValid} onClick={() => (finalStep ? onUpload() : onNextClick())}>
3✔
250
            {finalStep ? 'Upload artifact' : 'Next'}
72✔
251
          </Button>
252
        )}
253
      </DialogActions>
254
    </Dialog>
255
  );
256
};
257

258
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