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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

66.91
/src/js/components/help/downloads.js
1
// Copyright 2022 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, useMemo, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16

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

20
import copy from 'copy-to-clipboard';
21
import Cookies from 'universal-cookie';
22

23
import { setSnackbar } from '../../actions/appActions';
24
import { getToken } from '../../auth';
25
import { canAccess } from '../../constants/appConstants';
26
import { detectOsIdentifier, toggle } from '../../helpers';
27
import { getCurrentUser, getDocsVersion, getIsEnterprise, getTenantCapabilities, getVersionInformation } from '../../selectors';
28
import Tracking from '../../tracking';
29
import CommonDocsLink from '../common/docslink';
30
import Time from '../common/time';
31

32
const cookies = new Cookies();
4✔
33

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

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

47
const defaultArchitectures = [architectures.armhf, architectures.arm64, architectures.amd64];
4✔
48
const defaultOSVersions = ['debian+buster', 'debian+bullseye', 'ubuntu+bionic', 'ubuntu+focal'];
4✔
49

50
const getVersion = (versions, id) => versions[id] || 'master';
65!
51

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

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

74
const osArchLocationReducer = ({ archList, location = downloadLocations.public, packageName, packageId, id, osList, versionInfo }) =>
4✔
75
  osList.reduce((accu, os) => {
8✔
76
    const osArchitectureLocations = archList.map(arch => ({
56✔
77
      location: `${location}/repos/debian/pool/main/${id[0]}/${packageName || packageId || id}/${encodeURIComponent(
100!
78
        `${packageId}_${getVersion(versionInfo, id)}-1+${os}_${arch}.deb`
79
      )}`,
80
      title: `${os} - ${arch}`
81
    }));
82
    accu.push(...osArchitectureLocations);
32✔
83
    return accu;
32✔
84
  }, []);
85

86
const multiArchLocationFormatter = ({ tool, versionInfo }) => {
4✔
87
  const { id, packageId: packageName, packageExtras = [] } = tool;
5✔
88
  const packageId = packageName || id;
5✔
89
  const locations = osArchLocationReducer({ ...tool, packageId, versionInfo });
5✔
90
  const extraLocations = packageExtras.reduce((accu, extra) => {
5✔
91
    accu[extra.packageId] = osArchLocationReducer({ ...tool, ...extra, packageName: packageId, versionInfo });
3✔
92
    return accu;
3✔
93
  }, {});
94
  return { locations, ...extraLocations };
5✔
95
};
96

97
const nonOsLocationFormatter = ({ tool, versionInfo }) => {
4✔
98
  const { id, location = downloadLocations.public, title } = tool;
1!
99
  return {
1✔
100
    locations: [
101
      {
102
        location: `${location}/${id}/${getVersion(versionInfo, id)}/${id}-${getVersion(versionInfo, id)}.tar.xz`,
103
        title
104
      }
105
    ]
106
  };
107
};
108

109
const getAuthHeader = (headerFlag, tokens) => {
4✔
110
  let header = `${headerFlag} "Cookie: JWT=${getToken()}"`;
×
111
  if (tokens.length) {
×
112
    header = `${headerFlag} "Authorization: Bearer ${tokens[0]}"`;
×
113
  }
114
  return header;
×
115
};
116

117
const defaultCurlDownload = ({ location, tokens }) => `curl ${getAuthHeader('-H', tokens)} -LO ${location}`;
4✔
118

119
const defaultWgetDownload = ({ location, tokens }) => `wget ${getAuthHeader('--header', tokens)} ${location}`;
4✔
120

121
const defaultGitlabJob = ({ location, tokens }) => {
4✔
122
  const filename = location.substring(location.lastIndexOf('/') + 1);
×
123
  return `
×
124
download:mender-tools:
125
  image: curlimages/curl
126
  stage: download
127
  variables:
128
    ${tokens.length ? `MENDER_TOKEN: ${tokens}` : `MENDER_JWT: ${getToken()}`}
×
129
  script:
130
    - if [ -n "$MENDER_TOKEN" ]; then
131
    - curl -H "Authorization: Bearer $MENDER_TOKEN" -LO ${location}
132
    - else
133
    - ${defaultCurlDownload({ location, tokens })}
134
    - fi
135
  artifacts:
136
    expire_in: 1w
137
    paths:
138
      - ${filename}
139
`;
140
};
141

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

229
const copyOptions = [
4✔
230
  { id: 'curl', title: 'Curl command', format: defaultCurlDownload },
231
  { id: 'wget', title: 'Wget command', format: defaultWgetDownload },
232
  { id: 'gitlab', title: 'Gitlab Job definition', format: defaultGitlabJob }
233
];
234

235
const DocsLink = ({ title, ...remainder }) => (
4✔
236
  <CommonDocsLink
10✔
237
    {...remainder}
238
    title={
239
      <>
240
        {title} <Launch style={{ verticalAlign: 'text-bottom' }} fontSize="small" />
241
      </>
242
    }
243
  />
244
);
245

246
const DownloadableComponents = ({ locations, onMenuClick }) => {
4✔
247
  const onLocationClick = (location, title) => {
12✔
248
    console.log(location);
×
249
    Tracking.event({ category: 'download', action: title });
×
250
    cookies.set('JWT', getToken(), { path: '/', maxAge: 60, domain: '.mender.io', sameSite: false });
×
251
    const link = document.createElement('a');
×
252
    link.href = location;
×
253
    link.rel = 'noopener noreferrer';
×
254
    link.target = '_blank';
×
255
    document.body.appendChild(link);
×
256
    link.click();
×
257
    link.remove();
×
258
  };
259

260
  return locations.map(({ isUserOs, location, title }) => (
12✔
261
    <React.Fragment key={location}>
62✔
262
      <Chip
263
        avatar={<FileDownloadIcon />}
264
        className="margin-bottom-small margin-right-small"
265
        clickable
266
        onClick={() => onLocationClick(location, title)}
×
267
        variant={isUserOs ? 'filled' : 'outlined'}
62✔
268
        onDelete={onMenuClick}
269
        deleteIcon={<ArrowDropDown value={location} />}
270
        label={title}
271
      />
272
    </React.Fragment>
273
  ));
274
};
275

276
const DownloadSection = ({ docsVersion, item, isEnterprise, onMenuClick, os, versionInformation }) => {
4✔
277
  const [open, setOpen] = useState(false);
9✔
278
  const { id, getLocations, packageId, title } = item;
9✔
279
  const { locations, ...extraLocations } = getLocations({ isEnterprise, tool: item, versionInfo: versionInformation.repos, os });
9✔
280

281
  return (
9✔
282
    <Accordion className="margin-bottom-small" square expanded={open} onChange={() => setOpen(toggle)}>
×
283
      <AccordionSummary expandIcon={<ExpandMore />}>
284
        <div>
285
          <Typography variant="subtitle2">{title}</Typography>
286
          <Typography variant="caption" className="muted">
287
            Updated: {<Time format="YYYY-MM-DD" value={versionInformation.releaseDate} />}
288
          </Typography>
289
        </div>
290
      </AccordionSummary>
291
      <AccordionDetails>
292
        <div>
293
          <DownloadableComponents locations={locations} onMenuClick={onMenuClick} />
294
          {Object.entries(extraLocations).map(([key, locations]) => (
295
            <React.Fragment key={key}>
3✔
296
              <h5 className="margin-bottom-none muted">{key}</h5>
297
              <DownloadableComponents locations={locations} onMenuClick={onMenuClick} />
298
            </React.Fragment>
299
          ))}
300
        </div>
301
        <DocsLink docsVersion={docsVersion} path={`release-information/release-notes-changelog/${packageId || id}`} title="Changelog" />
15✔
302
      </AccordionDetails>
303
    </Accordion>
304
  );
305
};
306

307
export const Downloads = () => {
4✔
308
  const [anchorEl, setAnchorEl] = useState();
1✔
309
  const [currentLocation, setCurrentLocation] = useState('');
1✔
310
  const [os] = useState(detectOsIdentifier());
1✔
311
  const dispatch = useDispatch();
1✔
312
  const { tokens = [] } = useSelector(getCurrentUser);
1✔
313
  const docsVersion = useSelector(getDocsVersion);
1✔
314
  const isEnterprise = useSelector(getIsEnterprise);
1✔
315
  const tenantCapabilities = useSelector(getTenantCapabilities);
1✔
316
  const { latestRelease: versions = { repos: {}, releaseDate: '' } } = useSelector(getVersionInformation);
1!
317

318
  const availableTools = useMemo(
1✔
319
    () =>
320
      tools.reduce((accu, tool) => {
1✔
321
        if (!tool.canAccess({ isEnterprise, tenantCapabilities })) {
9!
322
          return accu;
×
323
        }
324
        accu.push(tool);
9✔
325
        return accu;
9✔
326
      }, []),
327
    [isEnterprise, tenantCapabilities]
328
  );
329

330
  const handleToggle = event => {
1✔
331
    setAnchorEl(current => (current ? null : event?.currentTarget.parentElement));
×
332
    const location = event?.target.getAttribute('value') || '';
×
333
    console.log(location);
×
334
    setCurrentLocation(location);
×
335
  };
336

337
  const handleSelection = useCallback(
1✔
338
    event => {
339
      const value = event?.target.getAttribute('value') || 'curl';
×
340
      const option = copyOptions.find(item => item.id === value);
×
341
      copy(option.format({ location: currentLocation, tokens }));
×
342
      dispatch(setSnackbar('Copied to clipboard'));
×
343
    },
344
    [currentLocation, tokens]
345
  );
346

347
  return (
1✔
348
    <div>
349
      <h2>Downloads</h2>
350
      <p>To get the most out of Mender, download the tools listed below.</p>
351
      {availableTools.map(tool => (
352
        <DownloadSection
9✔
353
          docsVersion={docsVersion}
354
          key={tool.id}
355
          item={tool}
356
          isEnterprise={isEnterprise}
357
          onMenuClick={handleToggle}
358
          os={os}
359
          versionInformation={versions}
360
        />
361
      ))}
362
      <Menu id="download-options-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleToggle} variant="menu">
363
        {copyOptions.map(option => (
364
          <MenuItem key={option.id} value={option.id} onClick={handleSelection}>
3✔
365
            Copy {option.title}
366
          </MenuItem>
367
        ))}
368
      </Menu>
369
      <p>
370
        To learn more about the tools availabe for Mender, read the{' '}
371
        <DocsLink docsVersion={docsVersion} path="downloads" title="Downloads section in our documentation" />.
372
      </p>
373
    </div>
374
  );
375
};
376

377
export default Downloads;
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