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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

82.54
/src/js/components/devices/device-details/connection.js
1
// Copyright 2021 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, useEffect, useRef, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16
import { Link } from 'react-router-dom';
17

18
import { InfoOutlined as InfoIcon, Launch as LaunchIcon } from '@mui/icons-material';
19
import { Button, Typography } from '@mui/material';
20
import { makeStyles } from 'tss-react/mui';
21

22
import { setSnackbar } from '../../../actions/appActions';
23
import { getDeviceFileDownloadLink } from '../../../actions/deviceActions';
24
import { BEGINNING_OF_TIME, BENEFITS, TIMEOUTS } from '../../../constants/appConstants';
25
import { ALL_DEVICES, DEVICE_CONNECT_STATES } from '../../../constants/deviceConstants';
26
import { AUDIT_LOGS_TYPES } from '../../../constants/organizationConstants';
27
import { checkPermissionsObject, uiPermissionsById } from '../../../constants/userConstants';
28
import { createDownload } from '../../../helpers';
29
import { getCurrentSession, getTenantCapabilities, getUserCapabilities } from '../../../selectors';
30
import { formatAuditlogs } from '../../../utils/locationutils';
31
import DocsLink from '../../common/docslink';
32
import EnterpriseNotification from '../../common/enterpriseNotification';
33
import Loader from '../../common/loader';
34
import MenderTooltip from '../../common/mendertooltip';
35
import Time from '../../common/time';
36
import FileTransfer from '../troubleshoot/filetransfer';
37
import TroubleshootContent from '../troubleshoot/terminal-wrapper';
38
import DeviceDataCollapse from './devicedatacollapse';
39

40
const useStyles = makeStyles()(theme => ({
10✔
41
  buttonStyle: { textTransform: 'none', textAlign: 'left' },
42
  connectionIcon: { marginRight: theme.spacing() },
43
  content: { maxWidth: 1280 },
44
  title: { marginRight: theme.spacing(0.5) },
45
  troubleshootButton: { marginRight: theme.spacing(2) }
46
}));
47

48
export const PortForwardLink = () => (
10✔
49
  <MenderTooltip
7✔
50
    arrow
51
    title={
52
      <div style={{ whiteSpace: 'normal' }}>
53
        <h3>Port forwarding</h3>
54
        <p>Port forwarding allows you to troubleshoot or use services on or via the device, without opening any ports on the device itself.</p>
55
        <p>
56
          To enable port forwarding you will need to install and configure the necessary software on the device and your workstation. Follow the link to learn
57
          more.
58
        </p>
59
      </div>
60
    }
61
  >
62
    <DocsLink className="flexbox centered margin-left" path="add-ons/port-forward">
63
      Enable port forwarding
64
      <LaunchIcon className="margin-left-small" fontSize="small" />
65
    </DocsLink>
66
  </MenderTooltip>
67
);
68

69
export const DeviceConnectionNote = ({ children }) => {
10✔
70
  const { classes } = useStyles();
5✔
71
  return (
5✔
72
    <div className="flexbox muted">
73
      <InfoIcon className={classes.connectionIcon} fontSize="small" />
74
      <Typography className={classes.buttonStyle} variant="body1">
75
        {children}
76
      </Typography>
77
    </div>
78
  );
79
};
80

81
export const DeviceConnectionMissingNote = () => (
10✔
82
  <DeviceConnectionNote>
1✔
83
    The troubleshoot add-on does not seem to be enabled on this device.
84
    <br />
85
    Please <DocsLink path="add-ons/remote-terminal" title="see the documentation" /> for a description on how it works and how to enable it.
86
  </DeviceConnectionNote>
87
);
88

89
export const DeviceDisconnectedNote = ({ lastConnectionTs }) => (
10✔
90
  <DeviceConnectionNote>
3✔
91
    The troubleshoot add-on is not currently connected on this device, it was last connected on <Time value={lastConnectionTs} />.
92
    <br />
93
    Please <DocsLink path="add-ons/remote-terminal" title="see the documentation" /> for more information.
94
  </DeviceConnectionNote>
95
);
96

97
export const TroubleshootButton = ({ disabled, item, onClick }) => {
10✔
98
  const { classes } = useStyles();
×
99
  return (
×
100
    <Button className={classes.troubleshootButton} onClick={() => onClick(item.key)} disabled={disabled} startIcon={item.icon}>
×
101
      <Typography className={classes.buttonStyle} variant="subtitle2">
102
        {item.title}
103
      </Typography>
104
    </Button>
105
  );
106
};
107

108
const deviceAuditlogType = AUDIT_LOGS_TYPES.find(type => type.value === 'device');
30✔
109

110
const tabs = {
10✔
111
  terminal: {
112
    title: 'Remote terminal',
113
    value: 'terminal',
114
    canShow: ({ canTroubleshoot, canWriteDevices, groupsPermissions }, { group }) =>
115
      (canTroubleshoot && canWriteDevices) || checkPermissionsObject(groupsPermissions, uiPermissionsById.connect.value, group, ALL_DEVICES),
3!
116
    Component: TroubleshootContent
117
  },
118
  transfer: { title: 'File transfer', value: 'transfer', canShow: ({ canTroubleshoot }) => canTroubleshoot, Component: FileTransfer }
3✔
119
};
120

121
export const DeviceConnection = ({ className = '', device }) => {
10✔
122
  const [socketClosed, setSocketClosed] = useState();
6✔
123
  const [availableTabs, setAvailableTabs] = useState(Object.values(tabs));
6✔
124
  const [downloadPath, setDownloadPath] = useState('');
6✔
125
  const [file, setFile] = useState();
6✔
126
  const [socketInitialized, setSocketInitialized] = useState(undefined);
6✔
127
  const [uploadPath, setUploadPath] = useState('');
6✔
128
  const closeTimer = useRef();
6✔
129
  const initTimer = useRef();
6✔
130

131
  const userCapabilities = useSelector(getUserCapabilities);
6✔
132
  const { canAuditlog, canTroubleshoot } = userCapabilities;
6✔
133
  const { hasAuditlogs } = useSelector(getTenantCapabilities);
6✔
134
  const { token } = useSelector(getCurrentSession);
6✔
135
  const { classes } = useStyles();
6✔
136
  const { connect_status, connect_updated_ts, isOffline } = device;
6✔
137
  const [connectionStatus, setConnectionStatus] = useState(connect_status);
6✔
138

139
  const dispatch = useDispatch();
6✔
140
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
6✔
141

142
  useEffect(() => {
6✔
143
    if (!socketClosed) {
3!
144
      return;
3✔
145
    }
146
    clearTimeout(closeTimer.current);
×
147
    closeTimer.current = setTimeout(() => setSocketClosed(false), TIMEOUTS.fiveSeconds);
×
148
  }, [socketClosed]);
149

150
  useEffect(() => {
6✔
151
    setConnectionStatus(connect_status);
3✔
152
    clearTimeout(initTimer.current);
3✔
153
    if (connectionStatus) {
3✔
154
      return;
2✔
155
    }
156
    initTimer.current = setTimeout(() => {
1✔
157
      setConnectionStatus(!connect_status || isOffline ? DEVICE_CONNECT_STATES.unknown : connect_status);
×
158
    }, TIMEOUTS.fiveSeconds);
159
    return () => clearTimeout(initTimer.current);
1✔
160
  }, [connect_status, connectionStatus, device.id, isOffline]);
161

162
  useEffect(() => {
6✔
163
    const allowedTabs = Object.values(tabs).reduce((accu, tab) => {
3✔
164
      if (tab.canShow(userCapabilities, device) && connectionStatus === DEVICE_CONNECT_STATES.connected) {
6✔
165
        accu.push(tab);
2✔
166
      }
167
      return accu;
6✔
168
    }, []);
169
    setAvailableTabs(allowedTabs);
3✔
170
    // eslint-disable-next-line react-hooks/exhaustive-deps
171
  }, [connectionStatus, JSON.stringify(device), JSON.stringify(userCapabilities)]);
172

173
  const onDownloadClick = useCallback(
6✔
174
    path => {
175
      setDownloadPath(path);
×
176
      dispatch(setSnackbar('Downloading file'));
×
177
      dispatch(getDeviceFileDownloadLink(device.id, path)).then(address => {
×
178
        const filename = path.substring(path.lastIndexOf('/') + 1) || 'file';
×
179
        createDownload(address, filename, token);
×
180
      });
181
    },
182
    [dispatch, device.id, token]
183
  );
184

185
  return (
6✔
186
    <DeviceDataCollapse
187
      isAddOn
188
      title={
189
        <div className="flexbox center-aligned">
190
          <h4>Troubleshooting</h4>
191
          <div className={`flexbox ${className}`}>
192
            {connectionStatus !== DEVICE_CONNECT_STATES.unknown && canTroubleshoot && <PortForwardLink />}
18✔
193
            {canAuditlog && hasAuditlogs && (
12!
194
              <Link
195
                className="flexbox center-aligned margin-left"
196
                to={`/auditlog?${formatAuditlogs({ pageState: { type: deviceAuditlogType, detail: device.id, startDate: BEGINNING_OF_TIME } }, {})}`}
197
              >
198
                List all log entries for this device
199
              </Link>
200
            )}
201
          </div>
202
          <EnterpriseNotification className="margin-left-small" id={BENEFITS.deviceTroubleshoot.id} />
203
        </div>
204
      }
205
    >
206
      <div className={`flexbox column ${classes.content}`}>
207
        {!connectionStatus && (
8✔
208
          <div className="flexbox centered">
209
            <Loader show />
210
          </div>
211
        )}
212
        {connectionStatus === DEVICE_CONNECT_STATES.unknown && <DeviceConnectionMissingNote />}
6!
213
        {connectionStatus === DEVICE_CONNECT_STATES.disconnected && <DeviceDisconnectedNote lastConnectionTs={connect_updated_ts} />}
8✔
214
        {availableTabs.map(({ Component, title, value }) => (
215
          <div key={value}>
8✔
216
            <h4 className="margin-top-large">{title}</h4>
217
            <Component
218
              device={device}
219
              downloadPath={downloadPath}
220
              file={file}
221
              onDownload={onDownloadClick}
222
              setDownloadPath={setDownloadPath}
223
              setFile={setFile}
224
              setSnackbar={dispatchedSetSnackbar}
225
              setSocketClosed={setSocketClosed}
226
              setSocketInitialized={setSocketInitialized}
227
              setUploadPath={setUploadPath}
228
              socketInitialized={socketInitialized}
229
              uploadPath={uploadPath}
230
              userCapabilities={userCapabilities}
231
            />
232
          </div>
233
        ))}
234
      </div>
235
    </DeviceDataCollapse>
236
  );
237
};
238

239
export default DeviceConnection;
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