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

mendersoftware / gui / 947088195

pending completion
947088195

Pull #2661

gitlab-ci

mzedel
chore: improved device filter scrolling behaviour

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #2661: chore: added lint rules for hooks usage

4411 of 6415 branches covered (68.76%)

297 of 440 new or added lines in 62 files covered. (67.5%)

1617 existing lines in 163 files now uncovered.

8311 of 10087 relevant lines covered (82.39%)

192.12 hits per line

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

76.39
/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 { advanceOnboarding } from '../../../actions/onboardingActions';
24
import { createArtifact, uploadArtifact } from '../../../actions/releaseActions';
25
import { onboardingSteps } from '../../../constants/onboardingConstants';
26
import { FileSize, unionizeStrings } from '../../../helpers';
27
import { getDeviceTypes, getOnboardingState } from '../../../selectors';
28
import Tracking from '../../../tracking';
29
import { getOnboardingComponentFor } from '../../../utils/onboardingmanager';
30
import useWindowSize from '../../../utils/resizehook';
31
import InfoHint from '../../common/info-hint';
32
import ArtifactInformationForm from './artifactinformationform';
33
import ArtifactUploadConfirmation from './artifactupload';
34

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

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

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

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

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

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

119
export const ArtifactUpload = ({ advanceOnboarding, onboardingState, releases, setSnackbar, updateCreation }) => {
7✔
120
  const onboardingAnchor = useRef();
3✔
121
  const { classes } = useStyles();
3✔
122
  // eslint-disable-next-line no-unused-vars
123
  const size = useWindowSize();
3✔
124

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

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

179
export const AddArtifactDialog = ({ onCancel, onUploadStarted, releases, selectedFile }) => {
7✔
180
  const [activeStep, setActiveStep] = useState(0);
78✔
181
  const [creation, setCreation] = useState({
78✔
182
    customDeviceTypes: '',
183
    destination: '',
184
    file: undefined,
185
    fileSystem: 'rootfs-image',
186
    finalStep: false,
187
    isValid: false,
188
    isValidDestination: false,
189
    name: '',
190
    selectedDeviceTypes: [],
191
    softwareName: '',
192
    softwareVersion: '',
193
    type: uploadTypes.mender.key
194
  });
195

196
  const onboardingState = useSelector(getOnboardingState);
78✔
197
  const pastCount = useSelector(state => state.deployments.byStatus.finished.total);
88✔
198
  const deviceTypes = useSelector(getDeviceTypes);
78✔
199
  const dispatch = useDispatch();
78✔
200

201
  const onAdvanceOnboarding = useCallback(step => dispatch(advanceOnboarding(step)), [dispatch]);
78✔
202
  const onCreateArtifact = useCallback((meta, file) => dispatch(createArtifact(meta, file)), [dispatch]);
78✔
203
  const onSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
78✔
204
  const onUploadArtifact = useCallback((meta, file) => dispatch(uploadArtifact(meta, file)), [dispatch]);
78✔
205

206
  useEffect(() => {
78✔
207
    setCreation(current => ({ ...current, file: selectedFile }));
3✔
208
  }, [selectedFile]);
209

210
  const addArtifact = useCallback(
78✔
211
    (meta, file, type = 'upload') => {
×
212
      const upload = type === 'create' ? onCreateArtifact(meta, file) : onUploadArtifact(meta, file);
2✔
213
      onUploadStarted();
2✔
214
      return upload.then(() => {
2✔
215
        if (!onboardingState.complete && deviceTypes.length && pastCount) {
1!
NEW
216
          onAdvanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP);
×
NEW
217
          if (type === 'create') {
×
NEW
218
            onAdvanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_DIALOG_CLICK);
×
219
          }
220
        }
221
        // track in GA
222
        Tracking.event({ category: 'artifacts', action: 'create' });
1✔
223
      });
224
    },
225
    [onAdvanceOnboarding, onCreateArtifact, deviceTypes.length, onUploadStarted, onboardingState.complete, pastCount, onUploadArtifact]
226
  );
227

228
  const onUpload = useCallback(() => {
78✔
229
    const { customDeviceTypes, destination, file, fileSystem, name, selectedDeviceTypes, softwareName, softwareVersion } = creation;
2✔
230
    const { name: filename = '' } = file;
2!
231
    let meta = { description: '' };
2✔
232
    if (filename.endsWith('.mender')) {
2✔
233
      return addArtifact(meta, file, 'upload');
1✔
234
    }
235
    const otherDeviceTypes = customDeviceTypes.split(',');
1✔
236
    const deviceTypes = unionizeStrings(selectedDeviceTypes, otherDeviceTypes);
1✔
237
    meta = {
1✔
238
      ...meta,
239
      device_types_compatible: deviceTypes,
240
      args: { dest_dir: destination, filename, software_filesystem: fileSystem, software_name: softwareName, software_version: softwareVersion },
241
      name
242
    };
243
    return addArtifact(meta, file, 'create');
1✔
244
    // eslint-disable-next-line react-hooks/exhaustive-deps
245
  }, [addArtifact, JSON.stringify(creation)]);
246

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

249
  const onNextClick = useCallback(() => {
78✔
250
    if (!onboardingState.complete && creation.destination) {
1!
NEW
251
      onAdvanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_DIALOG_DEVICE_TYPE);
×
252
    }
253
    onUpdateCreation({ isValid: false });
1✔
254
    setActiveStep(activeStep + 1);
1✔
255
  }, [activeStep, onAdvanceOnboarding, creation.destination, onboardingState.complete, onUpdateCreation]);
256

257
  const onRemove = () => onUpdateCreation({ file: undefined, isValid: false });
78✔
258
  const buttonRef = useRef();
78✔
259

260
  const { file, finalStep, isValid, type } = creation;
78✔
261
  const { component: ComponentToShow } = uploadTypes[type];
78✔
262
  const commonProps = { advanceOnboarding: onAdvanceOnboarding, onboardingState, releases, setSnackbar: onSetSnackbar, updateCreation: onUpdateCreation };
78✔
263

264
  let onboardingComponent = null;
78✔
265
  if (!onboardingState.complete && buttonRef.current && finalStep && isValid) {
78!
UNCOV
266
    const releaseNameAnchor = {
×
267
      left: buttonRef.current.offsetLeft - 15,
268
      top: buttonRef.current.offsetTop + buttonRef.current.clientHeight / 2
269
    };
UNCOV
270
    onboardingComponent = getOnboardingComponentFor(
×
271
      onboardingSteps.UPLOAD_NEW_ARTIFACT_DIALOG_CLICK,
272
      onboardingState,
273
      { anchor: releaseNameAnchor, place: 'left' },
274
      onboardingComponent
275
    );
276
  }
277
  return (
78✔
278
    <Dialog open={true} fullWidth={true} maxWidth="sm">
279
      <DialogTitle>Upload an Artifact</DialogTitle>
280
      <DialogContent className="dialog-content margin-top margin-left margin-right margin-bottom">
281
        {!file ? (
78✔
282
          <ArtifactUpload {...commonProps} />
283
        ) : (
284
          <ComponentToShow {...commonProps} activeStep={activeStep} creation={creation} deviceTypes={deviceTypes} onRemove={onRemove} />
285
        )}
286
      </DialogContent>
287
      <DialogActions>
288
        <Button onClick={onCancel}>Cancel</Button>
UNCOV
289
        {!!activeStep && <Button onClick={() => setActiveStep(activeStep - 1)}>Back</Button>}
✔
290
        <div style={{ flexGrow: 1 }} />
291
        {file && (
150✔
292
          <Button variant="contained" color="primary" disabled={!isValid} onClick={() => (finalStep ? onUpload() : onNextClick())} ref={buttonRef}>
3✔
293
            {finalStep ? 'Upload artifact' : 'Next'}
72✔
294
          </Button>
295
        )}
296
        {onboardingComponent}
297
      </DialogActions>
298
    </Dialog>
299
  );
300
};
301

302
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