• 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

80.52
/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, { useCallback, useEffect, useState } from 'react';
15
import { useIdleTimer, workerTimers } from 'react-idle-timer';
16
import { Provider, useDispatch, useSelector } from 'react-redux';
17
import { BrowserRouter, useLocation, useNavigate } from 'react-router-dom';
18

19
import createCache from '@emotion/cache';
20
import { CacheProvider } from '@emotion/react';
21
import { CssBaseline, GlobalStyles, ThemeProvider, createTheme, styled } from '@mui/material';
22
import { LocalizationProvider } from '@mui/x-date-pickers';
23
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
24
import { makeStyles } from 'tss-react/mui';
25

26
import Cookies from 'universal-cookie';
27

28
import { parseEnvironmentInfo, setSnackbar } from '../actions/appActions';
29
import { logoutUser, setAccountActivationCode, setShowConnectingDialog } from '../actions/userActions';
30
import { getSessionInfo, maxSessionAge, updateMaxAge } from '../auth';
31
import SharedSnackbar from '../components/common/sharedsnackbar';
32
import { PrivateRoutes, PublicRoutes } from '../config/routes';
33
import { TIMEOUTS } from '../constants/appConstants';
34
import ErrorBoundary from '../errorboundary';
35
import { isDarkMode, toggle } from '../helpers';
36
import store from '../reducers';
37
import { getCurrentSession, getCurrentUser, getUserSettings } from '../selectors';
38
import { dark as darkTheme, light as lightTheme } from '../themes/Mender';
39
import Tracking from '../tracking';
40
import ConfirmDismissHelptips from './common/dialogs/confirmdismisshelptips';
41
import DeviceConnectionDialog from './common/dialogs/deviceconnectiondialog';
42
import StartupNotificationDialog from './common/dialogs/startupnotification';
43
import Footer from './footer';
44
import Header from './header/header';
45
import LeftNav from './leftnav';
46
import SearchResult from './search-result';
47
import Uploads from './uploads';
48

49
const cache = createCache({ key: 'mui', prepend: true });
1✔
50

51
const activationPath = '/activate';
1✔
52
const trackingBlacklist = [/\/password\/.+/i];
1✔
53
const timeout = maxSessionAge * 1000; // 15 minutes idle time
1✔
54
const cookies = new Cookies();
1✔
55

56
const reducePalette =
57
  prefix =>
1✔
58
  (accu, [key, value]) => {
874✔
59
    if (value instanceof Object) {
3,838✔
60
      return {
836✔
61
        ...accu,
62
        ...Object.entries(value).reduce(reducePalette(`${prefix}-${key}`), {})
63
      };
64
    } else {
65
      accu[`${prefix}-${key}`] = value;
3,002✔
66
    }
67
    return accu;
3,002✔
68
  };
69

70
const cssVariables = ({ theme: { palette } }) => {
1✔
71
  const muiVariables = Object.entries(palette).reduce(reducePalette('--mui'), {});
38✔
72
  return {
38✔
73
    '@global': {
74
      ':root': {
75
        ...muiVariables,
76
        '--mui-overlay': palette.grey[400]
77
      }
78
    }
79
  };
80
};
81

82
const WrappedBaseline = styled(CssBaseline)(cssVariables);
1✔
83

84
const useStyles = makeStyles()(() => ({
5✔
85
  public: {
86
    display: 'grid',
87
    gridTemplateRows: 'max-content 1fr max-content',
88
    height: '100vh',
89
    '.content': {
90
      alignSelf: 'center',
91
      justifySelf: 'center'
92
    }
93
  }
94
}));
95

96
export const AppRoot = () => {
1✔
97
  const [showSearchResult, setShowSearchResult] = useState(false);
19✔
98
  const navigate = useNavigate();
19✔
99
  const { pathname = '', hash } = useLocation();
19!
100

101
  const dispatch = useDispatch();
19✔
102
  const { id: currentUser } = useSelector(getCurrentUser);
19✔
103
  const showDismissHelptipsDialog = useSelector(state => !state.onboarding.complete && state.onboarding.showTipsDialog);
2,148✔
104
  const showDeviceConnectionDialog = useSelector(state => state.users.showConnectDeviceDialog);
2,148✔
105
  const showStartupNotification = useSelector(state => state.users.showStartupNotification);
2,148✔
106
  const snackbar = useSelector(state => state.app.snackbar);
2,148✔
107
  const trackingCode = useSelector(state => state.app.trackerCode);
2,148✔
108
  const { mode } = useSelector(getUserSettings);
19✔
109
  const { token: storedToken } = getSessionInfo();
19✔
110
  const { expiresAt, token = storedToken } = useSelector(getCurrentSession);
19✔
111

112
  const trackLocationChange = useCallback(
19✔
113
    pathname => {
114
      let page = pathname;
6✔
115
      // if we're on page whose path might contain sensitive device/ group/ deployment names etc. we sanitize the sent information before submission
116
      if (page.includes('=') && (page.startsWith('/devices') || page.startsWith('/deployments'))) {
6!
UNCOV
117
        const splitter = page.lastIndexOf('/');
×
UNCOV
118
        const filters = page.slice(splitter + 1);
×
UNCOV
119
        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
120
        page = `${page.substring(0, splitter)}?${keyOnlyFilters.substring(0, keyOnlyFilters.length - 1)}`; // cut off the last & of the reduced filters string
×
121
      } else if (page.startsWith(activationPath)) {
6!
UNCOV
122
        dispatch(setAccountActivationCode(page.substring(activationPath.length + 1)));
×
UNCOV
123
        navigate('/settings/my-profile', { replace: true });
×
124
      } else if (trackingBlacklist.some(item => !!page.match(item))) {
6!
UNCOV
125
        return;
×
126
      }
127
      Tracking.pageview(page);
6✔
128
    },
129
    [dispatch, navigate]
130
  );
131

132
  useEffect(() => {
19✔
133
    dispatch(parseEnvironmentInfo());
6✔
134
    if (!trackingCode) {
6✔
135
      return;
3✔
136
    }
137
    if (!cookies.get('_ga')) {
3!
138
      Tracking.cookieconsent().then(({ trackingConsentGiven }) => {
3✔
UNCOV
139
        if (trackingConsentGiven) {
×
UNCOV
140
          Tracking.initialize(trackingCode);
×
UNCOV
141
          Tracking.pageview();
×
142
        }
143
      });
144
    } else {
UNCOV
145
      Tracking.initialize(trackingCode);
×
146
    }
147
  }, [dispatch, trackingCode]);
148

149
  useEffect(() => {
19✔
150
    if (!(trackingCode && cookies.get('_ga'))) {
7!
151
      return;
7✔
152
    }
UNCOV
153
    trackLocationChange(pathname);
×
154
  }, [pathname, trackLocationChange, trackingCode]);
155

156
  useEffect(() => {
19✔
157
    trackLocationChange(pathname);
6✔
158
    // the following is added to ensure backwards capability for hash containing routes & links (e.g. /ui/#/devices => /ui/devices)
159
    if (hash) {
6!
UNCOV
160
      navigate(hash.substring(1));
×
161
    }
162
  }, [hash, navigate, pathname, trackLocationChange]);
163

164
  const updateExpiryDate = useCallback(() => updateMaxAge({ expiresAt, token }), [expiresAt, token]);
19✔
165

166
  const onIdle = useCallback(() => {
19✔
167
    if (!!expiresAt && currentUser) {
4✔
168
      // logout user and warn
169
      return dispatch(logoutUser())
1✔
170
        .catch(updateExpiryDate)
171
        .then(() => {
172
          navigate('//'); // double / to ensure the logged out URL conforms to `/ui/` in order to not trigger a redirect and potentially use state
1✔
173
          // async snackbar setting to ensure the login screen has loaded as the snackbar might be cleared by other actions otherwise
174
          setTimeout(() => dispatch(setSnackbar('Your session has expired. You have been automatically logged out due to inactivity.')), TIMEOUTS.oneSecond);
1✔
175
        });
176
    }
177
  }, [currentUser, dispatch, expiresAt, navigate, updateExpiryDate]);
178

179
  useIdleTimer({ crossTab: true, onAction: updateExpiryDate, onActive: updateExpiryDate, onIdle, syncTimers: 400, timeout, timers: workerTimers });
19✔
180

181
  const onToggleSearchResult = () => setShowSearchResult(toggle);
19✔
182

183
  const theme = createTheme(isDarkMode(mode) ? darkTheme : lightTheme);
19!
184

185
  const { classes } = useStyles();
19✔
186
  const globalCssVars = cssVariables({ theme })['@global'];
19✔
187

188
  return (
19✔
189
    <ThemeProvider theme={theme}>
190
      <WrappedBaseline enableColorScheme />
191
      <GlobalStyles styles={globalCssVars} />
192
      <>
193
        {token ? (
19✔
194
          <div id="app">
195
            <Header mode={mode} />
196
            <LeftNav />
197
            <div className="rightFluid container">
198
              <ErrorBoundary>
199
                <SearchResult onToggleSearchResult={onToggleSearchResult} open={showSearchResult} />
200
                <PrivateRoutes />
201
              </ErrorBoundary>
202
            </div>
203
            {showDismissHelptipsDialog && <ConfirmDismissHelptips />}
15!
UNCOV
204
            {showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => dispatch(setShowConnectingDialog(false))} />}
×
205
            {showStartupNotification && <StartupNotificationDialog />}
16✔
206
          </div>
207
        ) : (
208
          <div className={classes.public}>
209
            <PublicRoutes />
210
            <Footer />
211
          </div>
212
        )}
UNCOV
213
        <SharedSnackbar snackbar={snackbar} setSnackbar={message => dispatch(setSnackbar(message))} />
×
214
        <Uploads />
215
      </>
216
    </ThemeProvider>
217
  );
218
};
219

220
export const AppProviders = ({ basename = 'ui' }) => (
1!
221
  <React.StrictMode>
1✔
222
    <Provider store={store}>
223
      <CacheProvider value={cache}>
224
        <LocalizationProvider dateAdapter={AdapterMoment}>
225
          <ErrorBoundary>
226
            <BrowserRouter basename={basename}>
227
              <AppRoot />
228
            </BrowserRouter>
229
          </ErrorBoundary>
230
        </LocalizationProvider>
231
      </CacheProvider>
232
    </Provider>
233
  </React.StrictMode>
234
);
235

236
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