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

mendersoftware / gui / 1308041585

28 May 2024 07:57AM UTC coverage: 83.386% (-16.6%) from 99.964%
1308041585

Pull #4424

gitlab-ci

mzedel
feat: restructured account menu & added option to switch tenant in supporting setups

Ticket: MEN-6906
Changelog: Title
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4424: MEN-6906 - tenant switching - wip

4464 of 6369 branches covered (70.09%)

24 of 28 new or added lines in 2 files covered. (85.71%)

1670 existing lines in 164 files now uncovered.

8477 of 10166 relevant lines covered (83.39%)

140.8 hits per line

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

85.53
/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
  avatarClasses
33
} from '@mui/material';
34
import { makeStyles } from 'tss-react/mui';
35

36
import moment from 'moment';
37
import Cookies from 'universal-cookie';
38

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

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

81
const cookies = new Cookies();
2✔
82

83
const useStyles = makeStyles()(theme => ({
24✔
84
  header: {
85
    minHeight: 'unset',
86
    paddingLeft: theme.spacing(4),
87
    paddingRight: theme.spacing(5),
88
    width: '100%',
89
    borderBottom: `1px solid ${theme.palette.grey[100]}`,
90
    display: 'grid'
91
  },
92
  banner: { gridTemplateRows: `1fr ${theme.mixins.toolbar.minHeight}px` },
93
  buttonColor: { color: theme.palette.grey[600] },
94
  dropDown: {
95
    height: '100%',
96
    textTransform: 'none',
97
    alignSelf: 'center',
98
    [`&.${avatarClasses.root}`]: {
99
      margin: 15,
100
      padding: 0
101
    }
102
  },
103
  exitIcon: { color: theme.palette.grey[600], fill: theme.palette.grey[600] },
104
  demoTrialAnnouncement: {
105
    fontSize: 14,
106
    height: 'auto'
107
  },
108
  demoAnnouncementIcon: {
109
    height: 16,
110
    color: theme.palette.primary.main,
111
    '&.MuiButton-textPrimary': {
112
      color: theme.palette.primary.main,
113
      height: 'inherit'
114
    }
115
  },
116
  redAnnouncementIcon: {
117
    color: theme.palette.error.dark
118
  }
119
}));
120

121
const AccountMenu = () => {
2✔
122
  const [anchorEl, setAnchorEl] = useState(null);
21✔
123
  const [tenantSwitcherShowing, setTenantSwitcherShowing] = useState(false);
21✔
124
  const showHelptips = useSelector(getShowHelptips);
21✔
125
  const {
126
    email,
127
    tenant_ids = [
21✔
128
      { id: '1239081239182309', name: 'foo' },
129
      { id: '1239081239182309123', name: 'bar' }
130
    ]
131
  } = useSelector(getCurrentUser);
21✔
132
  const { name } = useSelector(getOrganization);
21✔
133
  const isEnterprise = useSelector(getIsEnterprise);
21✔
134
  const { hasMultitenancy, isHosted } = useSelector(getFeatures);
21✔
135
  const multitenancy = hasMultitenancy || isEnterprise || isHosted;
21✔
136
  const dispatch = useDispatch();
21✔
137

138
  const { classes } = useStyles();
21✔
139

140
  const handleClose = () => {
21✔
NEW
141
    setAnchorEl(null);
×
NEW
142
    setTenantSwitcherShowing(false);
×
143
  };
144

145
  const handleSwitchTenant = id => {
21✔
NEW
146
    console.log(id);
×
147
  };
148

149
  const onLogoutClick = () => {
21✔
150
    setAnchorEl(null);
1✔
151
    dispatch(logoutUser()).then(() => window.location.replace('/ui/'));
1✔
152
  };
153

154
  const onToggleTooltips = () => dispatch(setAllTooltipsReadState(showHelptips ? READ_STATES.read : READ_STATES.unread));
21!
155

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

231
export const Header = ({ mode }) => {
2✔
232
  const { classes } = useStyles();
25✔
233
  const [gettingUser, setGettingUser] = useState(false);
25✔
234
  const [hasOfferCookie, setHasOfferCookie] = useState(false);
25✔
235

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

252
  const dispatch = useDispatch();
25✔
253
  const deviceTimer = useRef();
25✔
254

255
  useEffect(() => {
25✔
256
    if ((!userId || !user.email?.length || !userSettingInitialized) && !gettingUser && token) {
10✔
257
      setGettingUser(true);
1✔
258
      dispatch(initializeSelf());
1✔
259
      return;
1✔
260
    }
261
    Tracking.setTrackingEnabled(hasTrackingEnabled);
9✔
262
    if (hasTrackingEnabled && user.id && organization.id) {
9!
UNCOV
263
      Tracking.setOrganizationUser(organization, user);
×
UNCOV
264
      if (firstLoginAfterSignup) {
×
UNCOV
265
        Tracking.pageview('/signup/complete');
×
UNCOV
266
        dispatch(setFirstLoginAfterSignup(false));
×
267
      }
268
    }
269
  }, [dispatch, firstLoginAfterSignup, gettingUser, hasTrackingEnabled, organization, token, user, user.email, userId, userSettingInitialized]);
270

271
  useEffect(() => {
25✔
272
    const showOfferCookie = cookies.get('offer') === currentOffer.name;
6✔
273
    setHasOfferCookie(showOfferCookie);
6✔
274
    clearInterval(deviceTimer.current);
6✔
275
    deviceTimer.current = setInterval(() => dispatch(getAllDeviceCounts()), TIMEOUTS.refreshDefault);
276✔
276
    return () => {
6✔
277
      clearInterval(deviceTimer.current);
6✔
278
    };
279
  }, [dispatch]);
280

281
  const onSearch = useCallback((searchTerm, refreshTrigger) => dispatch(setSearchState({ refreshTrigger, searchTerm, page: 1 })), [dispatch]);
25✔
282

283
  const setHideOffer = () => {
25✔
UNCOV
284
    cookies.set('offer', currentOffer.name, { path: '/', maxAge: 2629746 });
×
UNCOV
285
    setHasOfferCookie(true);
×
286
  };
287

288
  const showOffer =
289
    isHosted && moment().isBefore(currentOffer.expires) && (organization.trial ? currentOffer.trial : currentOffer[organization.plan]) && !hasOfferCookie;
25!
290

291
  const headerLogo = isDarkMode(mode) ? (isEnterprise ? whiteEnterpriseLogo : whiteLogo) : isEnterprise ? enterpriseLogo : logo;
25!
292

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

330
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