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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

98.27
/frontend/src/js/components/releases/dialogs/AddArtifact.tsx
1
// Copyright 2020 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
2✔
15
import Dropzone from 'react-dropzone';
2✔
16
import { useDispatch, useSelector } from 'react-redux';
2✔
17

2✔
18
import { CloudUpload } from '@mui/icons-material';
2✔
19
import { Button, DialogActions, DialogContent } from '@mui/material';
2✔
20
import { makeStyles } from 'tss-react/mui';
2✔
21

2✔
22
import { InputErrorNotification } from '@northern.tech/common-ui/InputErrorNotification';
2✔
23
import { BaseDialog } from '@northern.tech/common-ui/dialogs/BaseDialog';
2✔
24
import storeActions from '@northern.tech/store/actions';
2✔
25
import { getDeviceTypes } from '@northern.tech/store/selectors';
2✔
26
import { createArtifact, uploadArtifact } from '@northern.tech/store/thunks';
2✔
27
import { unionizeStrings } from '@northern.tech/utils/helpers';
2✔
28
import { useWindowSize } from '@northern.tech/utils/resizehook';
2✔
29

2✔
30
import Tracking from '../../../tracking';
2✔
31
import ArtifactInformationForm from './ArtifactInformationForm';
2✔
32
import ArtifactUploadConfirmation from './ArtifactUpload';
2✔
33

2✔
34
const { setSnackbar } = storeActions;
7✔
35

2✔
36
type SupportedUploadTypes = 'mender' | 'singleFile';
2✔
37

2✔
38
type Update = {
2✔
39
  customDeviceTypes?: string;
2✔
40
  destination?: string;
2✔
41
  file?: File;
2✔
42
  fileSystem?: string;
2✔
43
  finalStep: boolean;
2✔
44
  isValid: boolean;
2✔
45
  isValidDestination?: boolean;
2✔
46
  name: string;
2✔
47
  selectedDeviceTypes?: string[];
2✔
48
  softwareName?: string;
2✔
49
  softwareVersion?: string;
2✔
50
  type: SupportedUploadTypes;
2✔
51
};
2✔
52

2✔
53
type UploadType = {
2✔
54
  component: ReactNode;
2✔
55
  key: SupportedUploadTypes;
2✔
56
};
2✔
57

2✔
58
type UploadTypes = Record<string, UploadType>;
2✔
59

2✔
60
const useStyles = makeStyles()(theme => ({
7✔
61
  dropzone: { ['&.dropzone']: { padding: theme.spacing(4) } },
2✔
62
  fileInfo: {
2✔
63
    alignItems: 'center',
2✔
64
    columnGap: theme.spacing(4),
2✔
65
    display: 'grid',
2✔
66
    gridTemplateColumns: 'max-content 1fr max-content max-content',
2✔
67
    marginBottom: theme.spacing(2),
2✔
68
    marginRight: theme.spacing(4)
2✔
69
  },
2✔
70
  fileSizeWrapper: { marginTop: 5 }
2✔
71
}));
2✔
72

2✔
73
const uploadTypes: UploadTypes = {
7✔
74
  mender: {
2✔
75
    key: 'mender',
2✔
76
    component: ArtifactUploadConfirmation
2✔
77
  },
2✔
78
  singleFile: {
2✔
79
    key: 'singleFile',
2✔
80
    component: ArtifactInformationForm
2✔
81
  }
2✔
82
};
2✔
83

2✔
84
const commonExtensions = ['zip', 'txt', 'tar', 'html', 'tar.gzip', 'gzip'];
7✔
85
const shortenFileName = name => {
7✔
86
  const extension = commonExtensions.find(extension => name.endsWith(extension));
10✔
87
  if (extension) {
4✔
88
    const dotIndex = name.lastIndexOf(`.${extension}`);
3✔
89
    return name.substring(0, dotIndex);
3✔
90
  }
2✔
91
  return name;
3✔
92
};
2✔
93

2✔
94
const singleFileLimit = 256 * 1024 ** 2; //256MiB
7✔
95
const menderFileLimit = 10 * 1024 ** 3; //10GiB
7✔
96
const reFilename = new RegExp(/^[\w\-.,]+$/);
7✔
97

2✔
98
const isMenderArtifact = (name: string): boolean => name.endsWith('.mender');
8✔
99

2✔
100
const validateFile = ({ name, size }: File): string => {
7✔
101
  if (!reFilename.test(name)) {
4!
102
    return 'Only letters, digits and characters in the set ".,_-" are allowed in the filename.';
2✔
103
  } else if (isMenderArtifact(name) && size > menderFileLimit) {
4!
104
    return 'Only artifacts smaller than 10GiB are supported.';
2✔
105
  } else if (!isMenderArtifact(name) && size > singleFileLimit) {
4!
106
    return 'Artifact generation is only supported for files smaller than 256MiB.';
2✔
107
  }
2✔
108
  return '';
4✔
109
};
2✔
110

2✔
111
export const ArtifactUpload = ({ updateCreation }: { updateCreation: (some: Partial<Update>) => void }) => {
7✔
112
  const onboardingAnchor = useRef();
5✔
113
  const { classes } = useStyles();
5✔
114
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2✔
115
  const size = useWindowSize();
5✔
116
  const [errorMessage, setErrorMessage] = useState<string>('');
5✔
117

2✔
118
  const onDrop = acceptedFiles => {
5✔
119
    const emptyFileInfo = { file: undefined, name: '', type: uploadTypes.mender.key };
4✔
120
    if (acceptedFiles.length === 1) {
4!
121
      const validationError = validateFile(acceptedFiles[0]);
4✔
122
      if (validationError) {
4!
123
        updateCreation(emptyFileInfo);
2✔
124
        setErrorMessage(validationError);
2✔
125
      } else {
2✔
126
        const { name } = acceptedFiles[0];
4✔
127
        updateCreation({
4✔
128
          file: acceptedFiles[0],
2✔
129
          name: shortenFileName(name),
2✔
130
          type: isMenderArtifact(name) ? uploadTypes.mender.key : uploadTypes.singleFile.key
2✔
131
        });
2✔
132
      }
2✔
133
    } else {
2✔
134
      updateCreation(emptyFileInfo);
2✔
135
      setErrorMessage('The selected file is not supported.');
2✔
136
    }
2✔
137
  };
2✔
138

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

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

2✔
179
  const deviceTypes = useSelector(getDeviceTypes);
90✔
180
  const dispatch = useDispatch();
90✔
181

2✔
182
  const onCreateArtifact = useCallback((meta, file) => dispatch(createArtifact({ meta, file })), [dispatch]);
90✔
183
  const onSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
90✔
184
  const onUploadArtifact = useCallback((meta, file) => dispatch(uploadArtifact({ meta, file })), [dispatch]);
90✔
185

2✔
186
  useEffect(() => {
90✔
187
    setCreation(current => ({ ...current, file: selectedFile }));
5✔
188
  }, [selectedFile]);
2✔
189

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

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

2✔
219
  const onUpdateCreation = useCallback(update => setCreation(current => ({ ...current, ...update })), []);
90✔
220

2✔
221
  const onNextClick = useCallback(() => {
90✔
222
    onUpdateCreation({ isValid: false });
3✔
223
    setActiveStep(activeStep + 1);
3✔
224
  }, [activeStep, onUpdateCreation]);
2✔
225

2✔
226
  const onRemove = () => onUpdateCreation({ file: undefined, isValid: false });
90✔
227

2✔
228
  const { file, finalStep, isValid, type } = creation;
90✔
229
  const { component: ComponentToShow } = uploadTypes[type];
90✔
230
  const commonProps = { releases, setSnackbar: onSetSnackbar, updateCreation: onUpdateCreation };
90✔
231

2✔
232
  return (
90✔
233
    <BaseDialog open title="Upload an Artifact" fullWidth maxWidth="sm" onClose={onCancel}>
2✔
234
      <DialogContent className="dialog-content margin-top margin-left margin-right margin-bottom">
2✔
235
        {!file ? (
2✔
236
          <ArtifactUpload updateCreation={onUpdateCreation} />
2✔
237
        ) : (
2✔
238
          <ComponentToShow {...commonProps} activeStep={activeStep} creation={creation} deviceTypes={deviceTypes} onRemove={onRemove} />
2✔
239
        )}
2✔
240
      </DialogContent>
2✔
241
      <DialogActions>
2✔
242
        <Button onClick={onCancel}>Cancel</Button>
2✔
UNCOV
243
        {!!activeStep && <Button onClick={() => setActiveStep(activeStep - 1)}>Back</Button>}
2✔
244
        <div style={{ flexGrow: 1 }} />
2✔
245
        {file && (
2✔
246
          <Button variant="contained" disabled={!isValid} onClick={() => (finalStep ? onUpload() : onNextClick())}>
5✔
247
            {finalStep ? 'Upload artifact' : 'Next'}
2✔
248
          </Button>
2✔
249
        )}
2✔
250
      </DialogActions>
2✔
251
    </BaseDialog>
2✔
252
  );
2✔
253
};
2✔
254

2✔
255
export default AddArtifactDialog;
2✔
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