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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

94.85
/frontend/src/js/components/App.tsx
1
// Copyright 2015 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import React, { useCallback, useEffect, useState } from 'react';
2✔
15
import { useIdleTimer, workerTimers } from 'react-idle-timer';
2✔
16
import { Provider, useDispatch, useSelector } from 'react-redux';
2✔
17
import { BrowserRouter, useLocation, useNavigate } from 'react-router-dom';
2✔
18

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

2✔
26
import SharedSnackbar from '@northern.tech/common-ui/SharedSnackbar';
2✔
27
import ConfirmDismissHelptips from '@northern.tech/common-ui/dialogs/ConfirmDismissHelpTips';
2✔
28
import FeedbackDialog from '@northern.tech/common-ui/dialogs/Feedback';
2✔
29
import StartupNotificationDialog from '@northern.tech/common-ui/dialogs/StartupNotification';
2✔
30
import storeActions from '@northern.tech/store/actions';
2✔
31
import { SentryConfig } from '@northern.tech/store/appSlice';
2✔
32
import { getSessionInfo, updateMaxAge } from '@northern.tech/store/auth';
2✔
33
import { DARK_MODE, LIGHT_MODE, TIMEOUTS, maxSessionAge } from '@northern.tech/store/constants';
2✔
34
import {
2✔
35
  getCommit,
2✔
36
  getCurrentSession,
2✔
37
  getCurrentUser,
2✔
38
  getIsDarkMode,
2✔
39
  getIsPreview,
2✔
40
  getIsServiceProvider,
2✔
41
  getOrganization,
2✔
42
  getSentryConfig,
2✔
43
  getSnackbar,
2✔
44
  getTrackerCode
2✔
45
} from '@northern.tech/store/selectors';
2✔
46
import { store } from '@northern.tech/store/store';
2✔
47
import { parseEnvironmentInfo } from '@northern.tech/store/storehooks';
2✔
48
import { logoutUser } from '@northern.tech/store/thunks';
2✔
49
import { dark as darkTheme, light as lightTheme } from '@northern.tech/themes/Mender';
2✔
50
import { dark as nextDarkTheme, light as nextLightTheme } from '@northern.tech/themes/MenderNext';
2✔
51
import { toggle } from '@northern.tech/utils/helpers';
2✔
52
import { browserTracingIntegration, replayIntegration, setUser } from '@sentry/react';
2✔
53
import Cookies from 'universal-cookie';
2✔
54

2✔
55
import ErrorBoundary from '../ErrorBoundary';
2✔
56
import { PrivateRoutes, PrivateSPRoutes, PublicRoutes } from '../config/routes';
2✔
57
import Tracking from '../tracking';
2✔
58
import Footer from './Footer';
2✔
59
import LeftNav from './LeftNav';
2✔
60
import SearchResult from './SearchResult';
2✔
61
import Uploads from './Uploads';
2✔
62
import DeviceConnectionDialog from './devices/dialogs/DeviceConnectionDialog';
2✔
63
import Header from './header/Header';
2✔
64

2✔
65
const { receivedActivationCode, setShowConnectingDialog, setSnackbar } = storeActions;
3✔
66

2✔
67
const cache = createCache({ key: 'mui', prepend: true });
3✔
68

2✔
69
const activationPath = '/activate';
3✔
70
const trackingBlacklist = [/\/password\/.+/i];
3✔
71
const timeout = maxSessionAge * 1000; // 15 minutes idle time
3✔
72
const cookies = new Cookies();
3✔
73

2✔
74
const reducePalette =
2✔
75
  prefix =>
3✔
76
  (accu, [key, value]) => {
1,004✔
77
    if (value instanceof Object) {
5,774✔
78
      return {
950✔
79
        ...accu,
2✔
80
        ...Object.entries(value).reduce(reducePalette(`${prefix}-${key}`), {})
2✔
81
      };
2✔
82
    } else {
2✔
83
      accu[`${prefix}-${key}`] = value;
4,826✔
84
    }
2✔
85
    return accu;
4,826✔
86
  };
2✔
87

2✔
88
const cssVariables = ({ theme: { palette } }) => {
3✔
89
  const muiVariables = Object.entries(palette).reduce(reducePalette('--mui'), {});
56✔
90
  return {
56✔
91
    '@global': {
2✔
92
      ':root': {
2✔
93
        ...muiVariables,
2✔
94
        '--mui-overlay': palette.grey[400]
2✔
95
      }
2✔
96
    }
2✔
97
  };
2✔
98
};
2✔
99

2✔
100
const WrappedBaseline = styled(CssBaseline)(cssVariables);
3✔
101

2✔
102
const useStyles = makeStyles()(() => ({
7✔
103
  public: {
2✔
104
    display: 'grid',
2✔
105
    gridTemplateRows: 'max-content 1fr max-content',
2✔
106
    height: '100vh',
2✔
107
    '.content': {
2✔
108
      alignSelf: 'center',
2✔
109
      justifySelf: 'center'
2✔
110
    }
2✔
111
  }
2✔
112
}));
2✔
113

2✔
114
const initSentry = async ({ commit, location, replaysSessionSampleRate, tracesSampleRate }: SentryConfig & { commit: string }) => {
3✔
115
  const Sentry = await import(/* webpackChunkName: "@sentry/react" */ '@sentry/react');
2✔
116
  Sentry.init({
2✔
117
    dsn: location,
2✔
118
    integrations: [browserTracingIntegration(), replayIntegration({ networkDetailAllowUrls: [window.location.origin] })],
2✔
119
    release: `mender-frontend@${commit}`,
2✔
120
    tracesSampleRate, // defaults to capturing 100% of the transactions
2✔
121
    tracePropagationTargets: ['localhost', /^https:\/\/(\w*\.)*hosted\.mender\.io/, 'https://docker.mender.io'],
2✔
122
    replaysSessionSampleRate, // defaults to 0.1 in the environment settings, to be adjusted externally
2✔
123
    replaysOnErrorSampleRate: 1.0 // always change the sample rate to 100% when sampling sessions where errors occur
2✔
124
  });
2✔
125
};
2✔
126

2✔
127
const THEMES = {
3✔
128
  default: {
2✔
129
    [LIGHT_MODE]: lightTheme,
2✔
130
    [DARK_MODE]: darkTheme
2✔
131
  },
2✔
132
  next: {
2✔
133
    [LIGHT_MODE]: nextLightTheme,
2✔
134
    [DARK_MODE]: nextDarkTheme
2✔
135
  }
2✔
136
};
2✔
137

2✔
138
export const AppRoot = () => {
3✔
139
  const [showSearchResult, setShowSearchResult] = useState(false);
30✔
140
  const navigate = useNavigate();
30✔
141
  const { pathname = '', hash } = useLocation();
30✔
142

2✔
143
  const dispatch = useDispatch();
30✔
144
  const { id: currentUser } = useSelector(getCurrentUser);
30✔
145
  const showDismissHelptipsDialog = useSelector(state => !state.onboarding.complete && state.onboarding.showTipsDialog);
508✔
146
  const showDeviceConnectionDialog = useSelector(state => state.users.showConnectDeviceDialog);
508✔
147
  const showStartupNotification = useSelector(state => state.users.showStartupNotification);
508✔
148
  const showFeedbackDialog = useSelector(state => state.users.showFeedbackDialog);
508✔
149
  const snackbar = useSelector(getSnackbar);
30✔
150
  const trackingCode = useSelector(getTrackerCode);
30✔
151
  const { location: sentryLocation, replaysSessionSampleRate, tracesSampleRate } = useSelector(getSentryConfig);
30✔
152
  const commit = useSelector(getCommit);
30✔
153
  const isDarkMode = useSelector(getIsDarkMode);
30✔
154
  const { token: storedToken } = getSessionInfo();
30✔
155
  const { expiresAt, token = storedToken } = useSelector(getCurrentSession);
30✔
156
  const { id: tenantId } = useSelector(getOrganization);
30✔
157
  const isPreview = useSelector(getIsPreview);
30✔
158

2✔
159
  useEffect(() => {
30✔
160
    const loadThemeStyles = async () => {
10✔
161
      if (isPreview) {
10✔
162
        await import('@northern.tech/themes/MenderNext/styles/main.css');
7✔
163
      } else {
2✔
164
        await import('@northern.tech/themes/Mender/styles/main.css');
5✔
165
      }
2✔
166
    };
2✔
167
    loadThemeStyles();
10✔
168
  }, [isPreview]);
2✔
169

2✔
170
  const trackLocationChange = useCallback(
30✔
171
    pathname => {
2✔
172
      let page = pathname;
7✔
173
      // if we're on page whose path might contain sensitive device/ group/ deployment names etc. we sanitize the sent information before submission
2✔
174
      if (page.includes('=') && (page.startsWith('/devices') || page.startsWith('/deployments'))) {
7!
175
        const splitter = page.lastIndexOf('/');
2✔
176
        const filters = page.slice(splitter + 1);
2✔
177
        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
2✔
178
        page = `${page.substring(0, splitter)}?${keyOnlyFilters.substring(0, keyOnlyFilters.length - 1)}`; // cut off the last & of the reduced filters string
2✔
179
      } else if (page.startsWith(activationPath)) {
7!
180
        dispatch(receivedActivationCode(page.substring(activationPath.length + 1)));
2✔
181
        navigate('/settings/my-profile', { replace: true });
2✔
182
      } else if (trackingBlacklist.some(item => !!page.match(item))) {
7!
183
        return;
2✔
184
      }
2✔
185
      Tracking.pageview(page);
7✔
186
    },
2✔
187
    [dispatch, navigate]
2✔
188
  );
2✔
189

2✔
190
  useEffect(() => {
30✔
191
    dispatch(parseEnvironmentInfo());
7✔
192
    if (!trackingCode) {
7✔
193
      return;
4✔
194
    }
2✔
195
    if (!cookies.get('_ga')) {
5!
196
      Tracking.cookieconsent().then(({ trackingConsentGiven }) => {
5✔
197
        if (trackingConsentGiven) {
2!
198
          Tracking.initialize(trackingCode);
2✔
199
          Tracking.pageview();
2✔
200
        }
2✔
201
      });
2✔
202
    } else {
2✔
203
      Tracking.initialize(trackingCode);
2✔
204
    }
2✔
205
  }, [dispatch, trackingCode]);
2✔
206

2✔
207
  useEffect(() => {
30✔
208
    if (!(sentryLocation && commit)) {
6!
209
      return;
6✔
210
    }
2✔
211
    initSentry({ commit, location: sentryLocation, replaysSessionSampleRate, tracesSampleRate });
2✔
212
  }, [commit, sentryLocation, replaysSessionSampleRate, tracesSampleRate]);
2✔
213

2✔
214
  useEffect(() => {
30✔
215
    if (sentryLocation) {
8!
216
      setUser({ tenantId });
2✔
217
    }
2✔
218
  }, [sentryLocation, tenantId]);
2✔
219

2✔
220
  useEffect(() => {
30✔
221
    if (!(trackingCode && cookies.get('_ga'))) {
8!
222
      return;
8✔
223
    }
2✔
224
    trackLocationChange(pathname);
2✔
225
  }, [pathname, trackLocationChange, trackingCode]);
2✔
226

2✔
227
  useEffect(() => {
30✔
228
    trackLocationChange(pathname);
7✔
229
    // the following is added to ensure backwards capability for hash containing routes & links (e.g. /ui/#/devices => /ui/devices)
2✔
230
    if (hash) {
7!
231
      navigate(hash.substring(1));
2✔
232
    }
2✔
233
  }, [hash, navigate, pathname, trackLocationChange]);
2✔
234

2✔
235
  const updateExpiryDate = useCallback(() => updateMaxAge({ expiresAt, token }), [expiresAt, token]);
30✔
236

2✔
237
  const onIdle = useCallback(() => {
30✔
238
    if (!!expiresAt && currentUser) {
4✔
239
      // logout user and warn
2✔
240
      return dispatch(logoutUser())
3✔
241
        .catch(updateExpiryDate)
2✔
242
        .then(() => {
2✔
243
          navigate('//'); // double / to ensure the logged out URL conforms to `/ui/` in order to not trigger a redirect and potentially use state
3✔
244
          // async snackbar setting to ensure the login screen has loaded as the snackbar might be cleared by other actions otherwise
2✔
245
          setTimeout(() => dispatch(setSnackbar('Your session has expired. You have been automatically logged out due to inactivity.')), TIMEOUTS.oneSecond);
3✔
246
        });
2✔
247
    }
2✔
248
  }, [currentUser, dispatch, expiresAt, navigate, updateExpiryDate]);
2✔
249

2✔
250
  useIdleTimer({ crossTab: true, onAction: updateExpiryDate, onActive: updateExpiryDate, onIdle, syncTimers: 400, timeout, timers: workerTimers });
30✔
251

2✔
252
  const onToggleSearchResult = () => setShowSearchResult(toggle);
30✔
253

2✔
254
  const theme = createTheme(THEMES[isPreview ? 'next' : 'default'][isDarkMode ? DARK_MODE : LIGHT_MODE] || THEMES.default.light);
30!
255

2✔
256
  const { classes } = useStyles();
30✔
257
  const globalCssVars = cssVariables({ theme })['@global'];
30✔
258

2✔
259
  const dispatchedSetSnackbar = useCallback(message => dispatch(setSnackbar(message)), [dispatch]);
30✔
260
  const isSP = useSelector(getIsServiceProvider);
30✔
261

2✔
262
  return (
30✔
263
    <ThemeProvider theme={theme}>
2✔
264
      <WrappedBaseline enableColorScheme />
2✔
265
      <GlobalStyles styles={globalCssVars} />
2✔
266
      <>
2✔
267
        {token ? (
2✔
268
          <div id="app">
2✔
269
            <Header isDarkMode={isDarkMode} />
2✔
270
            <LeftNav />
2✔
271
            <div className="rightFluid container">
2✔
272
              <ErrorBoundary>
2✔
273
                <SearchResult onToggleSearchResult={onToggleSearchResult} open={showSearchResult} />
2✔
274
                {isSP ? <PrivateSPRoutes /> : <PrivateRoutes />}
2!
275
              </ErrorBoundary>
2✔
276
            </div>
2✔
277
            {showDismissHelptipsDialog && <ConfirmDismissHelptips />}
2!
UNCOV
278
            {showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => dispatch(setShowConnectingDialog(false))} />}
2!
279
            {showStartupNotification && <StartupNotificationDialog />}
2✔
280
            {showFeedbackDialog && <FeedbackDialog />}
2!
281
          </div>
2✔
282
        ) : (
2✔
283
          <div className={classes.public}>
2✔
284
            <PublicRoutes />
2✔
285
            <Footer />
2✔
286
          </div>
2✔
287
        )}
2✔
288
        <SharedSnackbar snackbar={snackbar} setSnackbar={dispatchedSetSnackbar} />
2✔
289
        <Uploads />
2✔
290
      </>
2✔
291
    </ThemeProvider>
2✔
292
  );
2✔
293
};
2✔
294

2✔
295
export const AppProviders = ({ basename = 'ui' }) => (
3✔
296
  <React.StrictMode>
3✔
297
    <Provider store={store}>
2✔
298
      <CacheProvider value={cache}>
2✔
299
        <LocalizationProvider dateAdapter={AdapterDayjs}>
2✔
300
          <ErrorBoundary>
2✔
301
            <BrowserRouter basename={basename}>
2✔
302
              <AppRoot />
2✔
303
            </BrowserRouter>
2✔
304
          </ErrorBoundary>
2✔
305
        </LocalizationProvider>
2✔
306
      </CacheProvider>
2✔
307
    </Provider>
2✔
308
  </React.StrictMode>
2✔
309
);
2✔
310

2✔
311
export default AppRoot;
2✔
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