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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

76.42
/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 { canAccess } from '../../constants/appConstants';
25
import { detectOsIdentifier, toggle } from '../../helpers';
26
import { getCurrentSession, getCurrentUser, getIsEnterprise, getTenantCapabilities, getVersionInformation } from '../../selectors';
27
import Tracking from '../../tracking';
28
import CommonDocsLink from '../common/docslink';
29
import Time from '../common/time';
30

31
const cookies = new Cookies();
3✔
32

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

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

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

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

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

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

73
/**
74
 * [MEN-7004]: gateway and monitor packages changed their url patterns.
75
 * 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.
76
 * 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
77
 */
78
const locationFormatter = ({ location, id, packageName, packageId, os, arch, versionInfo }) => {
3✔
79
  const isPackageMonitorOrGateway = packageId === menderGateway || packageId === menderMonitor;
56✔
80
  const version = getVersion(versionInfo, id);
56✔
81
  return isPackageMonitorOrGateway
56✔
82
    ? `${location}/${packageId}/debian/${version}/${encodeURIComponent(`${packageId}_${version}-1+${os}_${arch}.deb`)}`
83
    : `${location}/repos/debian/pool/main/${id[0]}/${packageName || packageId || id}/${encodeURIComponent(`${packageId}_${version}-1+${os}_${arch}.deb`)}`;
68!
84
};
85

86
const osArchLocationReducer = ({ archList, location = downloadLocations.public, packageName, packageId, id, osList, versionInfo }) =>
3✔
87
  osList.reduce((accu, os) => {
8✔
88
    const osArchitectureLocations = archList.map(arch => ({
56✔
89
      location: locationFormatter({ location, id, packageName, packageId, os, arch, versionInfo }),
90
      title: `${os} - ${arch}`
91
    }));
92
    accu.push(...osArchitectureLocations);
32✔
93
    return accu;
32✔
94
  }, []);
95

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

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

119
const getAuthHeader = (headerFlag, personalAccessTokens, token) => {
3✔
UNCOV
120
  let header = `${headerFlag} "Cookie: JWT=${token}"`;
×
UNCOV
121
  if (personalAccessTokens.length) {
×
UNCOV
122
    header = `${headerFlag} "Authorization: Bearer ${personalAccessTokens[0]}"`;
×
123
  }
UNCOV
124
  return header;
×
125
};
126

127
const defaultCurlDownload = ({ location, tokens, token }) => `curl ${getAuthHeader('-H', tokens, token)} -LO ${location}`;
3✔
128

129
const defaultWgetDownload = ({ location, tokens, token }) => `wget ${getAuthHeader('--header', tokens, token)} ${location}`;
3✔
130

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

152
const menderGateway = 'mender-gateway';
3✔
153
const menderMonitor = 'mender-monitor';
3✔
154

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

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

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

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

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

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

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

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

330
  const availableTools = useMemo(
1✔
331
    () =>
332
      tools.reduce((accu, tool) => {
1✔
333
        if (!tool.canAccess({ isEnterprise, tenantCapabilities })) {
9!
UNCOV
334
          return accu;
×
335
        }
336
        accu.push(tool);
9✔
337
        return accu;
9✔
338
      }, []),
339
    [isEnterprise, tenantCapabilities]
340
  );
341

342
  const handleToggle = event => {
1✔
UNCOV
343
    setAnchorEl(current => (current ? null : event?.currentTarget.parentElement));
×
UNCOV
344
    const location = event?.target.getAttribute('value') || '';
×
UNCOV
345
    setCurrentLocation(location);
×
346
  };
347

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

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

379
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