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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

85.6
/src/js/actions/appActions.js
1
// Copyright 2019 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 Cookies from 'universal-cookie';
15

16
import GeneralApi from '../api/general-api';
17
import { getSessionInfo, getToken } from '../auth';
18
import {
19
  SET_ENVIRONMENT_DATA,
20
  SET_FEATURES,
21
  SET_FIRST_LOGIN_AFTER_SIGNUP,
22
  SET_OFFLINE_THRESHOLD,
23
  SET_SEARCH_STATE,
24
  SET_SNACKBAR,
25
  SET_VERSION_INFORMATION,
26
  TIMEOUTS
27
} from '../constants/appConstants';
28
import { DEPLOYMENT_STATES } from '../constants/deploymentConstants';
29
import { DEVICE_STATES } from '../constants/deviceConstants';
30
import { onboardingSteps } from '../constants/onboardingConstants';
31
import { SET_TOOLTIPS_STATE, SUCCESSFULLY_LOGGED_IN } from '../constants/userConstants';
32
import { deepCompare, extractErrorMessage, preformatWithRequestID, stringToBoolean } from '../helpers';
33
import { getFeatures, getIsEnterprise, getOfflineThresholdSettings, getUserSettings as getUserSettingsSelector } from '../selectors';
34
import { getOnboardingComponentFor } from '../utils/onboardingmanager';
35
import { getDeploymentsByStatus } from './deploymentActions';
36
import {
37
  getDeviceAttributes,
38
  getDeviceById,
39
  getDeviceLimit,
40
  getDevicesByStatus,
41
  getDynamicGroups,
42
  getGroups,
43
  searchDevices,
44
  setDeviceListState
45
} from './deviceActions';
46
import { getOnboardingState, setDemoArtifactPort, setOnboardingComplete } from './onboardingActions';
47
import { getIntegrations, getUserOrganization } from './organizationActions';
48
import { getReleases } from './releaseActions';
49
import { getGlobalSettings, getRoles, getUserSettings, saveGlobalSettings, saveUserSettings } from './userActions';
50

51
const cookies = new Cookies();
183✔
52

53
export const commonErrorFallback = 'Please check your connection.';
183✔
54
export const commonErrorHandler = (err, errorContext, dispatch, fallback, mightBeAuthRelated = false) => {
183✔
55
  const errMsg = extractErrorMessage(err, fallback);
6✔
56
  if (mightBeAuthRelated || getToken()) {
6!
57
    dispatch(setSnackbar(preformatWithRequestID(err.response, `${errorContext} ${errMsg}`), null, 'Copy to clipboard'));
6✔
58
  }
59
  return Promise.reject(err);
6✔
60
};
61

62
const getComparisonCompatibleVersion = version => (isNaN(version.charAt(0)) && version !== 'next' ? 'master' : version);
183✔
63

64
const featureFlags = [
183✔
65
  'hasAddons',
66
  'hasAuditlogs',
67
  'hasMultitenancy',
68
  'hasDeltaProgress',
69
  'hasDeviceConfig',
70
  'hasDeviceConnect',
71
  'hasReleaseTags',
72
  'hasReporting',
73
  'hasMonitor',
74
  'isEnterprise'
75
];
76
export const parseEnvironmentInfo = () => (dispatch, getState) => {
183✔
77
  const state = getState();
6✔
78
  let onboardingComplete = state.onboarding.complete || !!JSON.parse(window.localStorage.getItem('onboardingComplete') ?? 'false');
6✔
79
  let demoArtifactPort = 85;
6✔
80
  let environmentData = {};
6✔
81
  let environmentFeatures = {};
6✔
82
  let versionInfo = {};
6✔
83
  if (mender_environment) {
6!
84
    const {
85
      features = {},
×
86
      demoArtifactPort: port,
87
      disableOnboarding,
88
      hostAddress,
89
      hostedAnnouncement,
90
      integrationVersion,
91
      isDemoMode,
92
      menderVersion,
93
      menderArtifactVersion,
94
      metaMenderVersion,
95
      recaptchaSiteKey,
96
      services = {},
×
97
      stripeAPIKey,
98
      trackerCode
99
    } = mender_environment;
6✔
100
    onboardingComplete = stringToBoolean(features.isEnterprise) || stringToBoolean(disableOnboarding) || onboardingComplete;
6✔
101
    demoArtifactPort = port || demoArtifactPort;
6✔
102
    environmentData = {
6✔
103
      hostedAnnouncement: hostedAnnouncement || state.app.hostedAnnouncement,
12✔
104
      hostAddress: hostAddress || state.app.hostAddress,
12✔
105
      recaptchaSiteKey: recaptchaSiteKey || state.app.recaptchaSiteKey,
12✔
106
      stripeAPIKey: stripeAPIKey || state.app.stripeAPIKey,
12✔
107
      trackerCode: trackerCode || state.app.trackerCode
12✔
108
    };
109
    environmentFeatures = {
6✔
110
      ...featureFlags.reduce((accu, flag) => ({ ...accu, [flag]: stringToBoolean(features[flag]) }), {}),
60✔
111
      // the check in features is purely kept as a local override, it shouldn't become relevant for production again
112
      isHosted: features.isHosted || window.location.hostname.includes('hosted.mender.io'),
12✔
113
      isDemoMode: stringToBoolean(isDemoMode || features.isDemoMode)
12✔
114
    };
115
    versionInfo = {
6✔
116
      docs: isNaN(integrationVersion.charAt(0)) ? '' : integrationVersion.split('.').slice(0, 2).join('.'),
6!
117
      remainder: {
118
        Integration: getComparisonCompatibleVersion(integrationVersion),
119
        'Mender-Client': getComparisonCompatibleVersion(menderVersion),
120
        'Mender-Artifact': menderArtifactVersion,
121
        'Meta-Mender': metaMenderVersion,
122
        Deployments: services.deploymentsVersion,
123
        Deviceauth: services.deviceauthVersion,
124
        Inventory: services.inventoryVersion,
125
        GUI: services.guiVersion
126
      }
127
    };
128
  }
129
  return Promise.all([
6✔
130
    dispatch({ type: SUCCESSFULLY_LOGGED_IN, value: getSessionInfo() }),
131
    dispatch(setOnboardingComplete(onboardingComplete)),
132
    dispatch(setDemoArtifactPort(demoArtifactPort)),
133
    dispatch({ type: SET_FEATURES, value: environmentFeatures }),
134
    dispatch({ type: SET_VERSION_INFORMATION, docsVersion: versionInfo.docs, value: versionInfo.remainder }),
135
    dispatch({ type: SET_ENVIRONMENT_DATA, value: environmentData }),
136
    dispatch(getLatestReleaseInfo())
137
  ]);
138
};
139

140
const maybeAddOnboardingTasks = ({ devicesByStatus, dispatch, onboardingState, tasks }) => {
183✔
141
  if (!onboardingState.showTips || onboardingState.complete) {
3!
142
    return tasks;
3✔
143
  }
144
  const welcomeTip = getOnboardingComponentFor(onboardingSteps.ONBOARDING_START, {
×
145
    progress: onboardingState.progress,
146
    complete: onboardingState.complete,
147
    showTips: onboardingState.showTips
148
  });
149
  if (welcomeTip) {
×
150
    tasks.push(dispatch(setSnackbar('open', TIMEOUTS.refreshDefault, '', welcomeTip, () => {}, true)));
×
151
  }
152
  // try to retrieve full device details for onboarding devices to ensure ips etc. are available
153
  // we only load the first few/ 20 devices, as it is possible the onboarding is left dangling
154
  // and a lot of devices are present and we don't want to flood the backend for this
155
  return devicesByStatus[DEVICE_STATES.accepted].deviceIds.reduce((accu, id) => {
×
156
    accu.push(dispatch(getDeviceById(id)));
×
157
    return accu;
×
158
  }, tasks);
159
};
160

161
const interpretAppData = () => (dispatch, getState) => {
183✔
162
  const state = getState();
3✔
163
  let { columnSelection = [], trackingConsentGiven: hasTrackingEnabled, tooltips = {} } = getUserSettingsSelector(state);
3!
164
  let settings = {};
3✔
165
  if (cookies.get('_ga') && typeof hasTrackingEnabled === 'undefined') {
3!
166
    settings.trackingConsentGiven = true;
×
167
  }
168
  let tasks = [
3✔
169
    dispatch(setDeviceListState({ selectedAttributes: columnSelection.map(column => ({ attribute: column.key, scope: column.scope })) })),
×
170
    dispatch({ type: SET_TOOLTIPS_STATE, value: tooltips }), // tooltips read state is primarily trusted from the redux store, except on app init - here user settings are the reference
171
    dispatch(saveUserSettings(settings))
172
  ];
173
  tasks = maybeAddOnboardingTasks({ devicesByStatus: state.devices.byStatus, dispatch, tasks, onboardingState: state.onboarding });
3✔
174
  // the following is used as a migration and initialization of the stored identity attribute
175
  // changing the default device attribute to the first non-deviceId attribute, unless a stored
176
  // id attribute setting exists
177
  const identityOptions = state.devices.filteringAttributes.identityAttributes.filter(attribute => !['id', 'Device ID', 'status'].includes(attribute));
3✔
178
  const { id_attribute } = state.users.globalSettings;
3✔
179
  if (!id_attribute && identityOptions.length) {
3✔
180
    tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute: identityOptions[0], scope: 'identity' } })));
1✔
181
  } else if (typeof id_attribute === 'string') {
2!
182
    let attribute = id_attribute;
×
183
    if (attribute === 'Device ID') {
×
184
      attribute = 'id';
×
185
    }
186
    tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute, scope: 'identity' } })));
×
187
  }
188
  return Promise.all(tasks);
3✔
189
};
190

191
const retrieveAppData = () => (dispatch, getState) => {
183✔
192
  let tasks = [
3✔
193
    dispatch(parseEnvironmentInfo()),
194
    dispatch(getUserSettings()),
195
    dispatch(getGlobalSettings()),
196
    dispatch(getDeviceAttributes()),
197
    dispatch(getDeploymentsByStatus(DEPLOYMENT_STATES.finished, undefined, undefined, undefined, undefined, undefined, undefined, false)),
198
    dispatch(getDeploymentsByStatus(DEPLOYMENT_STATES.inprogress)),
199
    dispatch(getDevicesByStatus(DEVICE_STATES.accepted)),
200
    dispatch(getDevicesByStatus(DEVICE_STATES.pending)),
201
    dispatch(getDevicesByStatus(DEVICE_STATES.preauth)),
202
    dispatch(getDevicesByStatus(DEVICE_STATES.rejected)),
203
    dispatch(getDynamicGroups()),
204
    dispatch(getGroups()),
205
    dispatch(getIntegrations()),
206
    dispatch(getReleases()),
207
    dispatch(getDeviceLimit()),
208
    dispatch(getRoles()),
209
    dispatch(setFirstLoginAfterSignup(stringToBoolean(cookies.get('firstLoginAfterSignup'))))
210
  ];
211
  const { hasMultitenancy, isHosted } = getFeatures(getState());
3✔
212
  const multitenancy = hasMultitenancy || isHosted || getIsEnterprise(getState());
3✔
213
  if (multitenancy) {
3✔
214
    tasks.push(dispatch(getUserOrganization()));
2✔
215
  }
216
  return Promise.all(tasks);
3✔
217
};
218

219
export const initializeAppData = () => dispatch =>
183✔
220
  dispatch(retrieveAppData())
3✔
221
    .then(() => dispatch(interpretAppData()))
3✔
222
    // this is allowed to fail if no user information are available
223
    .catch(err => console.log(extractErrorMessage(err)))
×
224
    .then(() => dispatch(getOnboardingState()));
3✔
225

226
/*
227
  General
228
*/
229
export const setSnackbar = (message, autoHideDuration, action, children, onClick, onClose) => dispatch =>
183✔
230
  dispatch({
133✔
231
    type: SET_SNACKBAR,
232
    snackbar: {
233
      open: message ? true : false,
133✔
234
      message,
235
      maxWidth: '900px',
236
      autoHideDuration,
237
      action,
238
      children,
239
      onClick,
240
      onClose
241
    }
242
  });
243

244
export const setFirstLoginAfterSignup = firstLoginAfterSignup => dispatch => {
183✔
245
  cookies.set('firstLoginAfterSignup', !!firstLoginAfterSignup, { maxAge: 60, path: '/', domain: '.mender.io', sameSite: false });
8✔
246
  dispatch({ type: SET_FIRST_LOGIN_AFTER_SIGNUP, firstLoginAfterSignup: !!firstLoginAfterSignup });
8✔
247
};
248

249
const dateFunctionMap = {
183✔
250
  getDays: 'getDate',
251
  setDays: 'setDate'
252
};
253
export const setOfflineThreshold = () => (dispatch, getState) => {
183✔
254
  const { interval, intervalUnit } = getOfflineThresholdSettings(getState());
13✔
255
  const today = new Date();
13✔
256
  const intervalName = `${intervalUnit.charAt(0).toUpperCase()}${intervalUnit.substring(1)}`;
13✔
257
  const setter = dateFunctionMap[`set${intervalName}`] ?? `set${intervalName}`;
13✔
258
  const getter = dateFunctionMap[`get${intervalName}`] ?? `get${intervalName}`;
13✔
259
  today[setter](today[getter]() - interval);
13✔
260
  let value;
261
  try {
13✔
262
    value = today.toISOString();
13✔
263
  } catch {
264
    return Promise.resolve(dispatch(setSnackbar('There was an error saving the offline threshold, please check your settings.')));
×
265
  }
266
  return Promise.resolve(dispatch({ type: SET_OFFLINE_THRESHOLD, value }));
13✔
267
};
268

269
export const setVersionInfo = info => (dispatch, getState) =>
183✔
270
  Promise.resolve(
2✔
271
    dispatch({
272
      type: SET_VERSION_INFORMATION,
273
      docsVersion: getState().app.docsVersion,
274
      value: {
275
        ...getState().app.versionInformation,
276
        ...info
277
      }
278
    })
279
  );
280

281
const versionRegex = new RegExp(/\d+\.\d+/);
183✔
282
const getLatestRelease = thing => {
183✔
283
  const latestKey = Object.keys(thing)
8✔
284
    .filter(key => versionRegex.test(key))
20✔
285
    .sort()
286
    .reverse()[0];
287
  return thing[latestKey];
8✔
288
};
289

290
const repoKeyMap = {
183✔
291
  integration: 'Integration',
292
  mender: 'Mender-Client',
293
  'mender-artifact': 'Mender-Artifact'
294
};
295

296
const deductSaasState = (latestRelease, guiTags, saasReleases) => {
183✔
297
  const latestGuiTag = guiTags.length ? guiTags[0].name : '';
4!
298
  const latestSaasRelease = latestGuiTag.startsWith('saas-v') ? { date: latestGuiTag.split('-v')[1].replaceAll('.', '-'), tag: latestGuiTag } : saasReleases[0];
4!
299
  return latestSaasRelease.date > latestRelease.release_date ? latestSaasRelease.tag : latestRelease.release;
4!
300
};
301

302
export const getLatestReleaseInfo = () => (dispatch, getState) => {
183✔
303
  if (!getState().app.features.isHosted) {
10✔
304
    return Promise.resolve();
6✔
305
  }
306
  return Promise.all([GeneralApi.get('/versions.json'), GeneralApi.get('/tags.json')])
4✔
307
    .catch(err => {
308
      console.log('init error:', extractErrorMessage(err));
×
309
      return Promise.resolve([{ data: {} }, { data: [] }]);
×
310
    })
311
    .then(([{ data }, { data: guiTags }]) => {
312
      if (!guiTags.length) {
4!
313
        return Promise.resolve();
×
314
      }
315
      const { releases, saas } = data;
4✔
316
      const latestRelease = getLatestRelease(getLatestRelease(releases));
4✔
317
      const { latestRepos, latestVersions } = latestRelease.repos.reduce(
4✔
318
        (accu, item) => {
319
          if (repoKeyMap[item.name]) {
20✔
320
            accu.latestVersions[repoKeyMap[item.name]] = getComparisonCompatibleVersion(item.version);
12✔
321
          }
322
          accu.latestRepos[item.name] = getComparisonCompatibleVersion(item.version);
20✔
323
          return accu;
20✔
324
        },
325
        { latestVersions: { ...getState().app.versionInformation }, latestRepos: {} }
326
      );
327
      const info = deductSaasState(latestRelease, guiTags, saas);
4✔
328
      return Promise.resolve(
4✔
329
        dispatch({
330
          type: SET_VERSION_INFORMATION,
331
          docsVersion: getState().app.docsVersion,
332
          value: {
333
            ...latestVersions,
334
            backend: info,
335
            GUI: info,
336
            latestRelease: {
337
              releaseDate: latestRelease.release_date,
338
              repos: latestRepos
339
            }
340
          }
341
        })
342
      );
343
    });
344
};
345

346
export const setSearchState = searchState => (dispatch, getState) => {
183✔
347
  const currentState = getState().app.searchState;
4✔
348
  let nextState = {
4✔
349
    ...currentState,
350
    ...searchState,
351
    sort: {
352
      ...currentState.sort,
353
      ...searchState.sort
354
    }
355
  };
356
  let tasks = [];
4✔
357
  // eslint-disable-next-line no-unused-vars
358
  const { isSearching: currentSearching, deviceIds: currentDevices, searchTotal: currentTotal, ...currentRequestState } = currentState;
4✔
359
  // eslint-disable-next-line no-unused-vars
360
  const { isSearching: nextSearching, deviceIds: nextDevices, searchTotal: nextTotal, ...nextRequestState } = nextState;
4✔
361
  if (nextRequestState.searchTerm && !deepCompare(currentRequestState, nextRequestState)) {
4✔
362
    nextState.isSearching = true;
2✔
363
    tasks.push(
2✔
364
      dispatch(searchDevices(nextState))
365
        .then(results => {
366
          const searchResult = results[results.length - 1];
2✔
367
          return dispatch(setSearchState({ ...searchResult, isSearching: false }));
2✔
368
        })
369
        .catch(() => dispatch(setSearchState({ isSearching: false, searchTotal: 0 })))
×
370
    );
371
  }
372
  tasks.push(dispatch({ type: SET_SEARCH_STATE, state: nextState }));
4✔
373
  return Promise.all(tasks);
4✔
374
};
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