• 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

83.05
/src/js/components/header/header.js
1
// Copyright 2015 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 {
19
  AccountCircle as AccountCircleIcon,
20
  ArrowDropDown as ArrowDropDownIcon,
21
  ArrowDropUp as ArrowDropUpIcon,
22
  ExitToApp as ExitIcon
23
} from '@mui/icons-material';
24
import { Button, IconButton, ListItemSecondaryAction, ListItemText, Menu, MenuItem, Toolbar } from '@mui/material';
25
import { makeStyles } from 'tss-react/mui';
26

27
import moment from 'moment';
28
import Cookies from 'universal-cookie';
29

30
import enterpriseLogo from '../../../assets/img/headerlogo-enterprise.png';
31
import logo from '../../../assets/img/headerlogo.png';
32
import whiteEnterpriseLogo from '../../../assets/img/whiteheaderlogo-enterprise.png';
33
import whiteLogo from '../../../assets/img/whiteheaderlogo.png';
34
import { setFirstLoginAfterSignup, setSearchState } from '../../actions/appActions';
35
import { getAllDeviceCounts } from '../../actions/deviceActions';
36
import { initializeSelf, logoutUser, setAllTooltipsReadState, setHideAnnouncement } from '../../actions/userActions';
37
import { TIMEOUTS } from '../../constants/appConstants';
38
import { READ_STATES } from '../../constants/userConstants';
39
import { isDarkMode } from '../../helpers';
40
import {
41
  getAcceptedDevices,
42
  getCurrentSession,
43
  getCurrentUser,
44
  getDeviceCountsByStatus,
45
  getDeviceLimit,
46
  getFeatures,
47
  getIsEnterprise,
48
  getOrganization,
49
  getShowHelptips,
50
  getUserCapabilities,
51
  getUserSettings
52
} from '../../selectors';
53
import Tracking from '../../tracking';
54
import { useDebounce } from '../../utils/debouncehook';
55
import Search from '../common/search';
56
import Announcement from './announcement';
57
import DemoNotification from './demonotification';
58
import DeploymentNotifications from './deploymentnotifications';
59
import DeviceNotifications from './devicenotifications';
60
import OfferHeader from './offerheader';
61
import TrialNotification from './trialnotification';
62

63
// Change this when a new feature/offer is introduced
64
const currentOffer = {
2✔
65
  name: 'add-ons',
66
  expires: '2021-12-30',
67
  trial: true,
68
  os: true,
69
  professional: true,
70
  enterprise: true
71
};
72

73
const cookies = new Cookies();
2✔
74

75
const useStyles = makeStyles()(theme => ({
8✔
76
  header: {
77
    minHeight: 'unset',
78
    paddingLeft: theme.spacing(4),
79
    paddingRight: theme.spacing(5),
80
    width: '100%',
81
    borderBottom: `1px solid ${theme.palette.grey[100]}`,
82
    display: 'grid'
83
  },
84
  banner: { gridTemplateRows: `1fr ${theme.mixins.toolbar.minHeight}px` },
85
  buttonColor: { color: theme.palette.grey[600] },
86
  dropDown: { height: '100%', marginLeft: theme.spacing(0.5), textTransform: 'none' },
87
  exitIcon: { color: theme.palette.grey[600], fill: theme.palette.grey[600] },
88
  demoTrialAnnouncement: {
89
    fontSize: 14,
90
    height: 'auto'
91
  },
92
  demoAnnouncementIcon: {
93
    height: 16,
94
    color: theme.palette.primary.main,
95
    '&.MuiButton-textPrimary': {
96
      color: theme.palette.primary.main,
97
      height: 'inherit'
98
    }
99
  },
100
  redAnnouncementIcon: {
101
    color: theme.palette.error.dark
102
  }
103
}));
104

105
export const Header = ({ mode }) => {
2✔
106
  const { classes } = useStyles();
389✔
107
  const [anchorEl, setAnchorEl] = useState(null);
389✔
108
  const [gettingUser, setGettingUser] = useState(false);
389✔
109
  const [hasOfferCookie, setHasOfferCookie] = useState(false);
389✔
110

111
  const organization = useSelector(getOrganization);
389✔
112
  const { canManageUsers: allowUserManagement } = useSelector(getUserCapabilities);
389✔
113
  const { total: acceptedDevices = 0 } = useSelector(getAcceptedDevices);
389!
114
  const announcement = useSelector(state => state.app.hostedAnnouncement);
1,355✔
115
  const deviceLimit = useSelector(getDeviceLimit);
389✔
116
  const firstLoginAfterSignup = useSelector(state => state.app.firstLoginAfterSignup);
1,355✔
117
  const { trackingConsentGiven: hasTrackingEnabled } = useSelector(getUserSettings);
389✔
118
  const inProgress = useSelector(state => state.deployments.byStatus.inprogress.total);
1,355✔
119
  const isEnterprise = useSelector(getIsEnterprise);
389✔
120
  const { isDemoMode: demo, hasMultitenancy, isHosted } = useSelector(getFeatures);
389✔
121
  const { isSearching, searchTerm, refreshTrigger } = useSelector(state => state.app.searchState);
1,355✔
122
  const multitenancy = hasMultitenancy || isEnterprise || isHosted;
389✔
123
  const { pending: pendingDevices } = useSelector(getDeviceCountsByStatus);
389✔
124
  const user = useSelector(getCurrentUser);
389✔
125
  const { token } = useSelector(getCurrentSession);
389✔
126
  const userId = useDebounce(user.id, TIMEOUTS.debounceDefault);
389✔
127

128
  const dispatch = useDispatch();
389✔
129
  const deviceTimer = useRef();
389✔
130
  const showHelptips = useSelector(getShowHelptips);
389✔
131

132
  useEffect(() => {
389✔
133
    if ((!userId || !user.email?.length) && !gettingUser && token) {
7!
134
      setGettingUser(true);
×
135
      dispatch(initializeSelf());
×
136
      return;
×
137
    }
138
    Tracking.setTrackingEnabled(hasTrackingEnabled);
7✔
139
    if (hasTrackingEnabled && user.id && organization.id) {
7!
140
      Tracking.setOrganizationUser(organization, user);
×
141
      if (firstLoginAfterSignup) {
×
142
        Tracking.pageview('/signup/complete');
×
143
        dispatch(setFirstLoginAfterSignup(false));
×
144
      }
145
    }
146
  }, [dispatch, firstLoginAfterSignup, gettingUser, hasTrackingEnabled, organization, token, user, user.email, userId]);
147

148
  useEffect(() => {
389✔
149
    const showOfferCookie = cookies.get('offer') === currentOffer.name;
6✔
150
    setHasOfferCookie(showOfferCookie);
6✔
151
    clearInterval(deviceTimer.current);
6✔
152
    deviceTimer.current = setInterval(() => dispatch(getAllDeviceCounts()), TIMEOUTS.refreshDefault);
186✔
153
    return () => {
6✔
154
      clearInterval(deviceTimer.current);
6✔
155
    };
156
  }, [dispatch]);
157

158
  const onLogoutClick = () => {
389✔
159
    setAnchorEl(null);
3✔
160
    dispatch(logoutUser()).then(() => window.location.replace('/ui/'));
3✔
161
  };
162

163
  const onSearch = useCallback((searchTerm, refreshTrigger) => dispatch(setSearchState({ refreshTrigger, searchTerm, page: 1 })), [dispatch]);
389✔
164

165
  const onToggleTooltips = () => dispatch(setAllTooltipsReadState(showHelptips ? READ_STATES.read : READ_STATES.unread));
389!
166

167
  const setHideOffer = () => {
389✔
168
    cookies.set('offer', currentOffer.name, { path: '/', maxAge: 2629746 });
×
169
    setHasOfferCookie(true);
×
170
  };
171

172
  const handleClose = () => setAnchorEl(null);
389✔
173

174
  const showOffer =
175
    isHosted && moment().isBefore(currentOffer.expires) && (organization.trial ? currentOffer.trial : currentOffer[organization.plan]) && !hasOfferCookie;
389!
176

177
  const headerLogo = isDarkMode(mode) ? (isEnterprise ? whiteEnterpriseLogo : whiteLogo) : isEnterprise ? enterpriseLogo : logo;
389!
178

179
  return (
389✔
180
    <Toolbar id="fixedHeader" className={showOffer ? `${classes.header} ${classes.banner}` : classes.header}>
389!
181
      {!!announcement && (
389!
182
        <Announcement
183
          announcement={announcement}
184
          errorIconClassName={classes.redAnnouncementIcon}
185
          iconClassName={classes.demoAnnouncementIcon}
186
          sectionClassName={classes.demoTrialAnnouncement}
187
          onHide={() => dispatch(setHideAnnouncement(true))}
×
188
        />
189
      )}
190
      {showOffer && <OfferHeader onHide={setHideOffer} />}
389!
191
      <div className="flexbox space-between">
192
        <div className="flexbox center-aligned">
193
          <Link to="/">
194
            <img id="logo" src={headerLogo} />
195
          </Link>
196
          {demo && <DemoNotification iconClassName={classes.demoAnnouncementIcon} sectionClassName={classes.demoTrialAnnouncement} />}
389!
197
          {organization.trial && (
389!
198
            <TrialNotification
199
              expiration={organization.trial_expiration}
200
              iconClassName={classes.demoAnnouncementIcon}
201
              sectionClassName={classes.demoTrialAnnouncement}
202
            />
203
          )}
204
        </div>
205
        <Search isSearching={isSearching} searchTerm={searchTerm} onSearch={onSearch} trigger={refreshTrigger} />
206
        <div className="flexbox center-aligned">
207
          <DeviceNotifications pending={pendingDevices} total={acceptedDevices} limit={deviceLimit} />
208
          <DeploymentNotifications inprogress={inProgress} />
209
          <Button
210
            className={`header-dropdown ${classes.dropDown}`}
211
            onClick={e => setAnchorEl(e.currentTarget)}
3✔
212
            startIcon={<AccountCircleIcon className={classes.buttonColor} />}
213
            endIcon={anchorEl ? <ArrowDropUpIcon /> : <ArrowDropDownIcon />}
389✔
214
          >
215
            {user.email}
216
          </Button>
217
          <Menu
218
            anchorEl={anchorEl}
219
            onClose={handleClose}
220
            open={Boolean(anchorEl)}
221
            anchorOrigin={{
222
              vertical: 'center',
223
              horizontal: 'center'
224
            }}
225
            transformOrigin={{
226
              vertical: 'bottom',
227
              horizontal: 'center'
228
            }}
229
          >
230
            <MenuItem component={Link} to="/settings" onClick={handleClose}>
231
              Settings
232
            </MenuItem>
233
            <MenuItem component={Link} to="/settings/my-profile" onClick={handleClose}>
234
              My profile
235
            </MenuItem>
236
            {multitenancy && (
765✔
237
              <MenuItem component={Link} to="/settings/organization-and-billing" onClick={handleClose}>
238
                My organization
239
              </MenuItem>
240
            )}
241
            {allowUserManagement && (
778✔
242
              <MenuItem component={Link} to="/settings/user-management" onClick={handleClose}>
243
                User management
244
              </MenuItem>
245
            )}
246
            <MenuItem onClick={onToggleTooltips}>{`Mark help tips as ${showHelptips ? '' : 'un'}read`}</MenuItem>
389!
247
            <MenuItem component={Link} to="/help/get-started" onClick={handleClose}>
248
              Help & support
249
            </MenuItem>
250
            <MenuItem onClick={onLogoutClick}>
251
              <ListItemText primary="Log out" />
252
              <ListItemSecondaryAction>
253
                <IconButton>
254
                  <ExitIcon className={classes.exitIcon} />
255
                </IconButton>
256
              </ListItemSecondaryAction>
257
            </MenuItem>
258
          </Menu>
259
        </div>
260
      </div>
261
    </Toolbar>
262
  );
263
};
264

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