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

mendersoftware / gui / 908425489

pending completion
908425489

Pull #3799

gitlab-ci

mzedel
chore: aligned loader usage in devices list with deployment devices list

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3799: MEN-6553

4406 of 6423 branches covered (68.6%)

18 of 19 new or added lines in 3 files covered. (94.74%)

1777 existing lines in 167 files now uncovered.

8329 of 10123 relevant lines covered (82.28%)

144.7 hits per line

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

81.94
/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 { 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_SHOW_HELP } from '../constants/userConstants';
32
import { deepCompare, extractErrorMessage, preformatWithRequestID, stringToBoolean } from '../helpers';
33
import { getCurrentUser, 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 { 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();
190✔
52

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

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

64
const featureFlags = [
190✔
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) => {
190✔
77
  const state = getState();
11✔
78
  let onboardingComplete = state.onboarding.complete || !!JSON.parse(window.localStorage.getItem('onboardingComplete') ?? 'false');
11✔
79
  let demoArtifactPort = 85;
11✔
80
  let environmentData = {};
11✔
81
  let environmentFeatures = {};
11✔
82
  let versionInfo = {};
11✔
83
  if (mender_environment) {
11!
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;
11✔
100
    onboardingComplete = stringToBoolean(features.isEnterprise) || stringToBoolean(disableOnboarding) || onboardingComplete;
11✔
101
    demoArtifactPort = port || demoArtifactPort;
11✔
102
    environmentData = {
11✔
103
      hostedAnnouncement: hostedAnnouncement || state.app.hostedAnnouncement,
22✔
104
      hostAddress: hostAddress || state.app.hostAddress,
22✔
105
      recaptchaSiteKey: recaptchaSiteKey || state.app.recaptchaSiteKey,
22✔
106
      stripeAPIKey: stripeAPIKey || state.app.stripeAPIKey,
22✔
107
      trackerCode: trackerCode || state.app.trackerCode
22✔
108
    };
109
    environmentFeatures = {
11✔
110
      ...featureFlags.reduce((accu, flag) => ({ ...accu, [flag]: stringToBoolean(features[flag]) }), {}),
110✔
111
      isHosted: stringToBoolean(features.isHosted) || window.location.hostname.includes('hosted.mender.io'),
22✔
112
      isDemoMode: stringToBoolean(isDemoMode || features.isDemoMode)
22✔
113
    };
114
    versionInfo = {
11✔
115
      docs: isNaN(integrationVersion.charAt(0)) ? '' : integrationVersion.split('.').slice(0, 2).join('.'),
11!
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([
11✔
129
    dispatch(setOnboardingComplete(onboardingComplete)),
130
    dispatch(setDemoArtifactPort(demoArtifactPort)),
131
    dispatch({ type: SET_FEATURES, value: environmentFeatures }),
132
    dispatch({ type: SET_VERSION_INFORMATION, docsVersion: versionInfo.docs, value: versionInfo.remainder }),
133
    dispatch({ type: SET_ENVIRONMENT_DATA, value: environmentData }),
134
    dispatch(getLatestReleaseInfo())
135
  ]);
136
};
137

138
const maybeAddOnboardingTasks = ({ devicesByStatus, dispatch, showHelptips, onboardingState, tasks }) => {
190✔
139
  if (!(showHelptips && onboardingState.showTips) || onboardingState.complete) {
4!
140
    return tasks;
4✔
141
  }
UNCOV
142
  const welcomeTip = getOnboardingComponentFor(onboardingSteps.ONBOARDING_START, {
×
143
    progress: onboardingState.progress,
144
    complete: onboardingState.complete,
145
    showHelptips,
146
    showTips: onboardingState.showTips
147
  });
UNCOV
148
  if (welcomeTip) {
×
UNCOV
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
UNCOV
154
  return devicesByStatus[DEVICE_STATES.accepted].deviceIds.reduce((accu, id) => {
×
UNCOV
155
    accu.push(dispatch(getDeviceById(id)));
×
UNCOV
156
    return accu;
×
157
  }, tasks);
158
};
159

160
const processUserCookie = (user, showHelptips) => {
190✔
161
  const userCookie = cookies.get(user.id);
4✔
162
  if (userCookie && userCookie.help !== 'undefined') {
4!
UNCOV
163
    const { help, ...crumbles } = userCookie;
×
164
    // got user cookie with pre-existing value
UNCOV
165
    showHelptips = help;
×
166
    // store only remaining cookie values, to allow relying on stored settings from now on
UNCOV
167
    if (!Object.keys(crumbles).length) {
×
UNCOV
168
      cookies.remove(user.id);
×
169
    } else {
UNCOV
170
      cookies.set(user.id, crumbles);
×
171
    }
172
  }
173
  return showHelptips;
4✔
174
};
175

176
export const initializeAppData = () => (dispatch, getState) => {
190✔
177
  let tasks = [
4✔
178
    dispatch(parseEnvironmentInfo()),
179
    dispatch(getUserSettings()),
180
    dispatch(getGlobalSettings()),
181
    dispatch(getDeviceAttributes()),
182
    dispatch(getDeploymentsByStatus(DEPLOYMENT_STATES.finished, undefined, undefined, undefined, undefined, undefined, undefined, false)),
183
    dispatch(getDeploymentsByStatus(DEPLOYMENT_STATES.inprogress)),
184
    dispatch(getDevicesByStatus(DEVICE_STATES.accepted)),
185
    dispatch(getDevicesByStatus(DEVICE_STATES.pending)),
186
    dispatch(getDevicesByStatus(DEVICE_STATES.preauth)),
187
    dispatch(getDevicesByStatus(DEVICE_STATES.rejected)),
188
    dispatch(getDynamicGroups()),
189
    dispatch(getGroups()),
190
    dispatch(getIntegrations()),
191
    dispatch(getReleases()),
192
    dispatch(getDeviceLimit()),
193
    dispatch(getRoles()),
194
    dispatch(setFirstLoginAfterSignup(cookies.get('firstLoginAfterSignup')))
195
  ];
196
  const multitenancy = getState().app.features.hasMultitenancy || getState().app.features.isEnterprise || getState().app.features.isHosted;
4✔
197
  if (multitenancy) {
4✔
198
    tasks.push(dispatch(getUserOrganization()));
3✔
199
  }
200
  return Promise.all(tasks).then(() => {
4✔
201
    const state = getState();
4✔
202
    const user = getCurrentUser(state);
4✔
203
    let tasks = [];
4✔
204
    let { columnSelection = [], showHelptips = state.users.showHelptips, trackingConsentGiven: hasTrackingEnabled } = getUserSettingsSelector(state);
4!
205
    tasks.push(dispatch(setDeviceListState({ selectedAttributes: columnSelection.map(column => ({ attribute: column.key, scope: column.scope })) })));
4✔
206
    // checks if user id is set and if cookie for helptips exists for that user
207
    showHelptips = processUserCookie(user, showHelptips);
4✔
208
    tasks = maybeAddOnboardingTasks({ devicesByStatus: state.devices.byStatus, dispatch, tasks, onboardingState: state.onboarding, showHelptips });
4✔
209
    tasks.push(dispatch({ type: SET_SHOW_HELP, show: showHelptips }));
4✔
210
    let settings = { showHelptips };
4✔
211
    if (cookies.get('_ga') && typeof hasTrackingEnabled === 'undefined') {
4!
UNCOV
212
      settings.trackingConsentGiven = true;
×
213
    }
214
    tasks.push(dispatch(saveUserSettings(settings)));
4✔
215
    // the following is used as a migration and initialization of the stored identity attribute
216
    // changing the default device attribute to the first non-deviceId attribute, unless a stored
217
    // id attribute setting exists
218
    const identityOptions = state.devices.filteringAttributes.identityAttributes.filter(attribute => !['id', 'Device ID', 'status'].includes(attribute));
5✔
219
    const { id_attribute } = state.users.globalSettings;
4✔
220
    if (!id_attribute && identityOptions.length) {
4✔
221
      tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute: identityOptions[0], scope: 'identity' } })));
2✔
222
    } else if (typeof id_attribute === 'string') {
2!
UNCOV
223
      let attribute = id_attribute;
×
UNCOV
224
      if (attribute === 'Device ID') {
×
UNCOV
225
        attribute = 'id';
×
226
      }
UNCOV
227
      tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute, scope: 'identity' } })));
×
228
    }
229
    return Promise.all(tasks);
4✔
230
  });
231
};
232

233
/*
234
  General
235
*/
236
export const setSnackbar = (message, autoHideDuration, action, children, onClick, onClose) => dispatch =>
190✔
237
  dispatch({
118✔
238
    type: SET_SNACKBAR,
239
    snackbar: {
240
      open: message ? true : false,
118✔
241
      message,
242
      maxWidth: '900px',
243
      autoHideDuration,
244
      action,
245
      children,
246
      onClick,
247
      onClose
248
    }
249
  });
250

251
export const setFirstLoginAfterSignup = firstLoginAfterSignup => dispatch => {
190✔
252
  cookies.set('firstLoginAfterSignup', !!firstLoginAfterSignup, { maxAge: 60, path: '/', domain: '.mender.io', sameSite: false });
8✔
253
  dispatch({ type: SET_FIRST_LOGIN_AFTER_SIGNUP, firstLoginAfterSignup: !!firstLoginAfterSignup });
8✔
254
};
255

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

276
export const setVersionInfo = info => (dispatch, getState) =>
190✔
277
  Promise.resolve(
2✔
278
    dispatch({
279
      type: SET_VERSION_INFORMATION,
280
      docsVersion: getState().app.docsVersion,
281
      value: {
282
        ...getState().app.versionInformation,
283
        ...info
284
      }
285
    })
286
  );
287

288
const versionRegex = new RegExp(/\d+\.\d+/);
190✔
289
const getLatestRelease = thing => {
190✔
290
  const latestKey = Object.keys(thing)
8✔
291
    .filter(key => versionRegex.test(key))
20✔
292
    .sort()
293
    .reverse()[0];
294
  return thing[latestKey];
8✔
295
};
296

297
const repoKeyMap = {
190✔
298
  integration: 'Integration',
299
  mender: 'Mender-Client',
300
  'mender-artifact': 'Mender-Artifact'
301
};
302

303
const deductSaasState = (latestRelease, guiTags, saasReleases) => {
190✔
304
  const latestGuiTag = guiTags[0].name;
4✔
305
  const latestSaasRelease = latestGuiTag.startsWith('saas-v') ? { date: latestGuiTag.split('-v')[1].replaceAll('.', '-'), tag: latestGuiTag } : saasReleases[0];
4!
306
  return latestSaasRelease.date > latestRelease.release_date ? latestSaasRelease.tag : latestRelease.release;
4!
307
};
308

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

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