• 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

84.93
/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 { AccountCircle as AccountCircleIcon, ExitToApp as ExitIcon, ExpandMore } from '@mui/icons-material';
19
import {
20
  Accordion,
21
  AccordionDetails,
22
  AccordionSummary,
23
  Button,
24
  Divider,
25
  IconButton,
26
  ListItemSecondaryAction,
27
  ListItemText,
28
  Menu,
29
  MenuItem,
30
  Toolbar,
31
  Typography,
32
  accordionClasses,
33
  accordionSummaryClasses,
34
  listItemTextClasses,
35
  menuItemClasses
36
} from '@mui/material';
37
import { makeStyles } from 'tss-react/mui';
38

39
import moment from 'moment';
40
import Cookies from 'universal-cookie';
41

42
import enterpriseLogo from '../../../assets/img/headerlogo-enterprise.png';
43
import logo from '../../../assets/img/headerlogo.png';
44
import whiteEnterpriseLogo from '../../../assets/img/whiteheaderlogo-enterprise.png';
45
import whiteLogo from '../../../assets/img/whiteheaderlogo.png';
46
import { setFirstLoginAfterSignup, setSearchState } from '../../actions/appActions';
47
import { getAllDeviceCounts } from '../../actions/deviceActions';
48
import { initializeSelf, logoutUser, setAllTooltipsReadState, setHideAnnouncement, switchUserOrganization } from '../../actions/userActions';
49
import { TIMEOUTS } from '../../constants/appConstants';
50
import { READ_STATES } from '../../constants/userConstants';
51
import { isDarkMode, toggle } from '../../helpers';
52
import {
53
  getAcceptedDevices,
54
  getCurrentSession,
55
  getCurrentUser,
56
  getDeviceCountsByStatus,
57
  getDeviceLimit,
58
  getFeatures,
59
  getIsEnterprise,
60
  getOrganization,
61
  getShowHelptips,
62
  getUserSettings
63
} from '../../selectors';
64
import Tracking from '../../tracking';
65
import { useDebounce } from '../../utils/debouncehook';
66
import Search from '../common/search';
67
import Announcement from './announcement';
68
import DemoNotification from './demonotification';
69
import DeploymentNotifications from './deploymentnotifications';
70
import DeviceNotifications from './devicenotifications';
71
import OfferHeader from './offerheader';
72
import TrialNotification from './trialnotification';
73

74
// Change this when a new feature/offer is introduced
75
const currentOffer = {
2✔
76
  name: 'add-ons',
77
  expires: '2021-12-30',
78
  trial: true,
79
  os: true,
80
  professional: true,
81
  enterprise: true
82
};
83

84
const cookies = new Cookies();
2✔
85

86
const useStyles = makeStyles()(theme => ({
34✔
87
  accordion: {
88
    ul: { paddingInlineStart: 0 },
89
    [`&.${accordionClasses.disabled}, &.${accordionClasses.expanded}`]: {
90
      backgroundColor: theme.palette.background.paper
91
    },
92
    [`.${accordionSummaryClasses.root}:hover`]: {
93
      backgroundColor: theme.palette.grey[400],
94
      color: theme.palette.text.link
95
    },
96
    [`.${menuItemClasses.root}:hover`]: {
97
      color: theme.palette.text.link
98
    }
99
  },
100
  header: {
101
    minHeight: 'unset',
102
    paddingLeft: theme.spacing(4),
103
    paddingRight: theme.spacing(5),
104
    width: '100%',
105
    borderBottom: `1px solid ${theme.palette.grey[100]}`,
106
    display: 'grid'
107
  },
108
  banner: { gridTemplateRows: `1fr ${theme.mixins.toolbar.minHeight}px` },
109
  buttonColor: { color: theme.palette.grey[600] },
110
  dropDown: {
111
    height: '100%',
112
    textTransform: 'none',
113
    [`.${menuItemClasses.root}:hover, .${listItemTextClasses.root}:hover`]: {
114
      color: theme.palette.text.link
115
    }
116
  },
117
  exitIcon: { color: theme.palette.grey[600], fill: theme.palette.grey[600] },
118
  demoTrialAnnouncement: {
119
    fontSize: 14,
120
    height: 'auto'
121
  },
122
  demoAnnouncementIcon: {
123
    height: 16,
124
    color: theme.palette.primary.main,
125
    '&.MuiButton-textPrimary': {
126
      color: theme.palette.primary.main,
127
      height: 'inherit'
128
    }
129
  },
130
  organization: { marginBottom: theme.spacing() },
131
  redAnnouncementIcon: {
132
    color: theme.palette.error.dark
133
  }
134
}));
135

136
const AccountMenu = () => {
2✔
137
  const [anchorEl, setAnchorEl] = useState(null);
30✔
138
  const [tenantSwitcherShowing, setTenantSwitcherShowing] = useState(false);
30✔
139
  const showHelptips = useSelector(getShowHelptips);
30✔
140
  const { email, tenants = [] } = useSelector(getCurrentUser);
30✔
141
  const { name } = useSelector(getOrganization);
30✔
142
  const isEnterprise = useSelector(getIsEnterprise);
30✔
143
  const { hasMultitenancy, isHosted } = useSelector(getFeatures);
30✔
144
  const multitenancy = hasMultitenancy || isEnterprise || isHosted;
30✔
145
  const dispatch = useDispatch();
30✔
146

147
  const { classes } = useStyles();
30✔
148

149
  const handleClose = () => {
30✔
UNCOV
150
    setAnchorEl(null);
×
UNCOV
151
    setTenantSwitcherShowing(false);
×
152
  };
153

154
  const handleSwitchTenant = id => dispatch(switchUserOrganization(id));
30✔
155

156
  const onLogoutClick = () => {
30✔
157
    setAnchorEl(null);
1✔
158
    dispatch(logoutUser()).then(() => window.location.replace('/ui/'));
1✔
159
  };
160

161
  const onToggleTooltips = () => dispatch(setAllTooltipsReadState(showHelptips ? READ_STATES.read : READ_STATES.unread));
30!
162

163
  return (
30✔
164
    <>
165
      <Button className={classes.dropDown} onClick={e => setAnchorEl(e.currentTarget)} startIcon={<AccountCircleIcon className={classes.buttonColor} />}>
1✔
166
        {email}
167
      </Button>
168
      <Menu
169
        anchorEl={anchorEl}
170
        className={classes.dropDown}
171
        onClose={handleClose}
172
        open={Boolean(anchorEl)}
173
        anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
174
        transformOrigin={{ horizontal: 'right', vertical: 'top' }}
175
      >
176
        <MenuItem component={Link} to="/settings/my-profile" onClick={handleClose}>
177
          My profile
178
        </MenuItem>
179
        <Divider />
180
        {!!(multitenancy && name) && (
96✔
181
          <MenuItem component={Link} dense to="/settings/organization-and-billing" onClick={handleClose} className={classes.organization}>
182
            <div>
183
              <Typography variant="caption" className="muted">
184
                My organization
185
              </Typography>
186
              <Typography variant="subtitle2">{name}</Typography>
187
            </div>
188
          </MenuItem>
189
        )}
190
        {tenants.length > 1 && (
30!
191
          <div>
192
            <Divider style={{ marginBottom: 0 }} />
UNCOV
193
            <Accordion className={classes.accordion} square expanded={tenantSwitcherShowing} onChange={() => setTenantSwitcherShowing(toggle)}>
×
194
              <AccordionSummary expandIcon={<ExpandMore />}>Switch organization</AccordionSummary>
195
              <AccordionDetails className="padding-left-none padding-right-none">
196
                {tenants.map(({ id, name }) => (
UNCOV
197
                  <MenuItem className="padding-left padding-right" key={id} onClick={() => handleSwitchTenant(id)}>
×
198
                    {name}
199
                  </MenuItem>
200
                ))}
201
              </AccordionDetails>
202
            </Accordion>
203
          </div>
204
        )}
205
        <Divider />
206
        <MenuItem component={Link} to="/settings/global-settings" onClick={handleClose}>
207
          Settings
208
        </MenuItem>
209
        <MenuItem onClick={onToggleTooltips}>{`Mark help tips as ${showHelptips ? '' : 'un'}read`}</MenuItem>
30!
210
        <MenuItem component={Link} to="/help/get-started" onClick={handleClose}>
211
          Help & support
212
        </MenuItem>
213
        <MenuItem onClick={onLogoutClick}>
214
          <ListItemText primary="Log out" />
215
          <ListItemSecondaryAction>
216
            <IconButton>
217
              <ExitIcon className={classes.exitIcon} />
218
            </IconButton>
219
          </ListItemSecondaryAction>
220
        </MenuItem>
221
      </Menu>
222
    </>
223
  );
224
};
225

226
export const Header = ({ mode }) => {
2✔
227
  const { classes } = useStyles();
35✔
228
  const [gettingUser, setGettingUser] = useState(false);
35✔
229
  const [hasOfferCookie, setHasOfferCookie] = useState(false);
35✔
230

231
  const organization = useSelector(getOrganization);
35✔
232
  const { total: acceptedDevices = 0 } = useSelector(getAcceptedDevices);
35!
233
  const announcement = useSelector(state => state.app.hostedAnnouncement);
2,158✔
234
  const deviceLimit = useSelector(getDeviceLimit);
35✔
235
  const firstLoginAfterSignup = useSelector(state => state.app.firstLoginAfterSignup);
2,158✔
236
  const { trackingConsentGiven: hasTrackingEnabled } = useSelector(getUserSettings);
35✔
237
  const inProgress = useSelector(state => state.deployments.byStatus.inprogress.total);
2,158✔
238
  const isEnterprise = useSelector(getIsEnterprise);
35✔
239
  const { isDemoMode: demo, isHosted } = useSelector(getFeatures);
35✔
240
  const { isSearching, searchTerm, refreshTrigger } = useSelector(state => state.app.searchState);
2,158✔
241
  const { pending: pendingDevices } = useSelector(getDeviceCountsByStatus);
35✔
242
  const userSettingInitialized = useSelector(state => state.users.settingsInitialized);
2,158✔
243
  const user = useSelector(getCurrentUser);
35✔
244
  const { token } = useSelector(getCurrentSession);
35✔
245
  const userId = useDebounce(user.id, TIMEOUTS.debounceDefault);
35✔
246

247
  const dispatch = useDispatch();
35✔
248
  const deviceTimer = useRef();
35✔
249

250
  useEffect(() => {
35✔
251
    if ((!userId || !user.email?.length || !userSettingInitialized) && !gettingUser && token) {
14✔
252
      setGettingUser(true);
2✔
253
      dispatch(initializeSelf());
2✔
254
      return;
2✔
255
    }
256
    Tracking.setTrackingEnabled(hasTrackingEnabled);
12✔
257
    if (hasTrackingEnabled && user.id && organization.id) {
12!
UNCOV
258
      Tracking.setOrganizationUser(organization, user);
×
UNCOV
259
      if (firstLoginAfterSignup) {
×
UNCOV
260
        Tracking.pageview('/signup/complete');
×
UNCOV
261
        dispatch(setFirstLoginAfterSignup(false));
×
262
      }
263
    }
264
  }, [dispatch, firstLoginAfterSignup, gettingUser, hasTrackingEnabled, organization, token, user, user.email, userId, userSettingInitialized]);
265

266
  useEffect(() => {
35✔
267
    const showOfferCookie = cookies.get('offer') === currentOffer.name;
7✔
268
    setHasOfferCookie(showOfferCookie);
7✔
269
    clearInterval(deviceTimer.current);
7✔
270
    deviceTimer.current = setInterval(() => dispatch(getAllDeviceCounts()), TIMEOUTS.refreshDefault);
367✔
271
    return () => {
7✔
272
      clearInterval(deviceTimer.current);
7✔
273
    };
274
  }, [dispatch]);
275

276
  const onSearch = useCallback((searchTerm, refreshTrigger) => dispatch(setSearchState({ refreshTrigger, searchTerm, page: 1 })), [dispatch]);
35✔
277

278
  const setHideOffer = () => {
35✔
UNCOV
279
    cookies.set('offer', currentOffer.name, { path: '/', maxAge: 2629746 });
×
UNCOV
280
    setHasOfferCookie(true);
×
281
  };
282

283
  const showOffer =
284
    isHosted && moment().isBefore(currentOffer.expires) && (organization.trial ? currentOffer.trial : currentOffer[organization.plan]) && !hasOfferCookie;
35!
285

286
  const headerLogo = isDarkMode(mode) ? (isEnterprise ? whiteEnterpriseLogo : whiteLogo) : isEnterprise ? enterpriseLogo : logo;
35!
287

288
  return (
35✔
289
    <Toolbar id="fixedHeader" className={showOffer ? `${classes.header} ${classes.banner}` : classes.header}>
35!
290
      {!!announcement && (
48✔
291
        <Announcement
292
          announcement={announcement}
293
          errorIconClassName={classes.redAnnouncementIcon}
294
          iconClassName={classes.demoAnnouncementIcon}
295
          sectionClassName={classes.demoTrialAnnouncement}
UNCOV
296
          onHide={() => dispatch(setHideAnnouncement(true))}
×
297
        />
298
      )}
299
      {showOffer && <OfferHeader onHide={setHideOffer} />}
35!
300
      <div className="flexbox space-between">
301
        <div className="flexbox center-aligned">
302
          <Link to="/">
303
            <img id="logo" src={headerLogo} />
304
          </Link>
305
          {demo && <DemoNotification iconClassName={classes.demoAnnouncementIcon} sectionClassName={classes.demoTrialAnnouncement} />}
35!
306
          {organization.trial && (
35!
307
            <TrialNotification
308
              expiration={organization.trial_expiration}
309
              iconClassName={classes.demoAnnouncementIcon}
310
              sectionClassName={classes.demoTrialAnnouncement}
311
            />
312
          )}
313
        </div>
314
        <Search isSearching={isSearching} searchTerm={searchTerm} onSearch={onSearch} trigger={refreshTrigger} />
315
        <div className="flexbox center-aligned">
316
          <DeviceNotifications pending={pendingDevices} total={acceptedDevices} limit={deviceLimit} />
317
          <DeploymentNotifications inprogress={inProgress} />
318
          <AccountMenu />
319
        </div>
320
      </div>
321
    </Toolbar>
322
  );
323
};
324

325
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