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

mendersoftware / gui / 901187442

pending completion
901187442

Pull #3795

gitlab-ci

mzedel
feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

Ticket: None
Changelog: None
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3795: feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

4389 of 6365 branches covered (68.96%)

5 of 5 new or added lines in 1 file covered. (100.0%)

1729 existing lines in 165 files now uncovered.

8274 of 10019 relevant lines covered (82.58%)

144.86 hits per line

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

73.53
/src/js/components/app.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, { useEffect, useState } from 'react';
15
import { useIdleTimer, workerTimers } from 'react-idle-timer';
16
import { useDispatch, useSelector } from 'react-redux';
17
import { useLocation, useNavigate } from 'react-router-dom';
18

19
import { CssBaseline } from '@mui/material';
20
import { ThemeProvider, createTheme } from '@mui/material/styles';
21
import withStyles from '@mui/styles/withStyles';
22
import { makeStyles } from 'tss-react/mui';
23

24
import Cookies from 'universal-cookie';
25

26
import { parseEnvironmentInfo, setSnackbar } from '../actions/appActions';
27
import { logoutUser, setAccountActivationCode, setShowConnectingDialog } from '../actions/userActions';
28
import { expirySet, getToken, updateMaxAge } from '../auth';
29
import SharedSnackbar from '../components/common/sharedsnackbar';
30
import { PrivateRoutes, PublicRoutes } from '../config/routes';
31
import { onboardingSteps } from '../constants/onboardingConstants';
32
import ErrorBoundary from '../errorboundary';
33
import { toggle } from '../helpers';
34
import { getOnboardingState, getUserSettings } from '../selectors';
35
import { dark as darkTheme, light as lightTheme } from '../themes/Mender';
36
import Tracking from '../tracking';
37
import { getOnboardingComponentFor } from '../utils/onboardingmanager';
38
import ConfirmDismissHelptips from './common/dialogs/confirmdismisshelptips';
39
import DeviceConnectionDialog from './common/dialogs/deviceconnectiondialog';
40
import Footer from './footer';
41
import Header from './header/header';
42
import LeftNav from './leftnav';
43
import SearchResult from './search-result';
44
import Uploads from './uploads';
45

46
const activationPath = '/activate';
2✔
47
export const timeout = 900000; // 15 minutes idle time
2✔
48
const cookies = new Cookies();
2✔
49

50
const reducePalette =
51
  prefix =>
2✔
52
  (accu, [key, value]) => {
782✔
53
    if (value instanceof Object) {
3,332✔
54
      return {
748✔
55
        ...accu,
56
        ...Object.entries(value).reduce(reducePalette(`${prefix}-${key}`), {})
57
      };
58
    } else {
59
      accu[`${prefix}-${key}`] = value;
2,584✔
60
    }
61
    return accu;
2,584✔
62
  };
63

64
const cssVariables = ({ palette }) => {
2✔
65
  const muiVariables = Object.entries(palette).reduce(reducePalette('--mui'), {});
34✔
66
  return {
34✔
67
    '@global': {
68
      ':root': {
69
        ...muiVariables,
70
        '--mui-overlay': palette.grey[400]
71
      }
72
    }
73
  };
74
};
75

76
const WrappedBaseline = withStyles(cssVariables)(CssBaseline);
2✔
77

78
const useStyles = makeStyles()(() => ({
7✔
79
  public: {
80
    display: 'grid',
81
    gridTemplateRows: 'max-content 1fr max-content',
82
    height: '100vh',
83
    '.content': {
84
      alignSelf: 'center',
85
      justifySelf: 'center'
86
    }
87
  }
88
}));
89

90
export const AppRoot = () => {
2✔
91
  const [showSearchResult, setShowSearchResult] = useState(false);
59✔
92
  const navigate = useNavigate();
57✔
93
  const { pathname = '', hash } = useLocation();
57!
94

95
  const dispatch = useDispatch();
57✔
96
  const currentUser = useSelector(state => state.users.currentUser);
504✔
97
  const onboardingState = useSelector(getOnboardingState);
57✔
98
  const showDismissHelptipsDialog = useSelector(state => !state.onboarding.complete && state.onboarding.showTipsDialog);
504✔
99
  const showDeviceConnectionDialog = useSelector(state => state.users.showConnectDeviceDialog);
504✔
100
  const snackbar = useSelector(state => state.app.snackbar);
504✔
101
  const trackingCode = useSelector(state => state.app.trackerCode);
504✔
102
  const { mode } = useSelector(getUserSettings);
57✔
103

104
  const alwaysForward = () => navigate(1);
57✔
105

106
  useEffect(() => {
57✔
107
    window.addEventListener('popstate', alwaysForward);
5✔
108
    return () => {
5✔
109
      window.removeEventListener('popstate', alwaysForward);
5✔
110
    };
111
  }, []);
112

113
  useEffect(() => {
57✔
114
    dispatch(parseEnvironmentInfo());
7✔
115
    if (!trackingCode) {
7✔
116
      return;
4✔
117
    }
118
    if (!cookies.get('_ga')) {
3!
UNCOV
119
      Tracking.cookieconsent().then(({ trackingConsentGiven }) => {
×
UNCOV
120
        if (trackingConsentGiven) {
×
UNCOV
121
          Tracking.initialize(trackingCode);
×
UNCOV
122
          Tracking.pageview();
×
123
        }
124
      });
125
    } else {
126
      Tracking.initialize(trackingCode);
3✔
127
    }
128
    trackLocationChange(pathname);
3✔
129
  }, [trackingCode]);
130

131
  useEffect(() => {
57✔
132
    trackLocationChange(pathname);
5✔
133
    // the following is added to ensure backwards capability for hash containing routes & links (e.g. /ui/#/devices => /ui/devices)
134
    if (hash) {
5!
UNCOV
135
      navigate(hash.substring(1));
×
136
    }
137
  }, [hash, pathname]);
138

139
  const trackLocationChange = pathname => {
57✔
140
    let page = pathname;
8✔
141
    // if we're on page whose path might contain sensitive device/ group/ deployment names etc. we sanitize the sent information before submission
142
    if (page.includes('=') && (page.startsWith('/devices') || page.startsWith('/deployments'))) {
8!
UNCOV
143
      const splitter = page.lastIndexOf('/');
×
UNCOV
144
      const filters = page.slice(splitter + 1);
×
UNCOV
145
      const keyOnlyFilters = filters.split('&').reduce((accu, item) => `${accu}:${item.split('=')[0]}&`, ''); // assume the keys to filter by are not as revealing as the values things are filtered by
×
UNCOV
146
      page = `${page.substring(0, splitter)}?${keyOnlyFilters.substring(0, keyOnlyFilters.length - 1)}`; // cut off the last & of the reduced filters string
×
147
    } else if (page.startsWith(activationPath)) {
8!
UNCOV
148
      dispatch(setAccountActivationCode(page.substring(activationPath.length + 1)));
×
UNCOV
149
      navigate('/settings/my-profile', { replace: true });
×
150
    }
151
    Tracking.pageview(page);
8✔
152
  };
153

154
  const onIdle = () => {
57✔
155
    if (expirySet() && currentUser) {
2!
156
      // logout user and warn
157
      return dispatch(logoutUser('Your session has expired. You have been automatically logged out due to inactivity.')).catch(updateMaxAge);
2✔
158
    }
159
  };
160

161
  useIdleTimer({ crossTab: true, onAction: updateMaxAge, onActive: updateMaxAge, onIdle, syncTimers: 400, timeout, timers: workerTimers });
57✔
162

163
  const onToggleSearchResult = () => setShowSearchResult(toggle);
57✔
164

165
  const onboardingComponent = getOnboardingComponentFor(onboardingSteps.ARTIFACT_CREATION_DIALOG, onboardingState);
57✔
166
  const theme = createTheme(mode === 'dark' ? darkTheme : lightTheme);
57!
167

168
  const { classes } = useStyles();
57✔
169

170
  return (
57✔
171
    <ThemeProvider theme={theme}>
172
      <WrappedBaseline enableColorScheme />
173
      <>
174
        {getToken() ? (
57✔
175
          <div id="app">
176
            <Header mode={mode} />
177
            <LeftNav />
178
            <div className="rightFluid container">
179
              <ErrorBoundary>
180
                <SearchResult onToggleSearchResult={onToggleSearchResult} open={showSearchResult} />
181
                <PrivateRoutes />
182
              </ErrorBoundary>
183
            </div>
184
            {onboardingComponent ? onboardingComponent : null}
55!
185
            {showDismissHelptipsDialog && <ConfirmDismissHelptips />}
55!
UNCOV
186
            {showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => dispatch(setShowConnectingDialog(false))} />}
×
187
          </div>
188
        ) : (
189
          <div className={classes.public}>
190
            <PublicRoutes />
191
            <Footer />
192
          </div>
193
        )}
UNCOV
194
        <SharedSnackbar snackbar={snackbar} setSnackbar={message => dispatch(setSnackbar(message))} />
×
195
        <Uploads />
196
      </>
197
    </ThemeProvider>
198
  );
199
};
200

201
export default AppRoot;
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