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

mendersoftware / gui / 1493849842

13 Oct 2024 07:39AM UTC coverage: 83.457% (-16.5%) from 99.965%
1493849842

Pull #4531

gitlab-ci

web-flow
chore: Bump send and express in /tests/e2e_tests

Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4531: chore: Bump send and express in /tests/e2e_tests

4486 of 6422 branches covered (69.85%)

8551 of 10246 relevant lines covered (83.46%)

151.3 hits per line

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

86.57
/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 moment from 'moment';
15
import Cookies from 'universal-cookie';
16

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

52
const cookies = new Cookies();
184✔
53

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

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

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

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

160
const interpretAppData = () => (dispatch, getState) => {
184✔
161
  const state = getState();
9✔
162
  let { columnSelection = [], trackingConsentGiven: hasTrackingEnabled, tooltips = {} } = getUserSettingsSelector(state);
9!
163
  let settings = {};
9✔
164
  if (cookies.get('_ga') && typeof hasTrackingEnabled === 'undefined') {
9!
165
    settings.trackingConsentGiven = true;
×
166
  }
167
  let tasks = [
9✔
168
    dispatch(setDeviceListState({ selectedAttributes: columnSelection.map(column => ({ attribute: column.key, scope: column.scope })) })),
×
169
    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
170
    dispatch(saveUserSettings(settings))
171
  ];
172
  tasks = maybeAddOnboardingTasks({ devicesByStatus: state.devices.byStatus, dispatch, tasks, onboardingState: state.onboarding });
9✔
173

174
  const { canManageUsers } = getUserCapabilities(getState());
9✔
175
  const { interval, intervalUnit } = getOfflineThresholdSettings(getState());
9✔
176
  if (canManageUsers && intervalUnit && intervalUnit !== timeUnits.days) {
9✔
177
    const duration = moment.duration(interval, intervalUnit);
5✔
178
    const days = duration.asDays();
5✔
179
    if (days < 1) {
5✔
180
      tasks.push(Promise.resolve(setTimeout(() => dispatch(setShowStartupNotification(true)), TIMEOUTS.fiveSeconds)));
3✔
181
    } else {
182
      const roundedDays = Math.max(1, Math.round(days));
2✔
183
      tasks.push(dispatch(saveGlobalSettings({ offlineThreshold: { interval: roundedDays, intervalUnit: timeUnits.days } })));
2✔
184
    }
185
  }
186

187
  // the following is used as a migration and initialization of the stored identity attribute
188
  // changing the default device attribute to the first non-deviceId attribute, unless a stored
189
  // id attribute setting exists
190
  const identityOptions = state.devices.filteringAttributes.identityAttributes.filter(attribute => !['id', 'Device ID', 'status'].includes(attribute));
11✔
191
  const { id_attribute } = state.users.globalSettings;
9✔
192
  if (!id_attribute && identityOptions.length) {
9✔
193
    tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute: identityOptions[0], scope: 'identity' } })));
3✔
194
  } else if (typeof id_attribute === 'string') {
6!
195
    let attribute = id_attribute;
×
196
    if (attribute === 'Device ID') {
×
197
      attribute = 'id';
×
198
    }
199
    tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute, scope: 'identity' } })));
×
200
  }
201
  return Promise.all(tasks);
9✔
202
};
203

204
const retrieveAppData = () => (dispatch, getState) => {
184✔
205
  let tasks = [
9✔
206
    dispatch(parseEnvironmentInfo()),
207
    dispatch(getUserSettings()),
208
    dispatch(getGlobalSettings()),
209
    dispatch(getDeviceAttributes()),
210
    dispatch(getDeploymentsByStatus(DEPLOYMENT_STATES.finished, undefined, undefined, undefined, undefined, undefined, undefined, false)),
211
    dispatch(getDeploymentsByStatus(DEPLOYMENT_STATES.inprogress)),
212
    dispatch(getDevicesByStatus(DEVICE_STATES.accepted)),
213
    dispatch(getDevicesByStatus(DEVICE_STATES.pending)),
214
    dispatch(getDevicesByStatus(DEVICE_STATES.preauth)),
215
    dispatch(getDevicesByStatus(DEVICE_STATES.rejected)),
216
    dispatch(getDynamicGroups()),
217
    dispatch(getGroups()),
218
    dispatch(getIntegrations()),
219
    dispatch(getReleases()),
220
    dispatch(getDeviceLimit()),
221
    dispatch(getRoles()),
222
    dispatch(setFirstLoginAfterSignup(stringToBoolean(cookies.get('firstLoginAfterSignup'))))
223
  ];
224
  const { hasMultitenancy, isHosted } = getFeatures(getState());
9✔
225
  const multitenancy = hasMultitenancy || isHosted || getIsEnterprise(getState());
9✔
226
  if (multitenancy) {
9✔
227
    tasks.push(dispatch(getUserOrganization()));
8✔
228
  }
229
  return Promise.all(tasks);
9✔
230
};
231

232
export const initializeAppData = () => dispatch =>
184✔
233
  dispatch(retrieveAppData())
9✔
234
    .then(() => dispatch(interpretAppData()))
9✔
235
    // this is allowed to fail if no user information are available
236
    .catch(err => console.log(extractErrorMessage(err)))
×
237
    .then(() => dispatch(getOnboardingState()));
9✔
238

239
/*
240
  General
241
*/
242
export const setSnackbar = (message, autoHideDuration, action, children, onClick, onClose) => dispatch =>
184✔
243
  dispatch({
132✔
244
    type: SET_SNACKBAR,
245
    snackbar: {
246
      open: message ? true : false,
132✔
247
      message,
248
      maxWidth: '900px',
249
      autoHideDuration,
250
      action,
251
      children,
252
      onClick,
253
      onClose
254
    }
255
  });
256

257
export const setFirstLoginAfterSignup = firstLoginAfterSignup => dispatch => {
184✔
258
  cookies.set('firstLoginAfterSignup', !!firstLoginAfterSignup, { maxAge: 60, path: '/', domain: '.mender.io', sameSite: false });
13✔
259
  dispatch({ type: SET_FIRST_LOGIN_AFTER_SIGNUP, firstLoginAfterSignup: !!firstLoginAfterSignup });
13✔
260
};
261

262
const dateFunctionMap = {
184✔
263
  getDays: 'getDate',
264
  setDays: 'setDate'
265
};
266
export const setOfflineThreshold = () => (dispatch, getState) => {
184✔
267
  const { interval, intervalUnit } = getOfflineThresholdSettings(getState());
25✔
268
  const today = new Date();
25✔
269
  const intervalName = `${intervalUnit.charAt(0).toUpperCase()}${intervalUnit.substring(1)}`;
25✔
270
  const setter = dateFunctionMap[`set${intervalName}`] ?? `set${intervalName}`;
25✔
271
  const getter = dateFunctionMap[`get${intervalName}`] ?? `get${intervalName}`;
25✔
272
  today[setter](today[getter]() - interval);
25✔
273
  let value;
274
  try {
25✔
275
    value = today.toISOString();
25✔
276
  } catch {
277
    return Promise.resolve(dispatch(setSnackbar('There was an error saving the offline threshold, please check your settings.')));
×
278
  }
279
  return Promise.resolve(dispatch({ type: SET_OFFLINE_THRESHOLD, value }));
25✔
280
};
281

282
export const setVersionInfo = info => (dispatch, getState) =>
184✔
283
  Promise.resolve(
2✔
284
    dispatch({
285
      type: SET_VERSION_INFORMATION,
286
      docsVersion: getState().app.docsVersion,
287
      value: {
288
        ...getState().app.versionInformation,
289
        ...info
290
      }
291
    })
292
  );
293

294
const versionRegex = new RegExp(/\d+\.\d+/);
184✔
295
const getLatestRelease = thing => {
184✔
296
  const latestKey = Object.keys(thing)
16✔
297
    .filter(key => versionRegex.test(key))
40✔
298
    .sort()
299
    .reverse()[0];
300
  return thing[latestKey];
16✔
301
};
302

303
const repoKeyMap = {
184✔
304
  integration: 'Integration',
305
  mender: 'Mender-Client',
306
  'mender-artifact': 'Mender-Artifact'
307
};
308

309
const deductSaasState = (latestRelease, guiTags, saasReleases) => {
184✔
310
  const latestGuiTag = guiTags.length ? guiTags[0].name : '';
8!
311
  const latestSaasRelease = latestGuiTag.startsWith('saas-v') ? { date: latestGuiTag.split('-v')[1].replaceAll('.', '-'), tag: latestGuiTag } : saasReleases[0];
8!
312
  return latestSaasRelease.date > latestRelease.release_date ? latestSaasRelease.tag : latestRelease.release;
8!
313
};
314

315
export const getLatestReleaseInfo = () => (dispatch, getState) => {
184✔
316
  if (!getState().app.features.isHosted) {
19✔
317
    return Promise.resolve();
11✔
318
  }
319
  return Promise.all([GeneralApi.get('/versions.json'), GeneralApi.get('/tags.json')])
8✔
320
    .catch(err => {
321
      console.log('init error:', extractErrorMessage(err));
×
322
      return Promise.resolve([{ data: {} }, { data: [] }]);
×
323
    })
324
    .then(([{ data }, { data: guiTags }]) => {
325
      if (!guiTags.length) {
8!
326
        return Promise.resolve();
×
327
      }
328
      const { releases, saas } = data;
8✔
329
      const latestRelease = getLatestRelease(getLatestRelease(releases));
8✔
330
      const { latestRepos, latestVersions } = latestRelease.repos.reduce(
8✔
331
        (accu, item) => {
332
          if (repoKeyMap[item.name]) {
40✔
333
            accu.latestVersions[repoKeyMap[item.name]] = getComparisonCompatibleVersion(item.version);
24✔
334
          }
335
          accu.latestRepos[item.name] = getComparisonCompatibleVersion(item.version);
40✔
336
          return accu;
40✔
337
        },
338
        { latestVersions: { ...getState().app.versionInformation }, latestRepos: {} }
339
      );
340
      const info = deductSaasState(latestRelease, guiTags, saas);
8✔
341
      return Promise.resolve(
8✔
342
        dispatch({
343
          type: SET_VERSION_INFORMATION,
344
          docsVersion: getState().app.docsVersion,
345
          value: {
346
            ...latestVersions,
347
            backend: info,
348
            GUI: info,
349
            latestRelease: {
350
              releaseDate: latestRelease.release_date,
351
              repos: latestRepos
352
            }
353
          }
354
        })
355
      );
356
    });
357
};
358

359
export const setSearchState = searchState => (dispatch, getState) => {
184✔
360
  const currentState = getState().app.searchState;
4✔
361
  let nextState = {
4✔
362
    ...currentState,
363
    ...searchState,
364
    sort: {
365
      ...currentState.sort,
366
      ...searchState.sort
367
    }
368
  };
369
  let tasks = [];
4✔
370
  // eslint-disable-next-line no-unused-vars
371
  const { isSearching: currentSearching, deviceIds: currentDevices, searchTotal: currentTotal, ...currentRequestState } = currentState;
4✔
372
  // eslint-disable-next-line no-unused-vars
373
  const { isSearching: nextSearching, deviceIds: nextDevices, searchTotal: nextTotal, ...nextRequestState } = nextState;
4✔
374
  if (nextRequestState.searchTerm && !deepCompare(currentRequestState, nextRequestState)) {
4✔
375
    nextState.isSearching = true;
2✔
376
    tasks.push(
2✔
377
      dispatch(searchDevices(nextState))
378
        .then(results => {
379
          const searchResult = results[results.length - 1];
2✔
380
          return dispatch(setSearchState({ ...searchResult, isSearching: false }));
2✔
381
        })
382
        .catch(() => dispatch(setSearchState({ isSearching: false, searchTotal: 0 })))
×
383
    );
384
  }
385
  tasks.push(dispatch({ type: SET_SEARCH_STATE, state: nextState }));
4✔
386
  return Promise.all(tasks);
4✔
387
};
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