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

mendersoftware / gui / 944676341

pending completion
944676341

Pull #3875

gitlab-ci

mzedel
chore: aligned snapshots with updated design

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

4469 of 6446 branches covered (69.33%)

230 of 266 new or added lines in 43 files covered. (86.47%)

1712 existing lines in 161 files now uncovered.

8406 of 10170 relevant lines covered (82.65%)

196.7 hits per line

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

82.48
/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

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

21
import { onboardingSteps } from '../../../constants/onboardingConstants';
22
import { FileSize, unionizeStrings } from '../../../helpers';
23
import Tracking from '../../../tracking';
24
import { getOnboardingComponentFor } from '../../../utils/onboardingmanager';
25
import useWindowSize from '../../../utils/resizehook';
26
import { HELPTOOLTIPS, MenderHelpTooltip } from '../../helptips/helptooltips';
27
import ArtifactInformationForm from './artifactinformationform';
28
import ArtifactUploadConfirmation from './artifactupload';
29

30
const reFilename = new RegExp(/^[a-z0-9.,_-]+$/i);
7✔
31

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

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

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

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

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

106
export const ArtifactUpload = ({ advanceOnboarding, onboardingState, releases, setSnackbar, updateCreation }) => {
7✔
107
  const onboardingAnchor = useRef();
3✔
108
  const { classes } = useStyles();
3✔
109
  // eslint-disable-next-line no-unused-vars
110
  const size = useWindowSize();
3✔
111

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

135
  let onboardingComponent = null;
3✔
136
  if (!onboardingState.complete && onboardingAnchor.current) {
3!
UNCOV
137
    const anchor = {
×
138
      left: onboardingAnchor.current.offsetLeft + onboardingAnchor.current.clientWidth,
139
      top: onboardingAnchor.current.offsetTop + onboardingAnchor.current.clientHeight / 2
140
    };
UNCOV
141
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.UPLOAD_NEW_ARTIFACT_DIALOG_UPLOAD, onboardingState, { anchor, place: 'right' });
×
142
  }
143
  return (
3✔
144
    <>
145
      <div className="flexbox column centered margin">
146
        Upload a premade Mender Artifact
147
        <p className="muted">OR</p>
148
        Upload a file to generate a single file application update Artifact
149
      </div>
150
      <Dropzone multiple={false} onDrop={onDrop}>
151
        {({ getRootProps, getInputProps }) => (
152
          <div {...getRootProps({ className: `fadeIn onboard dropzone ${classes.dropzone}` })} ref={onboardingAnchor}>
13✔
153
            <input {...getInputProps()} />
154
            <CloudUpload fontSize="large" className="muted" />
155
            <div>
156
              Drag and drop here or <b>browse</b> to upload
157
            </div>
158
          </div>
159
        )}
160
      </Dropzone>
161
      {!!onboardingComponent && onboardingComponent}
3!
162
    </>
163
  );
164
};
165

166
export const AddArtifactDialog = ({
7✔
167
  advanceOnboarding,
168
  createArtifact,
169
  deviceTypes = [],
2✔
170
  onboardingState,
171
  onCancel,
172
  onUploadStarted,
173
  pastCount,
174
  releases,
175
  selectedFile,
176
  setSnackbar,
177
  uploadArtifact
178
}) => {
179
  const [activeStep, setActiveStep] = useState(0);
45✔
180
  const [creation, setCreation] = useState({
45✔
181
    customDeviceTypes: '',
182
    destination: '',
183
    file: undefined,
184
    fileSystem: 'rootfs-image',
185
    finalStep: false,
186
    isValid: false,
187
    isValidDestination: false,
188
    name: '',
189
    selectedDeviceTypes: [],
190
    softwareName: '',
191
    softwareVersion: '',
192
    type: uploadTypes.mender.key
193
  });
194

195
  useEffect(() => {
45✔
196
    setCreation(current => ({ ...current, file: selectedFile }));
3✔
197
  }, [selectedFile]);
198

199
  const onUpload = useCallback(() => {
45✔
200
    const { customDeviceTypes, destination, file, fileSystem, name, selectedDeviceTypes, softwareName, softwareVersion } = creation;
2✔
201
    const { name: filename = '' } = file;
2!
202
    let meta = { description: '' };
2✔
203
    if (filename.endsWith('.mender')) {
2✔
204
      return addArtifact(meta, file, 'upload');
1✔
205
    }
206
    const otherDeviceTypes = customDeviceTypes.split(',');
1✔
207
    const deviceTypes = unionizeStrings(selectedDeviceTypes, otherDeviceTypes);
1✔
208
    meta = {
1✔
209
      ...meta,
210
      device_types_compatible: deviceTypes,
211
      args: { dest_dir: destination, filename, software_filesystem: fileSystem, software_name: softwareName, software_version: softwareVersion },
212
      name
213
    };
214
    return addArtifact(meta, file, 'create');
1✔
215
  }, [JSON.stringify(creation)]);
216

217
  const addArtifact = (meta, file, type = 'upload') => {
45!
218
    const upload = type === 'create' ? createArtifact(meta, file) : uploadArtifact(meta, file);
2✔
219
    onUploadStarted();
2✔
220
    return upload.then(() => {
2✔
221
      if (!onboardingState.complete && deviceTypes.length && pastCount) {
2!
UNCOV
222
        advanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP);
×
UNCOV
223
        if (type === 'create') {
×
UNCOV
224
          advanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_DIALOG_CLICK);
×
225
        }
226
      }
227
      // track in GA
228
      Tracking.event({ category: 'artifacts', action: 'create' });
2✔
229
    });
230
  };
231

232
  const onUpdateCreation = update => setCreation(current => ({ ...current, ...update }));
45✔
233

234
  const onNextClick = useCallback(() => {
45✔
235
    if (!onboardingState.complete && creation.destination) {
1!
UNCOV
236
      advanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_DIALOG_DEVICE_TYPE);
×
237
    }
238
    onUpdateCreation({ isValid: false });
1✔
239
    setActiveStep(activeStep + 1);
1✔
240
  }, [activeStep, creation.destination, onboardingState.complete]);
241

242
  const onRemove = () => onUpdateCreation({ file: undefined, isValid: false });
45✔
243
  const buttonRef = useRef();
45✔
244

245
  const { file, finalStep, isValid, type } = creation;
45✔
246
  const { component: ComponentToShow } = uploadTypes[type];
45✔
247
  const commonProps = { advanceOnboarding, onboardingState, releases, setSnackbar, updateCreation: onUpdateCreation };
45✔
248

249
  let onboardingComponent = null;
45✔
250
  if (!onboardingState.complete && buttonRef.current && finalStep && isValid) {
45✔
251
    const releaseNameAnchor = {
1✔
252
      left: buttonRef.current.offsetLeft - 15,
253
      top: buttonRef.current.offsetTop + buttonRef.current.clientHeight / 2
254
    };
255
    onboardingComponent = getOnboardingComponentFor(
1✔
256
      onboardingSteps.UPLOAD_NEW_ARTIFACT_DIALOG_CLICK,
257
      onboardingState,
258
      { anchor: releaseNameAnchor, place: 'left' },
259
      onboardingComponent
260
    );
261
  }
262
  return (
45✔
263
    <Dialog open={true} fullWidth={true} maxWidth="sm">
264
      <DialogTitle>Upload an Artifact</DialogTitle>
265
      <DialogContent className="dialog-content margin-top margin-left margin-right margin-bottom">
266
        {!file ? (
45✔
267
          <ArtifactUpload {...commonProps} />
268
        ) : (
269
          <ComponentToShow {...commonProps} activeStep={activeStep} creation={creation} deviceTypes={deviceTypes} onRemove={onRemove} />
270
        )}
271
      </DialogContent>
272
      <DialogActions>
273
        <Button onClick={onCancel}>Cancel</Button>
UNCOV
274
        {!!activeStep && <Button onClick={() => setActiveStep(activeStep - 1)}>Back</Button>}
✔
275
        <div style={{ flexGrow: 1 }} />
276
        {file && (
84✔
277
          <Button variant="contained" color="primary" disabled={!isValid} onClick={() => (finalStep ? onUpload() : onNextClick())} ref={buttonRef}>
3✔
278
            {finalStep ? 'Upload artifact' : 'Next'}
39✔
279
          </Button>
280
        )}
281
        {onboardingComponent}
282
      </DialogActions>
283
    </Dialog>
284
  );
285
};
286

287
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