• 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

96.42
/frontend/src/js/components/help/Downloads.tsx
1
// Copyright 2022 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 React, { useCallback, useMemo, useState } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16

2✔
17
import { ArrowDropDown, ExpandMore, FileDownloadOutlined as FileDownloadIcon, Launch } from '@mui/icons-material';
2✔
18
import { Accordion, AccordionDetails, AccordionSummary, Chip, Menu, MenuItem, Typography } from '@mui/material';
2✔
19

2✔
20
import CommonDocsLink from '@northern.tech/common-ui/DocsLink';
2✔
21
import Time from '@northern.tech/common-ui/Time';
2✔
22
import storeActions from '@northern.tech/store/actions';
2✔
23
import { canAccess } from '@northern.tech/store/constants';
2✔
24
import { getCurrentSession, getCurrentUser, getIsEnterprise, getTenantCapabilities, getVersionInformation } from '@northern.tech/store/selectors';
2✔
25
import { detectOsIdentifier, toggle } from '@northern.tech/utils/helpers';
2✔
26
import copy from 'copy-to-clipboard';
2✔
27
import Cookies from 'universal-cookie';
2✔
28

2✔
29
import Tracking from '../../tracking';
2✔
30

2✔
31
const { setSnackbar } = storeActions;
6✔
32

2✔
33
const cookies = new Cookies();
6✔
34

2✔
35
const osMap = {
6✔
36
  MacOs: 'darwin',
2✔
37
  Unix: 'linux',
2✔
38
  Linux: 'linux'
2✔
39
};
2✔
40

2✔
41
const architectures = {
6✔
42
  all: 'all',
2✔
43
  amd64: 'amd64',
2✔
44
  arm64: 'arm64',
2✔
45
  armhf: 'armhf'
2✔
46
};
2✔
47

2✔
48
const defaultArchitectures = [architectures.armhf, architectures.arm64, architectures.amd64];
6✔
49
const defaultOSVersions = ['debian+bookworm', 'debian+bullseye', 'ubuntu+focal', 'ubuntu+jammy', 'ubuntu+noble'];
6✔
50

2✔
51
const getVersion = (versions, id) => versions[id] || 'master';
81!
52

2✔
53
const downloadLocations = {
6✔
54
  public: 'https://downloads.mender.io',
2✔
55
  private: 'https://downloads.customer.mender.io/content/hosted'
2✔
56
};
2✔
57

2✔
58
const defaultLocationFormatter = ({ os, tool, versionInfo }) => {
6✔
59
  const { id, location = downloadLocations.public, osList = [], title } = tool;
4✔
60
  let locations = [{ location: `${location}/${id}/${getVersion(versionInfo, id)}/linux/${id}`, title }];
4✔
61
  if (osList.length) {
4!
62
    locations = osList.reduce((accu, supportedOs) => {
4✔
63
      const title = Object.entries(osMap).find(entry => entry[1] === supportedOs)[0];
8✔
64
      accu.push({
6✔
65
        location: `${location}/${id}/${getVersion(versionInfo, id)}/${supportedOs}/${id}`,
2✔
66
        title,
2✔
67
        isUserOs: osMap[os] === supportedOs
2✔
68
      });
2✔
69
      return accu;
6✔
70
    }, []);
2✔
71
  }
2✔
72
  return { locations };
4✔
73
};
2✔
74

2✔
75
/**
2✔
76
 * [MEN-7004]: gateway and monitor packages changed their url patterns.
2✔
77
 * new urls are not like https://downloads.customer.mender.io/content/hosted/repos/debian/pool/main/m/mender-monitor/mender-monitor_1.3.0-1%2Bdebian%2Bbuster_all.deb anymore.
2✔
78
 * and should be like https://downloads.customer.mender.io/content/hosted/mender-monitor/debian/1.3.0/mender-monitor_1.3.0-1%2Bdebian%2Bbuster_all.deb
2✔
79
 */
2✔
80
const locationFormatter = ({ location, id, packageName, packageId, os, arch, versionInfo }) => {
6✔
81
  const isPackageMonitorOrGateway = packageId === menderGateway || packageId === menderMonitor;
72✔
82
  const version = getVersion(versionInfo, id);
72✔
83
  return isPackageMonitorOrGateway
72✔
84
    ? `${location}/${packageId}/debian/${version}/${encodeURIComponent(`${packageId}_${version}-1+${os}_${arch}.deb`)}`
2✔
85
    : `${location}/repos/debian/pool/main/${id[0]}/${packageName || packageId || id}/${encodeURIComponent(`${packageId}_${version}-1+${os}_${arch}.deb`)}`;
2!
86
};
2✔
87

2✔
88
const osArchLocationReducer = ({ archList, location = downloadLocations.public, packageName, packageId, id, osList, versionInfo }) =>
6✔
89
  osList.reduce((accu, os) => {
10✔
90
    const osArchitectureLocations = archList.map(arch => ({
72✔
91
      location: locationFormatter({ location, id, packageName, packageId, os, arch, versionInfo }),
2✔
92
      title: `${os} - ${arch}`
2✔
93
    }));
2✔
94
    accu.push(...osArchitectureLocations);
42✔
95
    return accu;
42✔
96
  }, []);
2✔
97

2✔
98
const multiArchLocationFormatter = ({ tool, versionInfo }) => {
6✔
99
  const { id, packageId: packageName, packageExtras = [] } = tool;
7✔
100
  const packageId = packageName || id;
7✔
101
  const locations = osArchLocationReducer({ ...tool, packageId, versionInfo });
7✔
102
  const extraLocations = packageExtras.reduce((accu, extra) => {
7✔
103
    accu[extra.packageId] = osArchLocationReducer({ ...tool, ...extra, packageName: packageId, versionInfo });
5✔
104
    return accu;
5✔
105
  }, {});
2✔
106
  return { locations, ...extraLocations };
7✔
107
};
2✔
108

2✔
109
const nonOsLocationFormatter = ({ tool, versionInfo }) => {
6✔
110
  const { id, location = downloadLocations.public, title } = tool;
3✔
111
  return {
3✔
112
    locations: [
2✔
113
      {
2✔
114
        location: `${location}/${id}/${getVersion(versionInfo, id)}/${id}-${getVersion(versionInfo, id)}.tar.xz`,
2✔
115
        title
2✔
116
      }
2✔
117
    ]
2✔
118
  };
2✔
119
};
2✔
120

2✔
121
const getAuthHeader = (headerFlag, personalAccessTokens, token) => {
6✔
122
  let header = `${headerFlag} "Cookie: JWT=${token}"`;
2✔
123
  if (personalAccessTokens.length) {
2!
124
    header = `${headerFlag} "Authorization: Bearer ${personalAccessTokens[0]}"`;
2✔
125
  }
2✔
126
  return header;
2✔
127
};
2✔
128

2✔
129
const defaultCurlDownload = ({ location, tokens, token }) => `curl ${getAuthHeader('-H', tokens, token)} -LO ${location}`;
6✔
130

2✔
131
const defaultWgetDownload = ({ location, tokens, token }) => `wget ${getAuthHeader('--header', tokens, token)} ${location}`;
6✔
132

2✔
133
const defaultGitlabJob = ({ location, tokens, token }) => {
6✔
134
  const filename = location.substring(location.lastIndexOf('/') + 1);
2✔
135
  return `
2✔
136
download:mender-tools:
2✔
137
  image: curlimages/curl
2✔
138
  stage: download
2✔
139
  variables:
2✔
140
    ${tokens.length ? `MENDER_TOKEN: ${tokens}` : `MENDER_JWT: ${token}`}
2!
141
  script:
2✔
142
    - if [ -n "$MENDER_TOKEN" ]; then
2✔
143
    - curl -H "Authorization: Bearer $MENDER_TOKEN" -LO ${location}
2✔
144
    - else
2✔
145
    - ${defaultCurlDownload({ location, tokens, token })}
2✔
146
    - fi
2✔
147
  artifacts:
2✔
148
    expire_in: 1w
2✔
149
    paths:
2✔
150
      - ${filename}
2✔
151
`;
2✔
152
};
2✔
153

2✔
154
const menderGateway = 'mender-gateway';
6✔
155
const menderMonitor = 'mender-monitor';
6✔
156

2✔
157
const tools = [
6✔
158
  {
2✔
159
    id: 'mender',
2✔
160
    packageId: 'mender-client',
2✔
161
    packageExtras: [{ packageId: 'mender-client-dev', archList: [architectures.all] }],
2✔
162
    title: 'Mender Client Debian package',
2✔
163
    getLocations: multiArchLocationFormatter,
2✔
164
    canAccess,
2✔
165
    osList: defaultOSVersions,
2✔
166
    archList: defaultArchitectures
2✔
167
  },
2✔
168
  {
2✔
169
    id: 'mender-artifact',
2✔
170
    title: 'Mender Artifact',
2✔
171
    getLocations: defaultLocationFormatter,
2✔
172
    canAccess,
2✔
173
    osList: [osMap.MacOs, osMap.Linux]
2✔
174
  },
2✔
175
  {
2✔
176
    id: 'mender-binary-delta',
2✔
177
    title: 'Mender Binary Delta generator and Update Module',
2✔
178
    getLocations: nonOsLocationFormatter,
2✔
179
    location: downloadLocations.private,
2✔
180
    canAccess: ({ isEnterprise, tenantCapabilities }) => isEnterprise || tenantCapabilities.canDelta
3!
181
  },
2✔
182
  {
2✔
183
    id: 'mender-cli',
2✔
184
    title: 'Mender CLI',
2✔
185
    getLocations: defaultLocationFormatter,
2✔
186
    canAccess,
2✔
187
    osList: [osMap.MacOs, osMap.Linux]
2✔
188
  },
2✔
189
  {
2✔
190
    id: 'mender-configure-module',
2✔
191
    packageId: 'mender-configure',
2✔
192
    packageExtras: [
2✔
193
      { packageId: 'mender-configure-demo', archList: [architectures.all] },
2✔
194
      { packageId: 'mender-configure-timezone', archList: [architectures.all] }
2✔
195
    ],
2✔
196
    title: 'Mender Configure',
2✔
197
    getLocations: multiArchLocationFormatter,
2✔
198
    canAccess: ({ tenantCapabilities }) => tenantCapabilities.hasDeviceConfig,
3✔
199
    osList: defaultOSVersions,
2✔
200
    archList: [architectures.all]
2✔
201
  },
2✔
202
  {
2✔
203
    id: 'mender-connect',
2✔
204
    title: 'Mender Connect',
2✔
205
    getLocations: multiArchLocationFormatter,
2✔
206
    canAccess,
2✔
207
    osList: defaultOSVersions,
2✔
208
    archList: defaultArchitectures
2✔
209
  },
2✔
210
  {
2✔
211
    id: 'mender-convert',
2✔
212
    title: 'Mender Convert',
2✔
213
    getLocations: ({ versionInfo }) => ({
3✔
214
      locations: [
2✔
215
        {
2✔
216
          location: `https://github.com/mendersoftware/mender-convert/archive/refs/tags/${getVersion(versionInfo, 'mender-convert')}.zip`,
2✔
217
          title: 'Mender Convert'
2✔
218
        }
2✔
219
      ]
2✔
220
    }),
2✔
221
    canAccess
2✔
222
  },
2✔
223
  {
2✔
224
    id: menderGateway,
2✔
225
    title: 'Mender Gateway',
2✔
226
    getLocations: multiArchLocationFormatter,
2✔
227
    location: downloadLocations.private,
2✔
228
    canAccess: ({ isEnterprise }) => isEnterprise,
3✔
229
    osList: defaultOSVersions,
2✔
230
    archList: defaultArchitectures
2✔
231
  },
2✔
232
  {
2✔
233
    id: 'monitor-client',
2✔
234
    packageId: menderMonitor,
2✔
235
    title: 'Mender Monitor',
2✔
236
    getLocations: multiArchLocationFormatter,
2✔
237
    location: downloadLocations.private,
2✔
238
    canAccess: ({ tenantCapabilities }) => tenantCapabilities.hasMonitor,
3✔
239
    osList: defaultOSVersions,
2✔
240
    archList: [architectures.all]
2✔
241
  }
2✔
242
];
2✔
243

2✔
244
const copyOptions = [
6✔
245
  { id: 'curl', title: 'Curl command', format: defaultCurlDownload },
2✔
246
  { id: 'wget', title: 'Wget command', format: defaultWgetDownload },
2✔
247
  { id: 'gitlab', title: 'Gitlab Job definition', format: defaultGitlabJob }
2✔
248
];
2✔
249

2✔
250
const DocsLink = ({ title, ...remainder }) => (
6✔
251
  <CommonDocsLink
12✔
252
    {...remainder}
2✔
253
    title={
2✔
254
      <>
2✔
255
        {title} <Launch style={{ verticalAlign: 'text-bottom' }} fontSize="small" />
2✔
256
      </>
2✔
257
    }
2✔
258
  />
2✔
259
);
2✔
260

2✔
261
const DownloadableComponents = ({ locations, onMenuClick, token }) => {
6✔
262
  const onLocationClick = (location, title) => {
14✔
263
    Tracking.event({ category: 'download', action: title });
2✔
264
    cookies.set('JWT', token, { path: '/', maxAge: 60, domain: '.mender.io', sameSite: false });
2✔
265
    const link = document.createElement('a');
2✔
266
    link.href = location;
2✔
267
    link.rel = 'noopener';
2✔
268
    link.target = '_blank';
2✔
269
    document.body.appendChild(link);
2✔
270
    link.click();
2✔
271
    link.remove();
2✔
272
  };
2✔
273

2✔
274
  return locations.map(({ isUserOs, location, title }) => (
14✔
275
    <React.Fragment key={location}>
78✔
276
      <Chip
2✔
277
        avatar={<FileDownloadIcon />}
2✔
278
        className="margin-bottom-small margin-right-small"
2✔
279
        clickable
2✔
UNCOV
280
        onClick={() => onLocationClick(location, title)}
2✔
281
        variant={isUserOs ? 'filled' : 'outlined'}
2✔
282
        onDelete={onMenuClick}
2✔
283
        deleteIcon={<ArrowDropDown value={location} />}
2✔
284
        label={title}
2✔
285
      />
2✔
286
    </React.Fragment>
2✔
287
  ));
2✔
288
};
2✔
289

2✔
290
const DownloadSection = ({ item, isEnterprise, onMenuClick, os, token, versionInformation }) => {
6✔
291
  const [open, setOpen] = useState(false);
11✔
292
  const { id, getLocations, packageId, title } = item;
11✔
293
  const { locations, ...extraLocations } = getLocations({ isEnterprise, tool: item, versionInfo: versionInformation.repos, os });
11✔
294

2✔
295
  return (
11✔
UNCOV
296
    <Accordion className="margin-bottom-small" square expanded={open} onChange={() => setOpen(toggle)}>
2✔
297
      <AccordionSummary expandIcon={<ExpandMore />}>
2✔
298
        <div>
2✔
299
          <Typography variant="subtitle2">{title}</Typography>
2✔
300
          <Typography variant="caption" className="muted">
2✔
301
            Updated: {<Time format="YYYY-MM-DD" value={versionInformation.releaseDate} />}
2✔
302
          </Typography>
2✔
303
        </div>
2✔
304
      </AccordionSummary>
2✔
305
      <AccordionDetails>
2✔
306
        <div>
2✔
307
          <DownloadableComponents locations={locations} onMenuClick={onMenuClick} token={token} />
2✔
308
          {Object.entries(extraLocations).map(([key, locations]) => (
2✔
309
            <React.Fragment key={key}>
5✔
310
              <h5 className="margin-bottom-none muted">{key}</h5>
2✔
311
              <DownloadableComponents locations={locations} onMenuClick={onMenuClick} token={token} />
2✔
312
            </React.Fragment>
2✔
313
          ))}
2✔
314
        </div>
2✔
315
        <DocsLink path={`release-information/release-notes-changelog/${packageId || id}`} title="Changelog" />
2✔
316
      </AccordionDetails>
2✔
317
    </Accordion>
2✔
318
  );
2✔
319
};
2✔
320

2✔
321
export const Downloads = () => {
6✔
322
  const [anchorEl, setAnchorEl] = useState();
3✔
323
  const [currentLocation, setCurrentLocation] = useState('');
3✔
324
  const [os] = useState(detectOsIdentifier());
3✔
325
  const dispatch = useDispatch();
3✔
326
  const { tokens = [] } = useSelector(getCurrentUser);
3✔
327
  const { token } = useSelector(getCurrentSession);
3✔
328
  const isEnterprise = useSelector(getIsEnterprise);
3✔
329
  const tenantCapabilities = useSelector(getTenantCapabilities);
3✔
330
  const { latestRelease: versions = { repos: {}, releaseDate: '' } } = useSelector(getVersionInformation);
3✔
331

2✔
332
  const availableTools = useMemo(
3✔
333
    () =>
2✔
334
      tools.reduce((accu, tool) => {
3✔
335
        if (!tool.canAccess({ isEnterprise, tenantCapabilities })) {
11!
336
          return accu;
2✔
337
        }
2✔
338
        accu.push(tool);
11✔
339
        return accu;
11✔
340
      }, []),
2✔
341
    [isEnterprise, tenantCapabilities]
2✔
342
  );
2✔
343

2✔
344
  const handleToggle = event => {
3✔
345
    setAnchorEl(current => (current ? null : event?.currentTarget.parentElement));
2!
346
    const location = event?.target.getAttribute('value') || '';
2!
347
    setCurrentLocation(location);
2✔
348
  };
2✔
349

2✔
350
  const handleSelection = useCallback(
3✔
351
    event => {
2✔
352
      const value = event?.target.getAttribute('value') || 'curl';
2!
353
      const option = copyOptions.find(item => item.id === value);
2✔
354
      copy(option.format({ location: currentLocation, tokens, token }));
2✔
355
      dispatch(setSnackbar('Copied to clipboard'));
2✔
356
    },
2✔
357
    [currentLocation, dispatch, tokens, token]
2✔
358
  );
2✔
359

2✔
360
  return (
3✔
361
    <div>
2✔
362
      <h2>Downloads</h2>
2✔
363
      <p>To get the most out of Mender, download the tools listed below.</p>
2✔
364
      {availableTools.map(tool => (
2✔
365
        <DownloadSection key={tool.id} item={tool} isEnterprise={isEnterprise} onMenuClick={handleToggle} os={os} token={token} versionInformation={versions} />
11✔
366
      ))}
2✔
367
      <Menu id="download-options-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleToggle} variant="menu">
2✔
368
        {copyOptions.map(option => (
2✔
369
          <MenuItem key={option.id} value={option.id} onClick={handleSelection}>
5✔
370
            Copy {option.title}
2✔
371
          </MenuItem>
2✔
372
        ))}
2✔
373
      </Menu>
2✔
374
      <p>
2✔
375
        To learn more about the tools availabe for Mender, read the <DocsLink path="downloads" title="Downloads section in our documentation" />.
2✔
376
      </p>
2✔
377
    </div>
2✔
378
  );
2✔
379
};
2✔
380

2✔
381
export default Downloads;
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