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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

65.96
/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 { connect } 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 { detectOsIdentifier, toggle } from '../../helpers';
26
import { getCurrentUser, getDocsVersion, getIsEnterprise, getTenantCapabilities } from '../../selectors';
27
import Tracking from '../../tracking';
28
import Time from '../common/time';
29

30
const cookies = new Cookies();
4✔
31

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

233
const DocsLink = ({ className = '', docsVersion, path, title }) => (
4✔
234
  <a className={className} href={`https://docs.mender.io/${docsVersion}${path}`} target="_blank" rel="noopener noreferrer">
10✔
235
    {title} <Launch style={{ verticalAlign: 'text-bottom' }} fontSize="small" />
236
  </a>
237
);
238

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

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

269
const DownloadSection = ({ docsVersion, item, isEnterprise, onMenuClick, os, versionInformation }) => {
4✔
270
  const [open, setOpen] = useState(false);
9✔
271
  const { id, getLocations, packageId, title } = item;
9✔
272
  const { locations, ...extraLocations } = getLocations({ isEnterprise, tool: item, versionInfo: versionInformation.repos, os });
9✔
273

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

300
export const Downloads = ({ docsVersion = '', isEnterprise, setSnackbar, tenantCapabilities, tokens = [], versions = { repos: {}, releaseDate: '' } }) => {
4!
301
  const [anchorEl, setAnchorEl] = useState();
1✔
302
  const [currentLocation, setCurrentLocation] = useState('');
1✔
303
  const [os] = useState(detectOsIdentifier());
1✔
304

305
  const availableTools = useMemo(
1✔
306
    () =>
307
      tools.reduce((accu, tool) => {
1✔
308
        if (!tool.canAccess({ isEnterprise, tenantCapabilities })) {
9!
309
          return accu;
×
310
        }
311
        accu.push(tool);
9✔
312
        return accu;
9✔
313
      }, []),
314
    [isEnterprise, tenantCapabilities]
315
  );
316

317
  const handleToggle = event => {
1✔
318
    setAnchorEl(current => (current ? null : event?.currentTarget.parentElement));
×
319
    const location = event?.target.getAttribute('value') || '';
×
320
    console.log(location);
×
321
    setCurrentLocation(location);
×
322
  };
323

324
  const handleSelection = useCallback(
1✔
325
    event => {
326
      const value = event?.target.getAttribute('value') || 'curl';
×
327
      const option = copyOptions.find(item => item.id === value);
×
328
      copy(option.format({ location: currentLocation, tokens }));
×
329
      setSnackbar('Copied to clipboard');
×
330
    },
331
    [currentLocation, tokens]
332
  );
333

334
  return (
1✔
335
    <div>
336
      <h2>Downloads</h2>
337
      <p>To get the most out of Mender, download the tools listed below.</p>
338
      {availableTools.map(tool => (
339
        <DownloadSection
9✔
340
          docsVersion={docsVersion}
341
          key={tool.id}
342
          item={tool}
343
          isEnterprise={isEnterprise}
344
          onMenuClick={handleToggle}
345
          os={os}
346
          versionInformation={versions}
347
        />
348
      ))}
349
      <Menu id="download-options-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleToggle} variant="menu">
350
        {copyOptions.map(option => (
351
          <MenuItem key={option.id} value={option.id} onClick={handleSelection}>
3✔
352
            Copy {option.title}
353
          </MenuItem>
354
        ))}
355
      </Menu>
356
      <p>
357
        To learn more about the tools availabe for Mender, read the{' '}
358
        <DocsLink docsVersion={docsVersion} path="downloads" title="Downloads section in our documentation" />.
359
      </p>
360
    </div>
361
  );
362
};
363

364
const actionCreators = { setSnackbar };
4✔
365

366
const mapStateToProps = state => {
4✔
367
  const { tokens } = getCurrentUser(state);
×
368
  return {
×
369
    docsVersion: getDocsVersion(state),
370
    isEnterprise: getIsEnterprise(state),
371
    isHosted: state.app.features.isHosted,
372
    tenantCapabilities: getTenantCapabilities(state),
373
    tokens,
374
    versions: state.app.versionInformation.latestRelease
375
  };
376
};
377

378
export default connect(mapStateToProps, actionCreators)(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