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

mendersoftware / gui / 1301920191

23 May 2024 07:13AM UTC coverage: 83.42% (-16.5%) from 99.964%
1301920191

Pull #4421

gitlab-ci

mzedel
fix: fixed an issue that sometimes prevented reopening paginated auditlog links

Ticket: None
Changelog: Title
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4421: MEN-7034 - device information in auditlog entries

4456 of 6367 branches covered (69.99%)

34 of 35 new or added lines in 7 files covered. (97.14%)

1668 existing lines in 162 files now uncovered.

8473 of 10157 relevant lines covered (83.42%)

140.52 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();
184✔
52

53
export const commonErrorFallback = 'Please check your connection.';
184✔
54
export const commonErrorHandler = (err, errorContext, dispatch, fallback, mightBeAuthRelated = false) => {
184✔
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);
184✔
63

64
const featureFlags = [
184✔
65
  'hasAuditlogs',
66
  'hasMultitenancy',
67
  'hasDeltaProgress',
68
  'hasDeviceConfig',
69
  'hasDeviceConnect',
70
  'hasReporting',
71
  'hasMonitor',
72
  'isEnterprise'
73
];
74
export const parseEnvironmentInfo = () => (dispatch, getState) => {
184✔
75
  const state = getState();
9✔
76
  let onboardingComplete = state.onboarding.complete || !!JSON.parse(window.localStorage.getItem('onboardingComplete') ?? 'false');
9✔
77
  let demoArtifactPort = 85;
9✔
78
  let environmentData = {};
9✔
79
  let environmentFeatures = {};
9✔
80
  let versionInfo = {};
9✔
81
  if (mender_environment) {
9!
82
    const {
83
      features = {},
×
84
      demoArtifactPort: port,
85
      disableOnboarding,
86
      hostAddress,
87
      hostedAnnouncement,
88
      integrationVersion,
89
      isDemoMode,
90
      menderVersion,
91
      menderArtifactVersion,
92
      metaMenderVersion,
93
      recaptchaSiteKey,
94
      services = {},
×
95
      stripeAPIKey,
96
      trackerCode
97
    } = mender_environment;
9✔
98
    onboardingComplete = stringToBoolean(features.isEnterprise) || stringToBoolean(disableOnboarding) || onboardingComplete;
9✔
99
    demoArtifactPort = port || demoArtifactPort;
9✔
100
    environmentData = {
9✔
101
      hostedAnnouncement: hostedAnnouncement || state.app.hostedAnnouncement,
18✔
102
      hostAddress: hostAddress || state.app.hostAddress,
18✔
103
      recaptchaSiteKey: recaptchaSiteKey || state.app.recaptchaSiteKey,
18✔
104
      stripeAPIKey: stripeAPIKey || state.app.stripeAPIKey,
18✔
105
      trackerCode: trackerCode || state.app.trackerCode
18✔
106
    };
107
    environmentFeatures = {
9✔
108
      ...featureFlags.reduce((accu, flag) => ({ ...accu, [flag]: stringToBoolean(features[flag]) }), {}),
72✔
109
      // the check in features is purely kept as a local override, it shouldn't become relevant for production again
110
      isHosted: features.isHosted || window.location.hostname.includes('hosted.mender.io'),
18✔
111
      isDemoMode: stringToBoolean(isDemoMode || features.isDemoMode)
18✔
112
    };
113
    versionInfo = {
9✔
114
      docs: isNaN(integrationVersion.charAt(0)) ? '' : integrationVersion.split('.').slice(0, 2).join('.'),
9!
115
      remainder: {
116
        Integration: getComparisonCompatibleVersion(integrationVersion),
117
        'Mender-Client': getComparisonCompatibleVersion(menderVersion),
118
        'Mender-Artifact': menderArtifactVersion,
119
        'Meta-Mender': metaMenderVersion,
120
        Deployments: services.deploymentsVersion,
121
        Deviceauth: services.deviceauthVersion,
122
        Inventory: services.inventoryVersion,
123
        GUI: services.guiVersion
124
      }
125
    };
126
  }
127
  return Promise.all([
9✔
128
    dispatch({ type: SUCCESSFULLY_LOGGED_IN, value: getSessionInfo() }),
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, onboardingState, tasks }) => {
184✔
139
  if (!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
    showTips: onboardingState.showTips
146
  });
UNCOV
147
  if (welcomeTip) {
×
UNCOV
148
    tasks.push(dispatch(setSnackbar('open', TIMEOUTS.refreshDefault, '', welcomeTip, () => {}, true)));
×
149
  }
150
  // try to retrieve full device details for onboarding devices to ensure ips etc. are available
151
  // we only load the first few/ 20 devices, as it is possible the onboarding is left dangling
152
  // and a lot of devices are present and we don't want to flood the backend for this
UNCOV
153
  return devicesByStatus[DEVICE_STATES.accepted].deviceIds.reduce((accu, id) => {
×
UNCOV
154
    accu.push(dispatch(getDeviceById(id)));
×
UNCOV
155
    return accu;
×
156
  }, tasks);
157
};
158

159
const interpretAppData = () => (dispatch, getState) => {
184✔
160
  const state = getState();
4✔
161
  let { columnSelection = [], trackingConsentGiven: hasTrackingEnabled, tooltips = {} } = getUserSettingsSelector(state);
4!
162
  let settings = {};
4✔
163
  if (cookies.get('_ga') && typeof hasTrackingEnabled === 'undefined') {
4!
UNCOV
164
    settings.trackingConsentGiven = true;
×
165
  }
166
  let tasks = [
4✔
UNCOV
167
    dispatch(setDeviceListState({ selectedAttributes: columnSelection.map(column => ({ attribute: column.key, scope: column.scope })) })),
×
168
    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
169
    dispatch(saveUserSettings(settings))
170
  ];
171
  tasks = maybeAddOnboardingTasks({ devicesByStatus: state.devices.byStatus, dispatch, tasks, onboardingState: state.onboarding });
4✔
172
  // the following is used as a migration and initialization of the stored identity attribute
173
  // changing the default device attribute to the first non-deviceId attribute, unless a stored
174
  // id attribute setting exists
175
  const identityOptions = state.devices.filteringAttributes.identityAttributes.filter(attribute => !['id', 'Device ID', 'status'].includes(attribute));
5✔
176
  const { id_attribute } = state.users.globalSettings;
4✔
177
  if (!id_attribute && identityOptions.length) {
4✔
178
    tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute: identityOptions[0], scope: 'identity' } })));
2✔
179
  } else if (typeof id_attribute === 'string') {
2!
UNCOV
180
    let attribute = id_attribute;
×
UNCOV
181
    if (attribute === 'Device ID') {
×
UNCOV
182
      attribute = 'id';
×
183
    }
UNCOV
184
    tasks.push(dispatch(saveGlobalSettings({ id_attribute: { attribute, scope: 'identity' } })));
×
185
  }
186
  return Promise.all(tasks);
4✔
187
};
188

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

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

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

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

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

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

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

288
const repoKeyMap = {
184✔
289
  integration: 'Integration',
290
  mender: 'Mender-Client',
291
  'mender-artifact': 'Mender-Artifact'
292
};
293

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

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

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