• 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

68.75
/src/js/components/devices/device-details/authsets/authsetlistitem.js
1
// Copyright 2020 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, { useEffect, useState } from 'react';
15
import CopyToClipboard from 'react-copy-to-clipboard';
16

17
import { FileCopy as CopyPasteIcon } from '@mui/icons-material';
18
// material ui
19
import { Accordion, AccordionActions, AccordionDetails, AccordionSummary, Button, Chip, Divider, IconButton } from '@mui/material';
20

21
import { TIMEOUTS } from '../../../../constants/appConstants';
22
import { DEVICE_DISMISSAL_STATE, DEVICE_STATES } from '../../../../constants/deviceConstants';
23
import { formatTime } from '../../../../helpers';
24
import Loader from '../../../common/loader';
25
import Time from '../../../common/time';
26

27
const padder = <div key="padder" style={{ flexGrow: 1 }}></div>;
13✔
28

29
const getDismissalConfirmation = (device, authset) => {
13✔
30
  switch (authset.status) {
6!
31
    case DEVICE_STATES.preauth:
32
      return 'The device authentication set will be removed from the preauthorization list.';
1✔
33
    case DEVICE_STATES.accepted:
34
      if (device.auth_sets.length > 1) {
2✔
35
        // if there are other authsets, device will still be in UI
36
        return 'The device with this public key will no longer be accepted, and this authorization request will be removed from the UI.';
1✔
37
      } else {
38
        return 'The device with this public key will no longer be accepted, and will be removed from the UI. If it makes another request in the future, it will show again as pending for you to accept or reject at that time.';
1✔
39
      }
40
    case DEVICE_STATES.pending: {
41
      const message = 'You can dismiss this authentication request for now.';
2✔
42
      if (device.auth_sets.length > 1) {
2✔
43
        // it has other authsets
44
        return `${message} This will remove this request from the UI, but won’t affect the device.`;
1✔
45
      }
46
      return `${message} The device will be removed from the UI, but if the same device asks for authentication again in the future, it will show again as pending.`;
1✔
47
    }
48
    case DEVICE_STATES.rejected:
49
      return 'This request will be removed from the UI, but if the device asks for authentication again in the future, it will show as pending for you to accept or reject it at that time.';
1✔
50
    default:
UNCOV
51
      break;
×
52
  }
UNCOV
53
  return '';
×
54
};
55

56
export const getConfirmationMessage = (status, device, authset) => {
13✔
57
  let message = '';
11✔
58
  switch (status) {
11!
59
    case DEVICE_STATES.accepted:
60
      message = 'By accepting, the device with this identity data and public key will be granted authentication by the server.';
3✔
61
      if (device.status === DEVICE_STATES.accepted) {
3✔
62
        // if device already accepted, and you are accepting a different authset:
63
        return `${message} The previously accepted public key will be rejected automatically in favor of this new key.`;
2✔
64
      }
65
      return message;
1✔
66
    case DEVICE_STATES.rejected:
67
      message = 'The device with this identity data and public key will be rejected, and blocked from communicating with the Mender server.';
2✔
68
      if (device.status === DEVICE_STATES.accepted && authset.status !== DEVICE_STATES.accepted) {
2✔
69
        // if device is accepted but you are rejecting an authset that is not accepted, device status is unaffected:
70
        return `${message} Rejecting this request will not affect the device status as it is using a different key. `;
1✔
71
      }
72
      return message;
1✔
73
    case DEVICE_DISMISSAL_STATE:
74
      message = getDismissalConfirmation(device, authset);
6✔
75
      break;
6✔
76
    default:
UNCOV
77
      break;
×
78
  }
79
  return message;
6✔
80
};
81

82
const LF = '\n';
13✔
83

84
const AuthSetStatus = ({ authset, device }) => {
13✔
85
  if (authset.status === device.status) {
32✔
86
    return <div className="capitalized">Active</div>;
12✔
87
  }
88
  if (authset.status === DEVICE_STATES.pending) {
20✔
89
    return <Chip size="small" label="new" color="primary" style={{ justifySelf: 'flex-start' }} />;
8✔
90
  }
91
  return <div />;
12✔
92
};
93

94
const ActionButtons = ({ authset, confirmMessage, newStatus, limitMaxed, onAcceptClick, onDismissClick, onRequestConfirm, userCapabilities }) => {
13✔
95
  const { canManageDevices } = userCapabilities;
32✔
96
  if (!canManageDevices) {
32!
UNCOV
97
    return null;
×
98
  }
99
  return confirmMessage.length ? (
32!
100
    <div>Set to: {newStatus}?</div>
101
  ) : (
102
    <div className="action-buttons flexbox">
103
      {authset.status !== DEVICE_STATES.accepted && authset.status !== DEVICE_STATES.preauth && !limitMaxed ? (
104✔
104
        <a onClick={onAcceptClick}>Accept</a>
105
      ) : (
106
        <div>Accept</div>
107
      )}
108
      {authset.status !== DEVICE_STATES.rejected && authset.status !== DEVICE_STATES.preauth ? (
90✔
UNCOV
109
        <a onClick={() => onRequestConfirm(DEVICE_STATES.rejected)}>Reject</a>
×
110
      ) : (
111
        <div>Reject</div>
112
      )}
113
      <a onClick={onDismissClick}>Dismiss</a>
114
    </div>
115
  );
116
};
117

118
const AuthsetListItem = ({ authset, classes, columns, confirm, device, isExpanded, limitMaxed, loading, onExpand, total, userCapabilities }) => {
13✔
119
  const [showKey, setShowKey] = useState(false);
32✔
120
  const [confirmMessage, setConfirmMessage] = useState('');
32✔
121
  const [newStatus, setNewStatus] = useState('');
32✔
122
  const [copied, setCopied] = useState(false);
32✔
123
  const [keyHash, setKeyHash] = useState('');
32✔
124
  const [endKey, setEndKey] = useState('');
32✔
125

126
  useEffect(() => {
32✔
127
    if (!isExpanded) {
16✔
128
      setShowKey(false);
15✔
129
      setConfirmMessage('');
15✔
130
      setNewStatus('');
15✔
131
    }
132
  }, [isExpanded]);
133

134
  useEffect(() => {
32✔
135
    const data = new TextEncoder().encode(authset.pubkey);
16✔
136
    if (crypto?.subtle) {
16!
UNCOV
137
      crypto.subtle.digest('SHA-256', data).then(hashBuffer => {
×
UNCOV
138
        const hashHex = Array.from(new Uint8Array(hashBuffer))
×
UNCOV
139
          .map(b => b.toString(16).padStart(2, '0'))
×
140
          .join('');
UNCOV
141
        setKeyHash(hashHex);
×
142
      });
143
    } else {
144
      setKeyHash('SHA calculation is not supported by this browser');
16✔
145
    }
146
    // to ensure the pubkey is copied with the new line at the end we have to double it at the end, as one of the endings gets trimmed in the process of copying
147
    const key = authset.pubkey.endsWith(LF) ? `${authset.pubkey}${LF}` : authset.pubkey;
16!
148
    setEndKey(key);
16✔
149
  }, [authset.pubkey]);
150

151
  const onShowKey = show => {
32✔
UNCOV
152
    onExpand(show && authset.id);
×
UNCOV
153
    setShowKey(show);
×
UNCOV
154
    setConfirmMessage(false);
×
155
  };
156

157
  const onCancelConfirm = () => {
32✔
UNCOV
158
    onExpand(false);
×
UNCOV
159
    setConfirmMessage('');
×
160
  };
161

162
  const onRequestConfirm = status => {
32✔
UNCOV
163
    let message = getConfirmationMessage(status, device, authset);
×
UNCOV
164
    setConfirmMessage(message);
×
UNCOV
165
    setNewStatus(status);
×
UNCOV
166
    setShowKey(false);
×
UNCOV
167
    onExpand(authset.id);
×
168
  };
169

170
  const onConfirm = confirmedState => confirm(device.id, authset.id, confirmedState).then(onCancelConfirm);
32✔
171

172
  const onDismissClick = () => {
32✔
UNCOV
173
    if (total > 1 || device.status !== DEVICE_STATES.pending) {
×
UNCOV
174
      return onRequestConfirm(DEVICE_DISMISSAL_STATE);
×
175
    }
UNCOV
176
    return onConfirm(DEVICE_DISMISSAL_STATE);
×
177
  };
178

179
  const onAcceptClick = () => {
32✔
UNCOV
180
    if (total > 1) {
×
UNCOV
181
      return onRequestConfirm(DEVICE_STATES.accepted);
×
182
    }
UNCOV
183
    return onConfirm(DEVICE_STATES.accepted);
×
184
  };
185

186
  const onCopied = (_, result) => {
32✔
UNCOV
187
    setCopied(result);
×
UNCOV
188
    setTimeout(() => setCopied(false), TIMEOUTS.fiveSeconds);
×
189
  };
190

191
  let key = <a onClick={onShowKey}>show key</a>;
32✔
192
  let content = [
32✔
193
    padder,
194
    <p className="bold expanded" key="content">
195
      {loading === authset.id ? 'Updating status' : `${confirmMessage} Are you sure you want to continue?`}
32!
196
    </p>
197
  ];
198

199
  if (showKey) {
32!
UNCOV
200
    content = [
×
201
      <div key="content">
202
        <CopyToClipboard text={endKey} onCopy={onCopied}>
203
          <IconButton style={{ float: 'right', margin: '-20px 0 0 10px' }} size="large">
204
            <CopyPasteIcon />
205
          </IconButton>
206
        </CopyToClipboard>
207
        <code className="pre-line">{endKey}</code>
208
        {copied && <p className="green fadeIn">Copied key to clipboard.</p>}
×
209
        <Divider className={classes.divider} />
210
        <div title="SHA256">
211
          Checksum
212
          <br />
213
          <code>{keyHash}</code>
214
        </div>
215
      </div>,
216
      padder
217
    ];
UNCOV
218
    key = <a onClick={() => onShowKey(false)}>hide key</a>;
×
219
  }
220
  return (
32✔
221
    <Accordion className={classes.accordion} square expanded={isExpanded}>
222
      <AccordionSummary className={`columns-${columns.length}`}>
223
        <AuthSetStatus authset={authset} device={device} />
224
        <div className="capitalized">{authset.status}</div>
225
        {key}
226
        <Time value={formatTime(authset.ts)} />
227
        {loading === authset.id ? (
32!
228
          <div>
229
            Updating status <Loader table={true} waiting={true} show={true} style={{ height: '4px', marginLeft: '10px' }} />
230
          </div>
231
        ) : (
232
          <ActionButtons
233
            authset={authset}
234
            confirmMessage={confirmMessage}
235
            newStatus={newStatus}
236
            limitMaxed={limitMaxed}
237
            onAcceptClick={onAcceptClick}
238
            onDismissClick={onDismissClick}
239
            onRequestConfirm={onRequestConfirm}
240
            userCapabilities={userCapabilities}
241
          />
242
        )}
243
      </AccordionSummary>
244
      <AccordionDetails>{content}</AccordionDetails>
245
      {isExpanded && !showKey && (
36✔
246
        <AccordionActions className="margin-right-small">
247
          {loading === authset.id ? (
2!
248
            <Loader table={true} waiting={true} show={true} style={{ height: '4px' }} />
249
          ) : (
250
            <>
251
              <Button className="margin-right-small" onClick={onCancelConfirm}>
252
                Cancel
253
              </Button>
UNCOV
254
              <Button variant="contained" onClick={() => onConfirm(newStatus)}>
×
255
                Confirm
256
              </Button>
257
            </>
258
          )}
259
        </AccordionActions>
260
      )}
261
    </Accordion>
262
  );
263
};
264

265
export default AuthsetListItem;
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